/**
 * Ships are drawn at true size, on both boards, and this is the file that says
 * so out loud.
 *
 * ### Why this needs an assertion at all
 *
 * "Make the ships a bit bigger so you can see them" is a reasonable-sounding
 * change that somebody will propose, and there is a precedent in this repo that
 * appears to license it: `aircraftGeometry.ts` holds an aeroplane at a fixed
 * 0.42 units on every board, which over Southern California is about four times
 * life size, and nobody has ever noticed. That oversizing is correct **because an
 * aeroplane in flight is alone in the sky with nothing to be wrong against.**
 *
 * A ship is never alone. It is alongside a quay, under a gantry crane and inside
 * a breakwater, all of which `ports.ts` draws at true scale. A hull scaled up to
 * be legible is a hull visibly longer than the berth it is lying in, and the
 * error is unmissable in exactly the frame the feature exists for. So the
 * inversion is deliberate, it is the opposite of the neighbouring convention,
 * and a bare number in a mesh builder would not survive a reviewer who
 * remembered the aircraft rule.
 *
 * The legibility oversizing would have bought is bought by the wake instead —
 * see the last case here, which is the whole design in one ratio.
 *
 * ### The numbers
 *
 * They come from the packs rather than from a comment. SoCal declares
 * `latScale: 285`, so a unit is 111,320 / 285 = 390.6 m and a 400 m ULCV is
 * **1.024 units**. The Bay declares `latScale: 1180`, a unit is 94.3 m, and the
 * same ship is **4.24 units** — four times bigger for the same steel, which is
 * the thing about board scale that is hardest to hold in your head and the
 * reason the tolerance below is tight.
 */

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

import { setReconcile } from "../../cities/reconcile.ts";
import { SOCAL_CITY } from "../../cities/socal.ts";
import { SAN_FRANCISCO_CITY } from "../../cities/sf.ts";
import {
  WAKE_FULL_SPEED_MPS,
  hullDraughtMetres,
  hullFreeboardMetres,
  hullLengthUnits,
  metresAcross,
  wakeLengthMetres,
} from "../../engine/vessels.ts";
import { DEFAULT_HULL, beamFor } from "../../server/vessels.ts";
import type { Vessel } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";

/*
 * Pinned off, and it must be pinned *here* rather than in a hook: `World`'s
 * constructor is what reconciles, so both worlds below are already built by the
 * time any `beforeEach` runs. This file asserts the packs' own true-scale
 * numbers — 390.6 m to the unit, an exaggeration of 3.4 — which are facts about
 * SoCal as authored and not about whichever rules ship on by default.
 */
setReconcile(false);

/** No `ready()`: every number here is projection, and projection is the constructor. */
const socal = new World(SOCAL_CITY);
const bay = new World(SAN_FRANCISCO_CITY);

const ULCV: Pick<Vessel, "kind" | "length" | "beam" | "draught"> = {
  kind: "container",
  length: 400,
  beam: 61,
};

describe("a ship is the size a ship is", () => {
  it("draws a 400 m hull as 1.024 units on the Southern California board", () => {
    const units = hullLengthUnits(socal, ULCV);
    assert.ok(
      Math.abs(units - 1.024) < 0.01,
      `a 400 m ULCV came out ${units.toFixed(4)} units on SoCal, not 1.024`,
    );
    // And the board's own scale is where that came from, not a constant.
    assert.ok(Math.abs(socal.metresPerUnit - 390.6) < 0.2);
  });

  it("draws the same hull as 4.24 units on the Bay Area board", () => {
    const units = hullLengthUnits(bay, ULCV);
    assert.ok(
      Math.abs(units - 4.24) < 0.05,
      `a 400 m ULCV came out ${units.toFixed(4)} units on the Bay, not 4.24`,
    );
    assert.ok(Math.abs(bay.metresPerUnit - 94.3) < 0.2);
  });

  it("is 4.14x bigger on the Bay for the same steel", () => {
    const ratio = hullLengthUnits(bay, ULCV) / hullLengthUnits(socal, ULCV);
    assert.ok(Math.abs(ratio - socal.metresPerUnit / bay.metresPerUnit) < 1e-9);
    assert.ok(ratio > 4 && ratio < 4.3, `boards differ by ${ratio.toFixed(2)}x`);
  });

  it("does NOT hold a hull at a fixed map-symbol size, the way an aeroplane is held", () => {
    // `aircraftGeometry.ts`'s AIRLINER glyph is a flat 0.42 units on every
    // board. If a hull were ever given the same treatment these two would be
    // equal, and they must never be.
    assert.notEqual(hullLengthUnits(socal, ULCV), hullLengthUnits(bay, ULCV));
  });

  it("scales a tug and a ULCV from one geometry, thirteen times apart", () => {
    const tug = { kind: "tug" as const, length: 30, beam: 11 };
    const ratio = hullLengthUnits(socal, ULCV) / hullLengthUnits(socal, tug);
    assert.ok(Math.abs(ratio - 400 / 30) < 1e-9, "the same solid at 13.3x, not two solids");
    // 30 m is 0.077 units on SoCal: a tug alone is genuinely invisible from the
    // board pose, which is why it is drawn with a wake or not noticed at all.
    assert.ok(hullLengthUnits(socal, tug) < 0.08);
  });

  it("does not run a plan measurement through the vertical exaggeration", () => {
    // `world.metres()` multiplies by `verticalExaggeration` — 3.4 on SoCal —
    // because it is for heights. Running a 400 m length through it would make
    // the ship 3.5 units long and overhang its berth by two ship lengths. The
    // separation between the two functions is the assertion.
    assert.ok(socal.metres(400) > 3.4);
    assert.ok(Math.abs(socal.metres(400) / metresAcross(socal, 400) - 3.4) < 1e-9);
  });
});

