import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import {
  MODEL_X_METRICS,
  LUMBRIDGE_EV_METRICS,
  advanceModelXWheels,
  buildModelX,
  buildLumbridgeEV,
  cloneModelX,
  disposeModelX,
  modelXInstanceParts,
  setModelXSteering,
  setModelXWheelRotation,
} 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;
}

describe("procedural Model X vehicle asset", () => {
  it("publishes the original unbranded Lumbridge EV identity at both LODs", () => {
    const follow = buildLumbridgeEV({ detail: "follow" });
    const corridor = buildLumbridgeEV({ detail: "corridor" });
    assert.equal(LUMBRIDGE_EV_METRICS, MODEL_X_METRICS);
    assert.equal(follow.root.userData.design, "lumbridge-ev-01");
    assert.equal(follow.root.userData.unbranded, true);
    assert.equal(follow.root.userData.lod, "follow");
    assert.equal(corridor.root.userData.lod, "corridor");
    const followVertices = meshes(follow.root).reduce((sum, mesh) => sum + mesh.geometry.getAttribute("position").count, 0);
    const corridorVertices = meshes(corridor.root).reduce((sum, mesh) => sum + mesh.geometry.getAttribute("position").count, 0);
    assert.ok(followVertices > corridorVertices, "follow LOD includes a readable interior and denser wheels");
    disposeModelX(follow);
    disposeModelX(corridor);
  });
  it("has a metre-scale crossover silhouette and faces -Z", () => {
    const rig = buildModelX({ detail: "corridor" });
    const size = new THREE.Box3().setFromObject(rig.root).getSize(new THREE.Vector3());

    assert.ok(Math.abs(size.x - MODEL_X_METRICS.width) < 0.12, `width ${size.x}`);
    assert.ok(Math.abs(size.y - MODEL_X_METRICS.height) < 0.08, `height ${size.y}`);
    assert.ok(Math.abs(size.z - MODEL_X_METRICS.length) < 0.08, `length ${size.z}`);
    assert.equal(rig.root.userData.forwardAxis, "-Z");
    assert.ok(rig.wheels.frontLeft.steering.position.z < 0);
    assert.ok(rig.wheels.rearLeft.steering.position.z > 0);
    disposeModelX(rig);
  });

  it("exposes independent steering and rolling joints", () => {
    const rig = buildModelX();
    setModelXWheelRotation(rig, 1.25);
    for (const wheel of Object.values(rig.wheels)) assert.equal(wheel.spin.rotation.x, 1.25);

    setModelXSteering(rig, 99);
    assert.equal(rig.wheels.frontLeft.steering.rotation.y, MODEL_X_METRICS.maxSteeringAngle);
    assert.equal(rig.wheels.frontRight.steering.rotation.y, MODEL_X_METRICS.maxSteeringAngle);
    assert.equal(rig.wheels.rearLeft.steering.rotation.y, 0);

    advanceModelXWheels(rig, MODEL_X_METRICS.wheelRadius);
    assert.equal(rig.wheels.frontLeft.spin.rotation.x, 0.25);
    disposeModelX(rig);
  });

  it("clones cheaply while keeping its pose independent", () => {
    const original = buildModelX();
    const clone = cloneModelX(original);
    const sourceMeshes = meshes(original.root);
    const clonedMeshes = meshes(clone.root);

    assert.equal(clonedMeshes.length, sourceMeshes.length);
    for (let i = 0; i < sourceMeshes.length; i++) {
      assert.equal(clonedMeshes[i]!.geometry, sourceMeshes[i]!.geometry);
      assert.equal(clonedMeshes[i]!.material, sourceMeshes[i]!.material);
    }

    setModelXSteering(clone, -0.3);
    assert.equal(clone.wheels.frontLeft.steering.rotation.y, -0.3);
    assert.equal(original.wheels.frontLeft.steering.rotation.y, 0);
    assert.equal(clone.ownsMaterials, false);
    disposeModelX(original);
  });

  it("publishes stable neutral-pose pieces for instanced traffic", () => {
    const rig = buildModelX({ detail: "corridor" });
    const parts = modelXInstanceParts(rig);
    assert.equal(parts.length, meshes(rig.root).length);
    assert.ok(parts.some((part) => part.name === "frontLeft.tire"));
    assert.ok(parts.some((part) => part.name.startsWith("model-x.body:")));
    assert.ok(parts.every((part) => Number.isFinite(part.matrix.determinant())));
    disposeModelX(rig);
  });
});
