import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import {
  createSceneActor,
  type ActorIdentity,
  type SceneActorOptions,
} from "../actors/index.ts";

const MEMBER: ActorIdentity = {
  id: "member-7",
  displayName: "Avery",
  authenticated: true,
  profile: {
    handle: "avery",
    appearance: { skinTone: "#9c6b50", primaryColor: "#183d57", accentColor: "#55b5c6" },
  },
};

function options(over: Partial<SceneActorOptions> = {}): SceneActorOptions {
  return {
    kind: "humanoid",
    identity: MEMBER,
    fixedStepSeconds: 0.1,
    position: { x: 2, z: 3 },
    ...over,
  };
}

describe("playable city scene actor", () => {
  it("is inert by default, exposes defensive actions, and animates when active", () => {
    const actor = createSceneActor(options());
    assert.equal(actor.root.name, "playable-scene-actor");
    assert.ok(actor.root.getObjectByName("humanoid"));
    actor.setActions({ forward: 1 });
    const leaked = actor.actions();
    leaked.forward = -1;
    actor.tick(0.2);
    assert.equal(actor.state().z, 3);
    actor.setActive(true);
    assert.equal(actor.actions().forward, 0, "activation clears stale movement");
    actor.setActions({ forward: 1 });
    actor.tick(0.2);
    assert.ok(actor.state().z < 3);
    assert.ok(actor.state().posePhase > 0);
    assert.equal(actor.view.position.z, actor.root.position.z);
    actor.dispose();
  });

  it("maps crow metre-space flight into a scaled California board and chase pose", () => {
    const actor = createSceneActor(options({
      kind: "crow",
      mode: "flight",
      position: { x: 10, y: 4, z: -20 },
      sceneUnitsPerMetre: 0.02,
      sceneOrigin: { x: 100, y: 3, z: -50 },
      active: true,
      minFlightAltitude: 1,
      maxFlightAltitude: 6,
    }));
    assert.equal(actor.root.scale.x, 0.02);
    assert.deepEqual(actor.root.position.toArray(), [100.2, 3.08, -50.4]);
    actor.setActions({ forward: 1, climb: 1, pitch: 0.6, turn: 0.3 });
    actor.tick(0.4);
    assert.equal(actor.state().mode, "flight");
    assert.ok(actor.state().y > 4 && actor.state().y <= 6);
    assert.equal(actor.root.rotation.order, "YXZ");
    assert.equal(actor.root.rotation.x, actor.state().pitch);
    const crow = actor.root.getObjectByName("crow");
    assert.equal(crow?.userData.pose, "flap");
    actor.setActions({ glide: true, turn: 1 });
    actor.tick(0.3);
    assert.equal(actor.state().crowPose, "bank");
    assert.equal(crow?.userData.pose, "bank");
    assert.notEqual(actor.root.rotation.z, 0, "the scene root presents controller-owned bank");
    const pose = actor.followPose();
    assert.ok(pose.position.toArray().every(Number.isFinite));
    assert.ok(pose.target.toArray().every(Number.isFinite));
    pose.position.x = 999;
    assert.notEqual(actor.followPose().position.x, 999);
    actor.dispose();
  });

  it("can exaggerate presentation scale without changing metre-space motion", () => {
    const actor = createSceneActor(options({
      sceneUnitsPerMetre: 0.001,
      visualSceneUnitsPerMetre: 0.025,
      active: true,
    }));
    assert.equal(actor.root.scale.x, 0.025);
    actor.setActions({ forward: 1 });
    actor.tick(0.2);
    assert.ok(actor.state().z < 3);
    assert.ok(Math.abs(actor.root.position.z - actor.state().z * 0.001) < 1e-12);
    actor.dispose();
  });

  it("maps the unchanged dog controller snapshot to trot, run, turn and stop presentation", () => {
    const actor = createSceneActor(options({ kind: "dog", active: true }));
    const dog = actor.root.getObjectByName("dog") as THREE.Group;
    assert.equal(dog.userData.pose, "idle");

    actor.setActions({ forward: 1, turn: 0.6 });
    actor.tick(0.1);
    assert.equal(actor.state().speedMps, 2.125);
    assert.equal(dog.userData.pose, "trot");
    assert.equal(dog.userData.turnLean, 0.6);
    assert.equal(dog.userData.acceleration, 1);

    actor.setActions({ forward: 1, sprint: true });
    actor.tick(0.1);
    assert.equal(actor.state().speedMps, 5.625);
    assert.equal(dog.userData.pose, "run");

    actor.setActions({ forward: 0, sprint: false });
    actor.tick(0.1);
    assert.equal(actor.state().speedMps, 0);
    assert.equal(dog.userData.pose, "idle");
    assert.equal(dog.userData.acceleration, -1);
    actor.dispose();
  });

  it("keeps its stable root and identity while replacing procedural rigs", () => {
    const actor = createSceneActor(options({ active: true }));
    const root = actor.root;
    const firstRig = root.children[0];
    actor.switchActor("crow", undefined, "flight");
    assert.equal(actor.root, root);
    assert.equal(firstRig?.parent, null);
    assert.ok(root.getObjectByName("crow"));
    assert.equal(actor.state().mode, "flight");
    assert.deepEqual(actor.state().identity, MEMBER);

    const guest: ActorIdentity = {
      id: "anon-3",
      displayName: "Guest Crow",
      authenticated: false,
      profile: { appearance: { primaryColor: "#20252b" } },
    };
    const crowRig = root.children[0];
    actor.setIdentity(guest);
    assert.equal(crowRig?.parent, null);
    assert.deepEqual(actor.state().identity, guest);
    actor.switchActor("dog");
    assert.ok(root.getObjectByName("dog"));
    assert.equal(actor.state().mode, "ground");
    assert.deepEqual(actor.state().identity, guest);
    actor.dispose();
  });

  it("refreshes humanoid identity around a caller-owned face texture", () => {
    const actor = createSceneActor(options({ active: true }));
    const texture = new THREE.Texture();
    let textureDisposals = 0;
    texture.addEventListener("dispose", () => textureDisposals++);
    assert.equal(actor.attachFaceTexture(texture), true);
    const firstFace = actor.root.getObjectByName("humanoid.face") as THREE.Mesh;
    const firstMaterial = firstFace.material as THREE.MeshBasicMaterial;
    assert.equal(firstMaterial.map, texture);

    actor.setActions({ forward: 1 });
    actor.tick(0.2);
    const before = actor.state();
    actor.setIdentity({
      ...MEMBER,
      profile: { ...MEMBER.profile, appearance: { primaryColor: "#654321" } },
    });
    const nextFace = actor.root.getObjectByName("humanoid.face") as THREE.Mesh;
    assert.notEqual(nextFace, firstFace);
    assert.equal((nextFace.material as THREE.MeshBasicMaterial).map, texture);
    assert.equal(firstMaterial.map, null, "released rig no longer retains caller texture");
    assert.deepEqual(
      { x: actor.state().x, y: actor.state().y, z: actor.state().z, elapsedSteps: actor.state().elapsedSteps },
      { x: before.x, y: before.y, z: before.z, elapsedSteps: before.elapsedSteps },
    );

    actor.clearFaceTexture();
    assert.equal((nextFace.material as THREE.MeshBasicMaterial).map, null);
    actor.attachFaceTexture(texture);
    actor.dispose();
    assert.equal(textureDisposals, 0);
    assert.equal((nextFace.material as THREE.MeshBasicMaterial).map, null);
  });

  it("refuses to attach a humanoid face to anonymous animal actors", () => {
    const actor = createSceneActor(options());
    const texture = new THREE.Texture();
    actor.attachFaceTexture(texture);
    const oldFace = actor.root.getObjectByName("humanoid.face") as THREE.Mesh;
    const oldMaterial = oldFace.material as THREE.MeshBasicMaterial;
    actor.switchActor("crow", undefined, "flight");
    assert.equal(oldMaterial.map, null);
    assert.equal(actor.attachFaceTexture(texture), false);
    actor.switchActor("dog");
    assert.equal(actor.attachFaceTexture(texture), false);
    actor.switchActor("humanoid");
    const newFace = actor.root.getObjectByName("humanoid.face") as THREE.Mesh;
    assert.equal((newFace.material as THREE.MeshBasicMaterial).map, null, "animal role released the hidden face reference");
    actor.dispose();
  });

  it("applies kind requests from the normalized action stream exactly once", () => {
    const actor = createSceneActor(options({ active: true }));
    actor.setActions({ kindRequest: "crow", modeRequest: "flight", forward: 0.4 });
    actor.tick(0.1);
    assert.equal(actor.state().kind, "crow");
    assert.equal(actor.state().mode, "flight");
    assert.equal(actor.actions().kindRequest, "none");
    assert.equal(actor.actions().modeRequest, "none");
    assert.equal(actor.actions().forward, 0.4, "held axes survive edge clearing");
    assert.ok(actor.root.getObjectByName("crow"));
    actor.dispose();
  });

  it("removes and disposes the stable root idempotently", () => {
    const actor = createSceneActor(options());
    const parent = new THREE.Group();
    parent.add(actor.root);
    actor.dispose();
    actor.dispose();
    assert.equal(actor.root.parent, null);
    const before = actor.state();
    actor.tick(1);
    assert.deepEqual(actor.state(), before);
  });

  it("rejects invalid scene scaling and camera configuration", () => {
    assert.throws(() => createSceneActor(options({ sceneUnitsPerMetre: 0 })), RangeError);
    assert.throws(() => createSceneActor(options({ sceneOrigin: { x: Infinity, y: 0, z: 0 } })), RangeError);
    assert.throws(() => createSceneActor(options({ camera: { distance: -1 } })), RangeError);
  });
});
