/**
 * The Model X hero pass, asserted where it can be asserted: on the mesh.
 *
 * Most of what makes a car look like a car is not testable and this file does
 * not pretend otherwise. What *is* testable is the set of properties that broke
 * the first version, each of which failed silently and none of which is visible
 * from reading the builder:
 *
 *  - the LOD split was nominal — a `follow` car that costs what a `corridor` car
 *    costs is forty background cars nobody budgeted for;
 *  - the shoulder crease was averaged away by one `computeVertexNormals()` over
 *    the whole shell, so the geometry had a crease and the shading did not;
 *  - the wheels intersected a flat flank, because there were no arches;
 *  - the glass floated on the paint instead of filling an opening;
 *  - the paint carried a fake ambient emissive that now double-counts against a
 *    real environment map.
 *
 * The bounding-box assertions are the load-bearing ones for everything
 * downstream: `MODEL_X_METRICS` is what `src/transport` collides, frames and
 * lane-positions with, and a mesh that is wider than its own published width is
 * a car that clips kerbs it looked like it cleared.
 */

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

import {
  MODEL_X_METRICS,
  MODEL_X_PAINTS,
  buildModelX,
  cloneModelX,
  createModelXMaterials,
  createModelXPaintPool,
  disposeModelX,
  disposeModelXPaintPool,
  type ModelXDetail,
  type ModelXRig,
} from "../../assets/vehicles/index.ts";

function meshes(root: THREE.Object3D): THREE.Mesh[] {
  const found: THREE.Mesh[] = [];
  root.traverse((object) => {
    if (object instanceof THREE.Mesh) found.push(object);
  });
  return found;
}

function triangles(root: THREE.Object3D): number {
  let total = 0;
  for (const mesh of meshes(root)) {
    const index = mesh.geometry.getIndex();
    total += index ? index.count / 3 : mesh.geometry.getAttribute("position").count / 3;
  }
  return total;
}

function meshNamed(rig: ModelXRig, material: string): THREE.Mesh {
  const found = meshes(rig.root).find(
    (mesh) => (mesh.material as THREE.Material).name === material,
  );
  assert.ok(found, `no mesh carries the "${material}" material`);
  return found;
}

function localBox(mesh: THREE.Mesh): THREE.Box3 {
  const box = new THREE.Box3();
  box.setFromBufferAttribute(mesh.geometry.getAttribute("position") as THREE.BufferAttribute);
  return box;
}

