/**
 * San Pedro Bay as the SoCal pack authors it — the assertions that keep a quay
 * on land and a breakwater the right length.
 *
 * Every coordinate in `socal.ts` is hand-traced by house rule (ARCHITECTURE
 * §3.2), which means it is eyeball-accurate and there is no authority to check
 * it against. What there *is* is a set of relationships that have to hold, and a
 * hand-traced number that breaks one of them is a typo rather than a judgement
 * call. This file is those relationships:
 *
 * - **A quay is on land.** Every vertex of every quay polygon lies inside a
 *   landmass. This is the assertion the whole re-trace of Terminal Island exists
 *   to satisfy: with the old six-point hexagon a quay could be on the water or
 *   buried in the fill, and nothing would have said so.
 * - **A berth is on its quay.** Within 200 m of a quay edge, which at 391 m to
 *   the scene unit is half a scene unit — close enough that a hull placed there
 *   is alongside rather than parked in the yard or moored in the fairway.
 * - **The breakwater is thirteen kilometres.** Between twelve and fifteen, and
 *   it comes out 13.06 against a real federal breakwater of 13.07.
 * - **Nothing is placed from `ports.sqlite`.** All seven rows of that table sit
 *   on an exact arc-minute grid; a coordinate here that lands on one is a
 *   coordinate somebody copied out of it, and it is up to 1,852 m from the water
 *   it claims. This is the cheapest possible guard against the single most
 *   likely way this data goes wrong later.
 * - **The bridges still land.** Re-tracing Terminal Island moved every shoreline
 *   the Vincent Thomas and the Long Beach Gateway touch, and a bridge whose
 *   abutment ends up over open water fails silently — it just looks slightly
 *   wrong from one angle.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";

import { metresBetween, yardCorners } from "../../engine/ports.ts";
import SOCAL_CITY, {
  LONG_BEACH,
  LOS_ANGELES,
  PORTS,
  TERMINAL_ISLAND,
  VINCENT_THOMAS,
  LONG_BEACH_GATEWAY,
} from "../../cities/socal.ts";
import { World } from "../../engine/world.ts";
import type { LatLng, Quay } from "../../engine/types.ts";

const world = new World(SOCAL_CITY);

function onLand(point: LatLng): boolean {
  return world.pointInAny(point[0], point[1], SOCAL_CITY.landmasses);
}

/** Shortest distance from a point to a polygon's boundary, in metres. */
function metresToEdge(point: LatLng, polygon: readonly LatLng[]): number {
  let best = Infinity;
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const from = polygon[j];
    const to = polygon[i];
    if (!from || !to) continue;
    const length = metresBetween(from, to);
    if (length === 0) {
      best = Math.min(best, metresBetween(point, from));
      continue;
    }
    // Project onto the segment in a local metres frame; at this size the earth
    // is flat enough that the error is centimetres.
    const scaleLng = Math.cos((point[0] * Math.PI) / 180);
    const ax = (from[1] - point[1]) * scaleLng;
    const ay = from[0] - point[0];
    const bx = (to[1] - point[1]) * scaleLng;
    const by = to[0] - point[0];
    const dx = bx - ax;
    const dy = by - ay;
    const square = dx * dx + dy * dy;
    const t = square === 0 ? 0 : Math.max(0, Math.min(1, -(ax * dx + ay * dy) / square));
    const nearest = Math.hypot(ax + t * dx, ay + t * dy);
    best = Math.min(best, nearest * 111_320);
  }
  return best;
}

const quays: { port: string; quay: Quay }[] = PORTS.flatMap((port) =>
  (port.quays ?? []).map((quay) => ({ port: port.id, quay })),
);

describe("the pack declares two ports and the engine can find them", () => {
  it("hangs them off the city", () => {
    assert.equal(SOCAL_CITY.ports, PORTS);
    assert.deepEqual(
      PORTS.map((port) => port.id),
      ["USLAX", "USLGB"],
    );
  });

  it("keeps every port record JSON-serialisable", () => {
    // A pack is posted to the terrain Worker as a structured clone. One method
    // on one port record and the whole board stops booting.
    assert.doesNotThrow(() => structuredClone(PORTS));
    assert.deepEqual(JSON.parse(JSON.stringify(PORTS)), JSON.parse(JSON.stringify(PORTS)));
  });
});

