import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import CALIFORNIA_CITY from "../cities/california.ts";
import { createFreewayWorld } from "../engine/structures.ts";
import type { World } from "../engine/world.ts";
import CALIFORNIA_TRANSPORT from "../transport/california.ts";
import { buildFreewayWorldPlan } from "../transport/freewayWorld.ts";

describe("freeway world v2", () => {
  it("compiles route identity and roadside placement deterministically", () => {
    const first = buildFreewayWorldPlan(CALIFORNIA_TRANSPORT, { seed: 115, sampleCount: 32 });
    const replay = buildFreewayWorldPlan(CALIFORNIA_TRANSPORT, { seed: 115, sampleCount: 32 });
    const alternate = buildFreewayWorldPlan(CALIFORNIA_TRANSPORT, { seed: 116, sampleCount: 32 });
    assert.deepEqual(first, replay);
    assert.notDeepEqual(first, alternate);
    assert.deepEqual(
      first.routes.map(({ routeId, identity, shield, character }) => ({ routeId, identity, shield, character })),
      [
        { routeId: "la-sf-us-101", identity: "us-highway", shield: "101", character: "coastal-oak" },
        { routeId: "la-sf-i-5", identity: "interstate", shield: "5", character: "central-valley" },
      ],
    );
    assert.ok(first.routes.every((route) => route.samples.length === 32));
    assert.ok(first.routes.flatMap((route) => route.samples).every((sample) =>
      sample.lanesPerDirection >= 2 && sample.lanesPerDirection <= 5 && Number.isFinite(sample.elevationPhase)
    ));
  });

  /**
   * The counts below changed when `structures.ts` started merging per material.
   *
   * They used to read 8 lane-dash meshes, 4 median barriers and 4 guardrails —
   * two corridors' worth of two sides each — and every one of those was a draw
   * call. Merging collapses each class to one mesh, so counting meshes no longer
   * says anything about how many corridors were built. The assertions therefore
   * moved down a level: **one** mesh per class, and its vertex count proves all
   * four spans went into it, which is the fact the old count was standing in for.
   */
  it("renders separated decks, markings, barriers, signs, and batched scenery", () => {
    const world = {
      city: CALIFORNIA_CITY,
      project(lat: number, lng: number): [number, number] {
        return [(lng + 121) * 20, -(lat - 36) * 20];
      },
      groundAt(): number {
        return 0;
      },
    } as unknown as World;
    const group = createFreewayWorld(world, CALIFORNIA_TRANSPORT);
    assert.equal(group.name, "freeway-world-v2");
    assert.equal(group.userData.planSeed, 101_005);
    const names: string[] = [];
    group.traverse((object) => names.push(object.name));
    assert.equal(names.filter((name) => name === "freeway:lane-dashes").length, 1);
    assert.equal(names.filter((name) => name === "freeway:median-barrier").length, 1);
    assert.equal(names.filter((name) => name === "freeway:outer-guardrail").length, 1);
    assert.ok(names.includes("freeway:sign:101"));
    assert.ok(names.includes("freeway:sign:5"));
    assert.ok(group.children.filter((child) => child instanceof THREE.InstancedMesh).length >= 6);

    // Four spans really did go into each of those single meshes. A merge that
    // silently dropped a bucket — mismatched attributes are the way that
    // happens — would leave one span's worth of vertices here and look fine.
    const meshFor = (name: string): THREE.Mesh => {
      const found = group.children.find(
        (child): child is THREE.Mesh => child instanceof THREE.Mesh && child.name === name,
      );
      assert.ok(found, `expected a merged mesh named ${name}`);
      return found;
    };
    const dashVertices = meshFor("freeway:lane-dashes").geometry.getAttribute("position").count;
    const guardVertices = meshFor("freeway:outer-guardrail").geometry.getAttribute("position").count;
    assert.ok(dashVertices > 400, `lane dashes merged to only ${dashVertices} vertices`);
    assert.ok(guardVertices > 400, `guardrails merged to only ${guardVertices} vertices`);
  });

  it("shares one material per colour instead of one per ribbon", () => {
    const world = {
      city: CALIFORNIA_CITY,
      project(lat: number, lng: number): [number, number] {
        return [(lng + 121) * 20, -(lat - 36) * 20];
      },
      groundAt(): number {
        return 0;
      },
    } as unknown as World;
    const group = createFreewayWorld(world, CALIFORNIA_TRANSPORT);

    let drawCalls = 0;
    const materials = new Set<THREE.Material>();
    group.traverse((object) => {
      if (!(object instanceof THREE.Mesh)) return;
      drawCalls += 1;
      const material = object.material;
      if (Array.isArray(material)) for (const m of material) materials.add(m);
      else materials.add(material);
    });

    // Was 59 meshes: eighteen road ribbons, eight dash strips, four guardrails,
    // four medians, nine two-mesh sign groups and seven instanced batches, each
    // ribbon carrying a material minted on the spot. Twenty-five is generous
    // headroom over the twenty it actually emits, and it fails loudly if anyone
    // reintroduces a `new THREE.Mesh` inside the corridor loop.
    assert.ok(drawCalls <= 25, `freeway world emits ${drawCalls} draw calls`);
    assert.ok(materials.size <= 20, `freeway world holds ${materials.size} materials`);
  });

  it("draws a 44 m berm in metre scale, not a 4.7 km glyph", () => {
    const mpu = 111_320 / CALIFORNIA_CITY.latScale;
    const world = {
      city: CALIFORNIA_CITY,
      metresPerUnit: mpu,
      project(lat: number, lng: number): [number, number] {
        return [(lng + 121) * 20, -(lat - 36) * 20];
      },
      groundAt(): number {
        return 0;
      },
    } as unknown as World;
    const group = createFreewayWorld(world, CALIFORNIA_TRANSPORT, "metre");
    assert.equal(group.name, "freeway-world-metre");
    const berm = group.children.find(
      (child): child is THREE.Mesh => child instanceof THREE.Mesh && child.name === "freeway:berm",
    );
    assert.ok(berm, "metre corridor has no berm");
    const pos = berm.geometry.getAttribute("position");
    const widthM = Math.hypot(pos.getX(1)! - pos.getX(0)!, pos.getZ(1)! - pos.getZ(0)!) * mpu;
    assert.ok(
      widthM > 40 && widthM < 50,
      `metre berm is ${widthM.toFixed(1)} m; US-101 is authored 44 m`,
    );

    const oak = group.children.find(
      (child): child is THREE.InstancedMesh =>
        child instanceof THREE.InstancedMesh && child.name === "freeway:coastal-oaks",
    );
    assert.ok(oak, "metre corridor has no oaks");
    const radius = (oak.geometry as THREE.IcosahedronGeometry).parameters.radius * mpu;
    assert.ok(
      radius > 4 && radius < 12,
      `a metre oak crown is ${radius.toFixed(1)} m; the atlas one was 500 m`,
    );
  });
});