describe("Model X hero geometry", () => {
  it("spends a hero budget at `follow` and a traffic budget at `corridor`", () => {
    const follow = buildModelX({ detail: "follow" });
    const corridor = buildModelX({ detail: "corridor" });

    const followTriangles = triangles(follow.root);
    const corridorTriangles = triangles(corridor.root);

    // The acceptance window. Below 4,000 it is not a hero asset; above 24,000 it
    // stops being one car's worth of the office's 550,000-triangle budget.
    assert.ok(
      followTriangles >= 4_000 && followTriangles <= 24_000,
      `follow LOD is ${followTriangles} triangles`,
    );
    // `corridor` is instanced up to forty times by `engine/roadTraffic.ts`, so
    // its count is multiplied by forty against the *city's* budget. Half of
    // follow is the ceiling that keeps that honest.
    assert.ok(
      corridorTriangles < followTriangles / 2,
      `corridor LOD is ${corridorTriangles} against follow's ${followTriangles} — the split is nominal`,
    );

    // And the split has to be in the geometry rather than only in the count:
    // corridor may not cost more draw calls than it did as a torus-wheeled box.
    assert.ok(meshes(corridor.root).length <= 20, "corridor LOD grew its draw calls");

    disposeModelX(follow);
    disposeModelX(corridor);
  });

  it("stays inside the silhouette `MODEL_X_METRICS` publishes, at both LODs", () => {
    for (const detail of ["corridor", "follow"] as ModelXDetail[]) {
      const rig = buildModelX({ detail });
      const box = new THREE.Box3().setFromObject(rig.root);
      const size = box.getSize(new THREE.Vector3());

      assert.ok(size.x <= 2.1, `${detail} is ${size.x} wide`);
      assert.ok(size.y <= 1.8, `${detail} is ${size.y} tall`);
      assert.ok(size.z <= 5.1, `${detail} is ${size.z} long`);

      // And it has to actually be the car those numbers describe, not merely
      // smaller than them.
      assert.ok(Math.abs(size.x - MODEL_X_METRICS.width) < 0.05, `${detail} width ${size.x}`);
      assert.ok(Math.abs(size.y - MODEL_X_METRICS.height) < 0.05, `${detail} height ${size.y}`);
      assert.ok(Math.abs(size.z - MODEL_X_METRICS.length) < 0.06, `${detail} length ${size.z}`);

      // The tyres define y = 0. A body that dips below it is a car sunk into the
      // road; a car that floats is worse, because the shadow gives it away.
      assert.ok(Math.abs(box.min.y) < 0.005, `${detail} does not sit on the road: ${box.min.y}`);

      disposeModelX(rig);
    }
  });

  it("names the mirrors and the window frames so they can be found", () => {
    const rig = buildModelX({ detail: "follow" });
    const mirrors = rig.root.getObjectByName("model-x.mirrors");
    const frames = rig.root.getObjectByName("model-x.glass-frames");
    assert.ok(mirrors, "no mirrors on the hero LOD");
    assert.ok(frames, "no window surround on the hero LOD");
    assert.ok(meshes(mirrors).length > 0);
    assert.ok(meshes(frames).length > 0);

    // A mirror is a first-surface reflector and the side glass is tinted and
    // translucent. One material cannot be both, and with an environment map
    // present that difference is most of what makes a mirror read as one.
    const mirrorMaterials = meshes(mirrors).map((m) => (m.material as THREE.Material).name);
    assert.ok(mirrorMaterials.includes("model-x.mirror"));

    // The corridor car does without both: nobody resolves a wing mirror at the
    // distance forty instanced cars are drawn at.
    const corridor = buildModelX({ detail: "corridor" });
    assert.equal(corridor.root.getObjectByName("model-x.mirrors"), undefined);
    assert.equal(corridor.root.getObjectByName("model-x.glass-frames"), undefined);

    disposeModelX(rig);
    disposeModelX(corridor);
  });

  it("keeps the shoulder crease by splitting normals rather than averaging them", () => {
    // The failure this replaces: one `computeVertexNormals()` over the whole
    // shell. The test for "there is a crease" is that two vertices share a
    // position and disagree about which way the surface points — which is
    // exactly what a split vertex is, and what a single averaged pass destroys.
    const rig = buildModelX({ detail: "follow" });
    const paint = meshNamed(rig, "model-x.paint");
    const position = paint.geometry.getAttribute("position");
    const normal = paint.geometry.getAttribute("normal");
    assert.ok(normal, "the bodyshell has no normals");

    const byPosition = new Map<string, THREE.Vector3[]>();
    for (let i = 0; i < position.count; i++) {
      // Only the right flank, at the height the shoulder line runs, and only
      // between the axles: that is where the crease is, and looking everywhere
      // would pass on any hard edge anywhere on the car.
      const x = position.getX(i);
      const y = position.getY(i);
      const z = position.getZ(i);
      if (x < 0.9 || y < 0.85 || y > 0.99 || z < -1.2 || z > 1.2) continue;
      const key = `${Math.round(x * 1e3)},${Math.round(y * 1e3)},${Math.round(z * 1e3)}`;
      const list = byPosition.get(key) ?? [];
      list.push(new THREE.Vector3(normal.getX(i), normal.getY(i), normal.getZ(i)));
      byPosition.set(key, list);
    }

    let creased = 0;
    for (const normals of byPosition.values()) {
      for (let a = 0; a < normals.length; a++) {
        for (let b = a + 1; b < normals.length; b++) {
          // 20° is well past any smoothing artefact and well under the ~60° the
          // shoulder actually turns through.
          if (normals[a]!.dot(normals[b]!) < Math.cos(0.35)) creased++;
        }
      }
    }
    assert.ok(
      creased >= 4,
      `found ${creased} split normals along the shoulder line — the crease has been averaged away again`,
    );
    disposeModelX(rig);
  });

  it("cuts real wheel openings, with the tyre inside them", () => {
    const rig = buildModelX({ detail: "follow" });
    rig.root.updateMatrixWorld(true);
    const paint = meshNamed(rig, "model-x.paint");
    const position = paint.geometry.getAttribute("position");

    // At the front axle plane, the paint has to *stop* well above the road on
    // the outboard side — that gap is the wheel opening. On the first version
    // the flank ran straight down past the tyre and this was 0.24 m.
    const axle = -MODEL_X_METRICS.wheelbase / 2;
    let lowestOutboard = Infinity;
    for (let i = 0; i < position.count; i++) {
      if (Math.abs(position.getZ(i) - axle) > 0.03) continue;
      if (position.getX(i) < 0.95) continue;
      lowestOutboard = Math.min(lowestOutboard, position.getY(i));
    }
    assert.ok(Number.isFinite(lowestOutboard), "no bodyside at the front axle at all");
    assert.ok(
      lowestOutboard > MODEL_X_METRICS.wheelRadius * 2 - 0.05,
      `the flank reaches down to ${lowestOutboard} at the front axle, which is through the tyre`,
    );

    // And the tyre has to fit under it rather than through it.
    const tire = rig.root.getObjectByName("frontRight.tire");
    assert.ok(tire instanceof THREE.Mesh);
    const tireBox = new THREE.Box3().setFromObject(tire);
    assert.ok(tireBox.max.y < lowestOutboard, "the tyre pokes through the arch lip");
    assert.ok(tireBox.max.y > 0.75, "the tyre is not the published diameter");

    disposeModelX(rig);
  });

  it("gives the tyre a sidewall, a shoulder and a flat tread", () => {
    const rig = buildModelX({ detail: "follow" });
    const tire = rig.root.getObjectByName("frontRight.tire");
    assert.ok(tire instanceof THREE.Mesh);
    const position = tire.geometry.getAttribute("position");

    // The wheel axis is X. A torus has one radius; a real tyre has a tread band
    // at the full radius and a sidewall that bulges *wider* than the tread while
    // sitting at a smaller radius, and that is the whole silhouette of a loaded
    // tyre.
    let treadHalfWidth = 0;
    let widest = 0;
    let sidewallRadius = 0;
    for (let i = 0; i < position.count; i++) {
      const x = Math.abs(position.getX(i));
      const r = Math.hypot(position.getY(i), position.getZ(i));
      widest = Math.max(widest, x);
      if (r > MODEL_X_METRICS.wheelRadius - 0.002) treadHalfWidth = Math.max(treadHalfWidth, x);
      if (x > widest - 1e-6) sidewallRadius = r;
    }
    assert.ok(treadHalfWidth > 0.08, `the tread band is only ${treadHalfWidth * 2} m wide`);
    assert.ok(
      widest > treadHalfWidth + 0.01,
      "the sidewall does not stand outboard of the tread — this is still a torus",
    );
    assert.ok(
      sidewallRadius < MODEL_X_METRICS.wheelRadius - 0.03,
      "the widest point of the tyre is at full radius, so there is no sidewall",
    );
    disposeModelX(rig);
  });

  it("puts the caliper on the upright, not on the spinning hub", () => {
    const rig = buildModelX({ detail: "follow" });
    const caliper = rig.root.getObjectByName("frontRight.caliper");
    assert.ok(caliper, "the brake has no caliper");
    // A caliper that rotates with the wheel is the single most common tell that
    // a wheel was assembled quickly. Its parent must be the steering group.
    assert.equal(caliper.parent?.name, "frontRight.steering");
    assert.equal(rig.wheels.frontRight.spin.getObjectByName("frontRight.caliper"), undefined);
    disposeModelX(rig);
  });

  it("fills the daylight opening with glass rather than laying glass on the paint", () => {
    const rig = buildModelX({ detail: "follow" });
    const glass = meshNamed(rig, "model-x.glass");
    const paint = meshNamed(rig, "model-x.paint");
    const glassBox = localBox(glass);
    const paintPosition = paint.geometry.getAttribute("position");

    // The greenhouse sits above the beltline and inside the body's own width.
    assert.ok(glassBox.min.y > 0.95, `glass starts at ${glassBox.min.y}, below the beltline`);
    assert.ok(glassBox.max.y > 1.6, "there is no glass at roof height — the roof is not glazed");
    assert.ok(glassBox.max.x < MODEL_X_METRICS.width / 2, "the glass stands outside the body");

    // And the paint has to have got out of the way: in the middle of the front
    // door, at glass height, there should be no painted surface outboard of the
    // recessed opening.
    let paintedInTheWindow = 0;
    for (let i = 0; i < paintPosition.count; i++) {
      const z = paintPosition.getZ(i);
      const y = paintPosition.getY(i);
      const x = paintPosition.getX(i);
      if (z < -0.7 || z > -0.1) continue;
      if (y < 1.12 || y > 1.42) continue;
      if (x > 0.9) paintedInTheWindow++;
    }
    assert.equal(
      paintedInTheWindow,
      0,
      "there is still paint where the front door glass should be — the window is not an opening",
    );
    disposeModelX(rig);
  });

  it("has no emissive term left on the paint or the wheel", () => {
    // The environment rig supplies real reflections now, so the fake ambient
    // these two carried double-counts: a `0x11191e` emissive on a metallic
    // clearcoat under ACES reads as a car lit from inside.
    const materials = createModelXMaterials(0x223344);
    for (const key of ["paint", "wheel"] as const) {
      const material = materials[key] as THREE.MeshStandardMaterial;
      // The colour is what has to be black, not the intensity: three defaults
      // `emissiveIntensity` to 1 whether you asked for emission or not, so a
      // material with a black `emissive` and an intensity of 1 emits nothing and
      // is the correct state to assert.
      assert.equal(material.emissive.getHex(), 0, `${key} still has an emissive colour`);
    }
    // The lamps keep theirs, and must.
    for (const key of ["headlight", "tailLight"] as const) {
      const material = materials[key] as THREE.MeshStandardMaterial;
      assert.notEqual(material.emissive.getHex(), 0, `${key} stopped being a lamp`);
      assert.ok(material.emissiveIntensity > 1, `${key} stopped being a lamp`);
    }
    for (const material of Object.values(materials)) material.dispose();
  });

  it("shares everything but the paint across a colour pool", () => {
    const pool = createModelXPaintPool();
    assert.equal(pool.length, MODEL_X_PAINTS.length);
    const first = pool[0];
    const second = pool[1];
    assert.ok(first && second);
    assert.notEqual(first.paint, second.paint, "two skins share one paint material");
    // Everything else is the same object, which is the entire point: a street of
    // seven colours costs six extra materials, not forty-two.
    for (const key of ["glass", "trim", "tire", "wheel", "brake", "mirror", "plate"] as const) {
      assert.equal(first[key], second[key], `${key} was duplicated across the pool`);
    }
    // And a rig handed a pooled skin must not claim to own it.
    const rig = buildModelX({ detail: "corridor", materials: second });
    assert.equal(rig.ownsMaterials, false);
    disposeModelX(rig);
    disposeModelXPaintPool(pool);
  });

  it("clones without duplicating a single buffer", () => {
    const original = buildModelX({ detail: "follow" });
    const clone = cloneModelX(original);
    const source = meshes(original.root);
    const copied = meshes(clone.root);
    assert.equal(copied.length, source.length);
    for (let i = 0; i < source.length; i++) {
      assert.equal(copied[i]!.geometry, source[i]!.geometry, `${source[i]!.name} was copied`);
      assert.equal(copied[i]!.material, source[i]!.material);
    }
    disposeModelX(original);
  });
});