describe("nothing is placed from ports.sqlite", () => {
  it("puts no port anchor on an exact arc-minute", () => {
    // Every row of the upstream table has lat*60 and lon*60 whole. One arc-minute
    // here is 1,852 m of latitude — 4.7 scene units — from the water.
    for (const port of PORTS) {
      const latMinutes = port.lat * 60;
      const lngMinutes = port.lng * 60;
      const onGrid =
        Math.abs(latMinutes - Math.round(latMinutes)) < 1e-6 &&
        Math.abs(lngMinutes - Math.round(lngMinutes)) < 1e-6;
      assert.equal(onGrid, false, `${port.id} sits on the arc-minute grid — that is a WPI row`);
    }
  });

  it("puts no quay vertex, berth or crane rail on an exact arc-minute either", () => {
    const suspects: LatLng[] = [
      ...quays.flatMap(({ quay }) => quay.polygon),
      ...PORTS.flatMap((port) => (port.berths ?? []).map((berth): LatLng => [berth.lat, berth.lng])),
      ...PORTS.flatMap((port) => (port.cranes ?? []).flatMap((crane) => [crane.from, crane.to])),
    ];
    assert.ok(suspects.length > 30);
    for (const [lat, lng] of suspects) {
      const onGrid =
        Math.abs(lat * 60 - Math.round(lat * 60)) < 1e-6 &&
        Math.abs(lng * 60 - Math.round(lng * 60)) < 1e-6;
      assert.equal(onGrid, false, `${lat}, ${lng} is on the arc-minute grid`);
    }
  });
});

describe("quays are on land and berths are on quays", () => {
  it("puts every quay vertex inside a landmass", () => {
    assert.ok(quays.length >= 6);
    for (const { port, quay } of quays) {
      for (const vertex of quay.polygon) {
        assert.ok(
          onLand(vertex),
          `${port}/${quay.id} vertex ${vertex.join(", ")} is on open water`,
        );
      }
    }
  });

  it("puts every berth anchor within 200 m of its quay", () => {
    const byId = new Map(quays.map(({ quay }) => [quay.id, quay]));
    let checked = 0;
    for (const port of PORTS) {
      for (const berth of port.berths ?? []) {
        const quay = berth.quayId ? byId.get(berth.quayId) : undefined;
        assert.ok(quay, `${berth.id} names quay ${berth.quayId}, which does not exist`);
        const distance = metresToEdge([berth.lat, berth.lng], quay.polygon);
        assert.ok(distance < 200, `${berth.id} is ${Math.round(distance)} m from its quay`);
        checked += 1;
      }
    }
    assert.equal(checked, 15, "San Pedro Bay is authored with fifteen berths");
  });

  it("puts every berth just off the wall rather than on top of it", () => {
    // A hull whose anchor is inside the quay polygon is a hull parked in the
    // yard. The berth is the water beside the wall, not the wall.
    for (const port of PORTS) {
      for (const berth of port.berths ?? []) {
        assert.equal(
          onLand([berth.lat, berth.lng]),
          false,
          `${berth.id} is inside the landmass — it should be alongside, not ashore`,
        );
      }
    }
  });

  it("puts every yard corner on land", () => {
    for (const port of PORTS) {
      for (const yard of port.yards ?? []) {
        for (const corner of yardCorners(yard)) {
          assert.ok(
            onLand(corner),
            `${port.id}/${yard.id} corner ${corner.map((n) => n.toFixed(4)).join(", ")} is on water`,
          );
        }
      }
    }
  });

  it("puts every crane rail on the quay it serves", () => {
    for (const port of PORTS) {
      for (const crane of port.cranes ?? []) {
        for (const end of [crane.from, crane.to]) {
          assert.ok(onLand(end), `${port.id}/${crane.id} rail end ${end.join(", ")} is on water`);
        }
      }
    }
  });
});

