/**
 * The gantries' obstruction lights, and the one thing about them a picture
 * cannot check.
 *
 * A screenshot tells you there are red dots over San Pedro Bay. It does not
 * tell you they are on the masts *this build drew* rather than on a mast height
 * computed a second time in a second file — and that is the whole failure mode,
 * because it does not look like a failure. A light hung off an independently
 * recomputed apex is a row of red dots hovering a few metres over a row of
 * cranes, which at any framing this board is looked at from is indistinguishable
 * from a row of red dots on a row of cranes. It would ship.
 *
 * So the assertion is the *identity*: for every gantry the pack declares, there
 * is exactly one light, and its height is the height of the tallest mast box in
 * that gantry's own instance list plus the clearance. Nothing here re-derives
 * the apex; both sides come out of `ports.ts`, which is the point. This is the
 * twin of `nightInfrastructure.test.ts`'s argument about `bridgeLights` and the
 * deck it hangs lamps off.
 *
 * `craneLights` is also the reason `nightlights.ts` may import `ports.ts` at
 * all, so the second thing asserted here is that this seam stays what
 * CONTRACT §4 allows it to be: **positions, not lights.** The function returns
 * numbers. If it ever returns something that can illuminate a surface, this
 * fails at compile time and then again here.
 *
 * The world is Southern California's real projection — `latScale: 285` puts one
 * scene unit at 390.6 m, and heights carry the pack's 3.4x exaggeration. A tidy
 * 1:1 fake would pass while every apex sat 3.4 times too high, which is exactly
 * the class of bug `ports.ts` already has a comment about.
 */

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";

import { craneLights, craneStations, createPorts } from "../../engine/ports.ts";
import { LOS_ANGELES, LONG_BEACH, PORTS } from "../../cities/socal.ts";
import type { World } from "../../engine/world.ts";

/** `socal.ts`: centre 33.82 / -118.05, `latScale: 285`, exaggeration 3.4. */
function socalWorld(): World {
  const centre = { lat: 33.82, lng: -118.05 };
  const latScale = 285;
  const lngScale = latScale * Math.cos((centre.lat * Math.PI) / 180);
  const metresPerUnit = 111_320 / latScale;
  return {
    project(lat: number, lng: number): [number, number] {
      return [(lng - centre.lng) * lngScale, -(lat - centre.lat) * latScale];
    },
    groundAt: () => 0,
    metresPerUnit,
    metres(value: number): number {
      return (value / metresPerUnit) * 3.4;
    },
  } as unknown as World;
}

/** Every gantry the two packs declare, across both ports. */
function gantryCount(): number {
  return PORTS.flatMap((port) => port.cranes ?? []).reduce((total, row) => total + row.count, 0);
}

describe("the container gantries mark themselves after dark", () => {
  it("puts one light over every gantry on the board, and no more", () => {
    const world = socalWorld();
    let lights = 0;
    for (const port of PORTS) lights += craneLights(world, port).heads.length / 3;
    assert.equal(gantryCount(), 56, "San Pedro Bay is authored with fifty-six gantries");
    assert.equal(lights, 56, "one obstruction light per gantry, never per crane *row*");
  });

  it("hands back numbers, not anything that could light a surface", () => {
    const world = socalWorld();
    const { heads } = craneLights(world, LOS_ANGELES);
    assert.ok(heads.length > 0);
    assert.equal(heads.length % 3, 0, "flat xyz triples, as `bridgeLights` returns");
    for (const value of heads) {
      assert.equal(typeof value, "number");
      assert.ok(Number.isFinite(value), "a light off the end of a board is a light nobody finds");
    }
  });

  /**
   * The identity this file exists for.
   *
   * `createPorts` builds the mast; `craneLights` builds the lamp. They must
   * agree, and the only honest way to check that is to measure the drawn mesh
   * rather than to recompute the arithmetic a third time here — a test that
   * recomputes it is a test that passes when all three copies drift together.
   *
   * The apex is found as the highest point of any instance box, using the box's
   * true rotated half-extent: the mast is a strut standing at near eighty
   * degrees, so most of its length is in world Y and almost none in the local X
   * it is long on.
   */
  it("hangs each light on the mast the same build drew, not on a second guess at it", () => {
    const world = socalWorld();
    // One port, one row, all booms down, so the tallest thing on the board is
    // unambiguously a mast and not somebody's raised boom.
    const row = (LONG_BEACH.cranes ?? [])[0];
    assert.ok(row);
    const port = { ...LONG_BEACH, cranes: [{ ...row, idleFraction: 0 }] };
    const group = createPorts(world, [port]);

    let mesh: THREE.InstancedMesh | null = null;
    group.traverse((object) => {
      if (object instanceof THREE.InstancedMesh && object.name === "ports:cranes") mesh = object;
    });
    assert.ok(mesh, "no crane mesh to measure against");
    const gantries = mesh as THREE.InstancedMesh;

    const matrix = new THREE.Matrix4();
    const position = new THREE.Vector3();
    const quaternion = new THREE.Quaternion();
    const scale = new THREE.Vector3();
    const basis = new THREE.Matrix4();
    let apex = -Infinity;
    for (let i = 0; i < gantries.count; i += 1) {
      gantries.getMatrixAt(i, matrix);
      matrix.decompose(position, quaternion, scale);
      basis.makeRotationFromQuaternion(quaternion);
      const e = basis.elements;
      const halfY =
        0.5 * (scale.x * Math.abs(e[1]!) + scale.y * Math.abs(e[5]!) + scale.z * Math.abs(e[9]!));
      apex = Math.max(apex, position.y + halfY);
    }
    assert.ok(Number.isFinite(apex));

    const { heads } = craneLights(world, port);
    assert.equal(heads.length / 3, craneStations(row).length);
    // Every light at the same height — one row of gantries is one height — and
    // that height within a member of the drawn apex. The clearance is what stops
    // the sprite being half-eaten by the apex cap's own depth; more than a
    // member above it and the light has come off the crane.
    const member = Math.max(5, 12.5) / world.metresPerUnit;
    for (let i = 1; i < heads.length; i += 3) {
      const y = heads[i]!;
      assert.ok(
        y > apex - member * 1.2 && y < apex + member * 1.6,
        `light at ${y.toFixed(4)} against a drawn apex of ${apex.toFixed(4)} — ` +
          "the lamp and the mast have stopped agreeing, which looks like nothing at all",
      );
    }
  });

  /**
   * A light is a *marker*, so it has to be over the mast rather than over the
   * boom — the boom moves and the mast does not, and a lamp that followed the
   * boom would swing out over the water on a working crane and stand over the
   * yard on a parked one. Working and parked rows must agree.
   */
  it("does not move when the boom does", () => {
    const world = socalWorld();
    const row = (LOS_ANGELES.cranes ?? [])[0];
    assert.ok(row);
    const working = craneLights(world, { ...LOS_ANGELES, cranes: [{ ...row, idleFraction: 0 }] });
    const parked = craneLights(world, { ...LOS_ANGELES, cranes: [{ ...row, idleFraction: 1 }] });
    assert.equal(working.heads.length, parked.heads.length);
    for (let i = 0; i < working.heads.length; i += 1) {
      assert.ok(
        Math.abs(working.heads[i]! - parked.heads[i]!) < 1e-9,
        "an obstruction light followed the boom; it belongs on the mast",
      );
    }
  });
});
