import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import {
  DOG_METRICS,
  buildDog,
  cloneDog,
  disposeDog,
  dogPoseStateForSpeed,
  poseDog,
  poseDogAttention,
  type DogRig,
} from "../assets/actors/index.ts";

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

function transformSnapshot(rig: DogRig): readonly number[][] {
  const result: number[][] = [];
  rig.root.traverse((object) => {
    result.push([
      ...object.position.toArray(),
      object.rotation.x,
      object.rotation.y,
      object.rotation.z,
      ...object.scale.toArray(),
    ]);
  });
  return result;
}

describe("procedural dog v2", () => {
  it("builds a floor-centred articulated silhouette within the peer/mobile budget", () => {
    const rig = buildDog();
    assert.equal(rig.root.userData.rigVersion, 2);
    for (const leg of [rig.joints.frontLeft, rig.joints.frontRight, rig.joints.rearLeft, rig.joints.rearRight]) {
      assert.equal(leg.lower.parent, leg.upper);
      assert.equal(leg.paw.parent, leg.lower);
    }
    for (const name of [
      "dog.shoulder-mass",
      "dog.hip-mass",
      "dog.neck",
      "dog.chest",
      "dog.muzzle",
      "dog.collar",
      "dog.tag",
      "dog.tail.tip",
    ]) assert.ok(rig.root.getObjectByName(name), `missing ${name}`);

    const box = new THREE.Box3().setFromObject(rig.root);
    const size = box.getSize(new THREE.Vector3());
    assert.ok(box.min.y > -0.02, `floor ${box.min.y}`);
    assert.ok(Math.abs(size.y - DOG_METRICS.height) < 0.08, `height ${size.y}`);
    assert.ok(Math.abs(size.z - DOG_METRICS.length) < 0.08, `length ${size.z}`);
    const allMeshes = meshes(rig.root);
    const triangles = allMeshes.reduce((sum, mesh) => {
      const geometry = mesh.geometry;
      return sum + (geometry.index?.count ?? geometry.getAttribute("position").count) / 3;
    }, 0);
    assert.ok(allMeshes.length <= 34, `${allMeshes.length} meshes`);
    assert.ok(triangles <= 3_600, `${triangles} triangles`);
    disposeDog(rig);
  });

  it("separates idle, attention, walk, trot, run, turn and start/stop poses", () => {
    const rig = buildDog();
    poseDog(rig, { state: "idle", phase: Math.PI / 2, amount: 1 });
    const idleChest = rig.joints.chest.scale.y;
    assert.equal(rig.root.userData.pose, "idle");
    assert.equal(rig.joints.frontLeft.upper.rotation.x, 0);

    poseDog(rig, { state: "attention", phase: Math.PI / 2, amount: 1, attentionYaw: 0.35 });
    assert.equal(rig.root.userData.pose, "attention");
    assert.equal(rig.joints.head.rotation.y, 0.35);
    assert.notEqual(rig.joints.earLeft.rotation.z, -rig.joints.earRight.rotation.z);

    poseDog(rig, { state: "walk", phase: Math.PI / 2, amount: 1 });
    const walkStride = Math.abs(rig.joints.frontLeft.upper.rotation.x);
    assert.ok(walkStride > 0.3);
    assert.equal(rig.joints.frontLeft.upper.rotation.x, rig.joints.rearRight.upper.rotation.x);
    assert.equal(rig.joints.frontRight.upper.rotation.x, rig.joints.rearLeft.upper.rotation.x);
    assert.notEqual(rig.joints.chest.scale.y, idleChest);

    poseDog(rig, { state: "trot", phase: Math.PI / 2, amount: 1 });
    const trotStride = Math.abs(rig.joints.frontLeft.upper.rotation.x);
    assert.ok(trotStride > walkStride);

    poseDog(rig, { state: "run", phase: 0.8, amount: 1 });
    assert.equal(rig.root.userData.pose, "run");
    assert.notEqual(rig.joints.frontLeft.upper.rotation.x, rig.joints.frontRight.upper.rotation.x,
      "a gathered run offsets the front pair rather than using a rigid scissor");

    poseDog(rig, { state: "trot", phase: 0.7, amount: 1, turn: 0.8, acceleration: 0.7 });
    assert.ok(rig.joints.body.rotation.z < -0.09, "turn banks through the chest");
    assert.ok(rig.joints.body.rotation.x < -0.05, "starting lowers the chest into motion");
    poseDog(rig, { state: "idle", phase: 0.7, amount: 1, acceleration: -0.8 });
    assert.ok(rig.joints.body.rotation.x > 0.06, "stopping braces the chest");
    disposeDog(rig);
  });

  it("selects gait thresholds and repeats an exact pose without accumulating transforms", () => {
    assert.equal(dogPoseStateForSpeed(Number.NaN), "idle");
    assert.equal(dogPoseStateForSpeed(0.07), "idle");
    assert.equal(dogPoseStateForSpeed(0.5), "walk");
    assert.equal(dogPoseStateForSpeed(1.5), "trot");
    assert.equal(dogPoseStateForSpeed(3), "run");

    const rig = buildDog();
    const pose = {
      state: "trot" as const,
      phase: 2.17,
      amount: 0.83,
      turn: -0.42,
      acceleration: -0.3,
      attentionYaw: 0.11,
      attentionPitch: -0.08,
    };
    poseDog(rig, pose);
    const first = transformSnapshot(rig);
    poseDog(rig, pose);
    assert.deepEqual(transformSnapshot(rig), first);
    poseDogAttention(rig, 0.2, 1.7);
    const attention = transformSnapshot(rig);
    poseDogAttention(rig, 0.2, 1.7);
    assert.deepEqual(transformSnapshot(rig), attention, "legacy attention remains idempotent");
    disposeDog(rig);
  });

  it("clones shared resources into independent joints and respects material ownership", () => {
    const source = buildDog();
    const clone = cloneDog(source);
    const sourceMeshes = meshes(source.root);
    const cloneMeshes = meshes(clone.root);
    assert.equal(cloneMeshes.length, sourceMeshes.length);
    for (let index = 0; index < sourceMeshes.length; index += 1) {
      assert.equal(cloneMeshes[index]!.geometry, sourceMeshes[index]!.geometry);
      assert.equal(cloneMeshes[index]!.material, sourceMeshes[index]!.material);
    }
    poseDog(clone, { state: "run", phase: 1.3, amount: 1 });
    assert.notEqual(clone.joints.frontLeft.upper.rotation.x, source.joints.frontLeft.upper.rotation.x);
    assert.equal(clone.ownsMaterials, false);

    const shared = sourceMeshes[0]!.material as THREE.Material;
    let disposals = 0;
    shared.addEventListener("dispose", () => disposals++);
    const external = { coat: shared, markings: shared, nose: shared, collar: shared };
    const externalRig = buildDog({ materials: external });
    disposeDog(externalRig);
    assert.equal(disposals, 0);
    disposeDog(source);
    assert.equal(disposals, 1);
  });
});