describe("the vertical, which is the axis the board does exaggerate", () => {
  it("stands a laden box ship about a third of its own length tall", () => {
    // Draught plus freeboard, through `world.metres`, is what makes a hull read
    // as a solid rather than as a decal — and it is exaggerated on purpose,
    // exactly as the 130 m gantry crane beside it is.
    const depth = hullDraughtMetres(ULCV) + hullFreeboardMetres(ULCV);
    assert.ok(depth > 40 && depth < 50, `${depth.toFixed(1)} m of hull, keel to deck`);
    const units = socal.metres(depth);
    assert.ok(units > 0.3 && units < 0.45, `${units.toFixed(3)} units tall on SoCal`);
  });

  it("authors draught rather than observing it, and never as a cargo claim", () => {
    // `vessels` in the store has no draught column at all. This is a hull
    // dimension, it is derived from the length, and an authored value overrides
    // it — but nothing here is ever an observation and no hull is ever labelled
    // laden or in ballast. "Empty or full" is answered at the port.
    assert.ok(Math.abs(hullDraughtMetres(ULCV) - 14.4) < 0.1);
    assert.equal(hullDraughtMetres({ ...ULCV, draught: 12 }), 12);
    const tug = hullDraughtMetres({ kind: "tug", length: 30 });
    assert.ok(tug > 3 && tug < 5, `a harbour tug drawing ${tug.toFixed(1)} m`);
  });
});

describe("the wake is what is actually legible", () => {
  it("is 3.75 hull lengths at speed, and 3.84 units on SoCal", () => {
    const metres = wakeLengthMetres(WAKE_FULL_SPEED_MPS, 400);
    assert.ok(Math.abs(metres - 1_500) < 20, `${metres.toFixed(0)} m of wake`);
    const units = metresAcross(socal, metres);
    assert.ok(Math.abs(units - 3.84) < 0.06, `${units.toFixed(3)} units of wake on SoCal`);
  });

  it("is 3.7x more legible than the hull it trails, which is the design", () => {
    // At the whole-board pose a 1.02-unit hull is about four pixels and is
    // invisible; a 3.84-unit wake is about fourteen and reads. That ratio is why
    // the wake is the primary object in this layer and the hull is the thing at
    // the sharp end of it.
    const hull = hullLengthUnits(socal, ULCV);
    const wake = metresAcross(socal, wakeLengthMetres(WAKE_FULL_SPEED_MPS, 400));
    assert.ok(Math.abs(wake / hull - 3.75) < 0.01, `the wake is ${(wake / hull).toFixed(2)}x`);
  });

  it("is 15.9 units on the Bay, where everything is four times bigger", () => {
    const units = metresAcross(bay, wakeLengthMetres(WAKE_FULL_SPEED_MPS, 400));
    assert.ok(Math.abs(units - 15.9) < 0.2, `${units.toFixed(2)} units of wake on the Bay`);
  });
});

describe("the display defaults for a source that sent no dimensions", () => {
  it("gives a Panamax-plus box ship its real 400 x 61", () => {
    // The ratios are display defaults, stated as ratios because that is what
    // they are. The one worth checking is the box ship, which is the hull the
    // whole San Pedro complex is shaped around.
    assert.ok(Math.abs(beamFor("container", 400) - 61) < 1.5);
    assert.ok(Math.abs(beamFor("tug", 30) - 10.7) < 1);
  });

  it("keeps every default hull a plausible ship rather than a placeholder", () => {
    for (const [kind, hull] of Object.entries(DEFAULT_HULL)) {
      const ratio = hull.length / hull.beam;
      assert.ok(ratio > 2 && ratio < 9, `${kind} is ${ratio.toFixed(1)} long to a beam`);
      assert.ok(hull.length > 10 && hull.length < 450, `${kind} is ${hull.length} m long`);
    }
  });
});