describe("the breakwater", () => {
  it("is between twelve and fifteen kilometres, in three arms", () => {
    const arms = LOS_ANGELES.breakwater ?? [];
    assert.equal(arms.length, 3, "San Pedro, Middle and Long Beach");
    let total = 0;
    for (const arm of arms) {
      for (let i = 1; i < arm.length; i += 1) total += metresBetween(arm[i - 1]!, arm[i]!);
    }
    assert.ok(total > 12_000 && total < 15_000, `${Math.round(total)} m`);
  });

  it("leaves Angels Gate and Queens Gate open", () => {
    const arms = LOS_ANGELES.breakwater ?? [];
    const angels = metresBetween(arms[0]!.at(-1)!, arms[1]![0]!);
    const queens = metresBetween(arms[1]!.at(-1)!, arms[2]![0]!);
    // Both real gates are between five hundred and a thousand metres wide, and a
    // harbour whose arms meet is a lagoon.
    assert.ok(angels > 400 && angels < 1_200, `Angels Gate ${Math.round(angels)} m`);
    assert.ok(queens > 400 && queens < 1_200, `Queens Gate ${Math.round(queens)} m`);
  });

  it("lies in open water for its whole length", () => {
    for (const arm of LOS_ANGELES.breakwater ?? []) {
      for (const point of arm) {
        assert.equal(onLand(point), false, `breakwater point ${point.join(", ")} is inland`);
      }
    }
  });

  it("belongs to the coastal-breakwater harbour and is drawn once", () => {
    // `harbor_type` is CB for both San Pedro ports and CN for Oakland. The arms
    // are one federal structure: declaring them on both would double the
    // geometry for an identical picture, and giving them to a CN harbour would
    // be inventing the largest object on its waterfront.
    for (const port of PORTS) {
      if (port.breakwater) assert.equal(port.harborType, "CB", `${port.id} is not a CB harbour`);
    }
    assert.equal(LONG_BEACH.breakwater, undefined);
  });
});

describe("the dredged channels stay in the water", () => {
  it("keeps every channel vertex off both landmasses", () => {
    for (const port of PORTS) {
      for (const point of port.channel ?? []) {
        assert.equal(onLand(point), false, `${port.id} channel point ${point.join(", ")} is inland`);
      }
    }
  });
});

describe("Terminal Island still carries the two bridges", () => {
  const island = TERMINAL_ISLAND;

  it("lands the Vincent Thomas on the island and San Pedro on the mainland", () => {
    const path = VINCENT_THOMAS.path;
    const mainland = path[0]!;
    const islandEnd = path.at(-1)!;
    assert.equal(world.pointInPolygon(islandEnd[0], islandEnd[1], island), true);
    assert.equal(world.pointInPolygon(mainland[0], mainland[1], island), false);
    assert.equal(onLand(mainland), true);
  });

  it("lands the Long Beach Gateway on the island and Long Beach on the mainland", () => {
    const path = LONG_BEACH_GATEWAY.path;
    const islandEnd = path[0]!;
    const mainland = path.at(-1)!;
    assert.equal(world.pointInPolygon(islandEnd[0], islandEnd[1], island), true);
    assert.equal(world.pointInPolygon(mainland[0], mainland[1], island), false);
  });

  it("carries the comb of slips rather than a hexagon", () => {
    // The point of the re-trace. Six points cannot express a basin; this outline
    // has the West Basin, the East Basin, Fish Harbor and the Pier 400 causeway
    // in it, and every one of them shows up as a reversal in the north-south
    // walk along the north shore.
    assert.ok(island.length >= 24, `${island.length} points`);
    const north = island.filter(([lat]) => lat > 33.755);
    let reversals = 0;
    for (let i = 2; i < north.length; i += 1) {
      const a = north[i - 2]![0];
      const b = north[i - 1]![0];
      const c = north[i]![0];
      if (Math.sign(b - a) !== Math.sign(c - b)) reversals += 1;
    }
    assert.ok(reversals >= 3, `the north shore has ${reversals} basin walls cut into it`);
  });

  it("does not overlap the mainland", () => {
    // The Main Channel and the Back Channel are the two pieces of water this
    // board cannot afford to lose: an island fused to the shore has no harbour
    // in it at all.
    const mainland = SOCAL_CITY.landmasses[0]!;
    for (const point of island) {
      assert.equal(
        world.pointInPolygon(point[0], point[1], mainland),
        false,
        `${point.join(", ")} is inside the mainland`,
      );
    }
  });

  it("keeps the port off the district lattice", () => {
    // The Harbour chapter's whole failure was that Terminal Island sat inside
    // the San Pedro and Long Beach district polygons, so the busiest container
    // terminal in the hemisphere came out as generic industrial blocks. No
    // district may claim a quay.
    for (const { port, quay } of quays) {
      for (const vertex of quay.polygon) {
        for (const district of SOCAL_CITY.districts) {
          assert.equal(
            world.pointInPolygon(vertex[0], vertex[1], district.polygon),
            false,
            `${port}/${quay.id} is inside district ${district.id}; blocks.ts will build on it`,
          );
        }
      }
    }
  });
});

