/**
 * A harbour member is a true-metre object, and every board has to agree.
 *
 * `size(metres, floor)` used to take `floor` in scene units, tuned at 391 m to
 * the unit on Southern California. On the merged California board (1,919 m/unit)
 * those floors always won: a 340 m channel became 1.05 km, and San Pedro Bay
 * read as a river. The floor is metres now. This is the property that would
 * have caught it: two boards five times apart draw the same channel on the
 * water.
 */

import assert from "node:assert/strict";
import test from "node:test";
import * as THREE from "three";

import { createPorts } from "../../engine/ports.ts";
import type { Port } from "../../engine/types.ts";
import type { World } from "../../engine/world.ts";

/**
 * One straight channel and one gantry. Widths come from the kit, not from the
 * pack, so the fixture only has to exist — two points of water, two points of
 * rail.
 */
const SAMPLE: Port = {
  id: "USXXX",
  name: "Sample",
  lat: 33.74,
  lng: -118.27,
  harborType: "CB",
  channel: [
    [33.74, -118.27],
    [33.75, -118.27],
  ],
  cranes: [
    {
      from: [33.74, -118.26],
      to: [33.741, -118.26],
      count: 1,
      bearing: 90,
      height: 82,
      outreach: 70,
      idleFraction: 0,
    },
  ],
};

function board(latScale: number): World {
  const centre = { lat: 33.82, lng: -118.05 };
  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(): number {
      return 0;
    },
    metres(value: number): number {
      return (value / metresPerUnit) * 3.4;
    },
    metresPerUnit,
  } as unknown as World;
}

function namedMesh(root: THREE.Object3D, name: string): THREE.Mesh {
  let found: THREE.Mesh | undefined;
  root.traverse((object) => {
    if (object instanceof THREE.Mesh && object.name === name) found = object;
  });
  assert.ok(found, `no mesh named ${name}`);
  return found;
}

/** The dredged strip's width, in metres of real water. */
function channelMetres(world: World): number {
  const mesh = namedMesh(createPorts(world, [SAMPLE]), "ports:channel");
  const pos = mesh.geometry.getAttribute("position");
  assert.ok(pos && pos.count >= 2, "the channel strip has no rails to measure");
  const width =
    Math.hypot(pos.getX(0) - pos.getX(1), pos.getZ(0) - pos.getZ(1)) * world.metresPerUnit;
  return width;
}

/**
 * Distance between the two portal legs of the one gantry, in metres.
 *
 * `pushCraneMatrices` emits the waterside leg first and the landside leg next,
 * `gauge` apart along the boom. Measuring the drawn matrices is the same move
 * `craneLights.test.ts` makes for the apex: a helper that recomputes the
 * number would pass while the mesh drifted.
 */
function craneGaugeMetres(world: World): number {
  const mesh = namedMesh(createPorts(world, [SAMPLE]), "ports:cranes") as THREE.InstancedMesh;
  assert.ok(mesh.count >= 2, "a gantry is at least two portal legs");
  const a = new THREE.Matrix4();
  const b = new THREE.Matrix4();
  const pa = new THREE.Vector3();
  const pb = new THREE.Vector3();
  mesh.getMatrixAt(0, a);
  mesh.getMatrixAt(1, b);
  pa.setFromMatrixPosition(a);
  pb.setFromMatrixPosition(b);
  return Math.hypot(pa.x - pb.x, pa.z - pb.z) * world.metresPerUnit;
}

test("San Pedro's channel is the same width on a 391 m board and a 1,919 m one", () => {
  const metro = channelMetres(board(285));
  const state = channelMetres(board(58));
  assert.ok(
    Math.abs(metro - state) < 2,
    `channel is ${metro.toFixed(0)} m at Los Angeles and ${state.toFixed(0)} m on California`,
  );
  assert.ok(
    metro > 300 && metro < 400,
    `channel measures ${metro.toFixed(0)} m; that is not a river`,
  );
});

test("a gantry's rail gauge is the same metres on both boards", () => {
  const metro = craneGaugeMetres(board(285));
  const state = craneGaugeMetres(board(58));
  assert.ok(
    Math.abs(metro - state) < 1,
    `gauge is ${metro.toFixed(1)} m at Los Angeles and ${state.toFixed(1)} m on California`,
  );
  assert.ok(
    metro > 30 && metro < 60,
    `gauge measures ${metro.toFixed(1)} m; that is not a city block`,
  );
});
