/**
 * A marker is a true-metre object, and every board has to agree about how big
 * it is.
 *
 * This exists because it did not, and the way it failed is the way this engine
 * keeps failing. `markers.ts` sized a pin in **scene units** — 1.6 of lift, a
 * 1.1 stem, a 0.16 head — numbers chosen against the Bay Area board, where a
 * unit is 94.3 m and they come out as a 151 m lift under a 104 m pin. Correct,
 * and correct for exactly one board.
 *
 * `cities/unify.ts` then put San Francisco's districts onto a board that is
 * **1,919 m to the unit**, and `main.ts` draws markers on any board carrying
 * metro detail. The same constants became a 3.07 km lift under a 2.1 km pin
 * with a 614 m sphere on it, standing on downtown San Francisco. Nothing
 * failed: not a type, not a test, not a budget — twenty triangles do not move a
 * triangle count. It was found by taking a photograph and looking at it.
 *
 * So the assertion here is not "the head is 0.16". It is the property that
 * makes a photograph unnecessary: **two boards twenty times apart in scale draw
 * the same marker at the same size on the ground.** Any future constant that
 * forgets what a scene unit is worth breaks this, whatever number it picks.
 *
 * The same class has now been found three times — the freeway corridor's 4.7 km
 * ribbon, the aeroplane glyph, and this. If it is found a fourth time, the
 * lesson is not another test; it is that scene-unit constants need a type.
 */

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

import { setReconcile } from "../../cities/reconcile.ts";
import { createMarkerLayer } from "../../engine/markers.ts";
import type { City, Marker, MarkerPalette } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";

setReconcile(false);

/**
 * The same square of California at two scales.
 *
 * `latScale` is the whole of the difference: it is what `World` divides 111,320
 * by to get `metresPerUnit`, so 1180 is the Bay Area's 94.3 m to the unit and 58
 * is the merged board's 1,919 m. Same ground, same markers, same everything
 * else — which is what makes a difference in the result attributable.
 */
function board(latScale: number, verticalExaggeration: number): City {
  return {
    id: `scale-${latScale}`,
    name: "Scale Board",
    center: { lat: 37.78, lng: -122.42 },
    bounds: { minLat: 37.7, maxLat: 37.86, minLng: -122.5, maxLng: -122.34 },
    latScale,
    verticalExaggeration,
    cellLat: 0.01,
    cellLng: 0.01,
    coastFalloff: 0.02,
    landmasses: [
      [
        [37.71, -122.49],
        [37.85, -122.49],
        [37.85, -122.35],
        [37.71, -122.35],
      ],
    ],
    parks: [],
    inlandWater: [],
    districts: [],
    landmarks: [],
    bridges: [],
    roads: [],
    chapters: [],
    hills: [],
  };
}

const PALETTE: MarkerPalette = { applied: 0x4c8dd8 };
const MARKERS: Marker[] = [
  { id: "here", label: "Here", lat: 37.78, lng: -122.42, colorKey: "applied" },
  {
    id: "nowhere",
    label: "Nowhere",
    lat: 37.79,
    lng: -122.41,
    colorKey: "applied",
    located: false,
  },
];

/** Every mesh in the layer, with its radius in **true metres**. */
async function pinMetres(latScale: number, verticalExaggeration = 3.6) {
  const world = new World(board(latScale, verticalExaggeration));
  assert.equal(await world.ready(), true, `board at latScale ${latScale} failed to build`);
  const layer = createMarkerLayer(world, PALETTE);
  layer.setMarkers(MARKERS);

  /*
   * The **head**, by geometry type, and not "the largest bounding sphere".
   *
   * The first draft took the widest bounding sphere in the layer and it measured
   * 187 m — which is not a bug in the pin, it is the *stem*: a cylinder 104 true
   * metres tall, drawn through a 3.6x exaggeration, has a bounding sphere half
   * its own drawn height. A test that reaches for "the biggest thing" gets the
   * one dimension that is *supposed* to carry the exaggeration, and then reports
   * the pin as implausible for doing exactly what it should. Ask for the head.
   */
  let headRadius = 0;
  let top = -Infinity;
  layer.group.traverse((node) => {
    if (!(node instanceof THREE.Mesh)) return;
    node.updateWorldMatrix(true, false);
    const geo = node.geometry as THREE.SphereGeometry;
    if (geo.type === "SphereGeometry") {
      const radius = (geo.parameters?.radius ?? 0) * node.matrixWorld.getMaxScaleOnAxis();
      headRadius = Math.max(headRadius, radius * world.metresPerUnit);
    }
    const at = new THREE.Vector3().setFromMatrixPosition(node.matrixWorld);
    if (at.y > top) top = at.y;
  });

  assert.ok(headRadius > 0, "the layer built no pin head; the fixture is wrong");
  return {
    /** The head's radius, in metres of real ground. */
    headMetres: headRadius,
    /** How far the highest piece floats, in true metres above sea level. */
    topMetres: world.unitsToMetres(top),
  };
}

test("a marker is the same size on the ground whatever the board's scale is", async () => {
  // 1180 is the Bay Area's own `latScale` (94.3 m to the unit); 58 is the merged
  // California board's (1,919 m). Twenty times apart, which is the whole point.
  const metro = await pinMetres(1180);
  const state = await pinMetres(58);

  assert.ok(
    Math.abs(metro.headMetres - state.headMetres) < 0.5,
    `a marker is ${metro.headMetres.toFixed(1)} m across on a 94 m/unit board and ` +
      `${state.headMetres.toFixed(1)} m on a 1,919 m/unit one. A pin is a true-metre ` +
      `object; a constant in scene units is a constant that assumes one board.`,
  );

  // And it is a *plausible* size, not merely a consistent one — a test that only
  // compared the two would pass just as well on two boards drawing a moon.
  assert.ok(
    metro.headMetres > 5 && metro.headMetres < 60,
    `a marker head measures ${metro.headMetres.toFixed(1)} m; that is not a pin`,
  );
});

test("a pin floats the same true height over both boards", async () => {
  const metro = await pinMetres(1180);
  const state = await pinMetres(58);

  /*
   * Height goes through `world.metres`, so it carries the board's vertical
   * exaggeration on purpose: a pin has to clear a skyline that is itself
   * exaggerated. Both fixtures declare the same exaggeration, so what is left is
   * the horizontal scale — and that must not reach the answer at all.
   */
  assert.ok(
    Math.abs(metro.topMetres - state.topMetres) < 2,
    `a pin tops out at ${metro.topMetres.toFixed(0)} m on one board and ` +
      `${state.topMetres.toFixed(0)} m on the other`,
  );
  assert.ok(
    metro.topMetres > 80 && metro.topMetres < 500,
    `a pin tops out at ${metro.topMetres.toFixed(0)} m, which clears no skyline or every one`,
  );
});