describe("the empty-box figures are the ones that were measured", () => {
  it("leads Los Angeles with July 2026 and says which month it is", () => {
    const throughput = LOS_ANGELES.throughput;
    assert.ok(throughput);
    assert.equal(throughput.asOf, "2026-07");
    const exported = throughput.loadedExport + throughput.emptyExport;
    assert.equal(exported, 460_467);
    assert.equal(throughput.emptyExport, 348_691);
    const share = throughput.emptyExport / exported;
    assert.ok(Math.abs(share - 0.757) < 0.001, `${(share * 100).toFixed(1)}%`);
  });

  it("draws that share in the yards rather than only writing it in a caption", () => {
    for (const yard of LOS_ANGELES.yards ?? []) {
      // The rail yard is the deliberate exception; see the next assertion.
      if (yard.id === "rail-yard") continue;
      assert.equal(yard.emptyShare, 0.757);
    }
    for (const yard of LONG_BEACH.yards ?? []) assert.equal(yard.emptyShare, 0.765);
  });

  it("leaves the one yard nobody counts without a share, rather than guessing one", () => {
    // `emptyShare` absent means unknown, and `yardAtlas` paints an unknown yard
    // in one flat colour. That difference is visible on the board, which is the
    // point: a measured number and an unmeasured one must not look alike.
    const rail = (LOS_ANGELES.yards ?? []).find((yard) => yard.id === "rail-yard");
    assert.ok(rail);
    assert.equal(rail.emptyShare, undefined);
    assert.equal("emptyShare" in rail, false);
  });

  it("carries the freight pair that explains it, and no timestamp on it", () => {
    const rates = LOS_ANGELES.rates ?? [];
    assert.deepEqual(
      rates.map((rate) => [rate.id, rate.usdPerFeu]),
      [
        ["FBX01", 7_491],
        ["FBX02", 347],
      ],
    );
    // `observed_at` upstream is our own read clock; Freightos publishes none.
    for (const rate of rates) assert.equal("asOf" in rate, false);
  });

  it("gives Long Beach no half-written throughput record", () => {
    // Its export split is known — 341,806 empty against 104,843 loaded — and its
    // import halves were never read. `PortThroughput` requires all four, and two
    // real numbers beside two invented ones is the failure the fire layer nearly
    // shipped. No record beats half a record.
    assert.equal(LONG_BEACH.throughput, undefined);
  });

  it("puts the split in the Harbour chapter, which is the card the page shows", () => {
    const chapter = SOCAL_CITY.chapters.find((one) => one.id === "harbour");
    assert.ok(chapter);
    assert.match(chapter.description, /348,691 of 460,467/);
    assert.match(chapter.description, /\$7,491/);
    assert.match(chapter.description, /\$347/);
  });
});
