import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import { createOfficeWalker } from "../interiors/officeWalker.ts";
import { Plan } from "../interiors/plan.ts";
import type { Level, Office, Room, Wall } from "../interiors/types.ts";

const FLOOR: Room = {
  id: "floor",
  name: "Floor",
  floor: "floor" as never,
  outline: [{ x: 0, z: 0 }, { x: 10, z: 0 }, { x: 10, z: 8 }, { x: 0, z: 8 }],
};

function makePlan(walls: Wall[] = [], elevation = 2.5): Plan {
  const level: Level = {
    id: "ground",
    name: "Ground",
    elevation,
    wallHeight: 3,
    wallThickness: 0.1,
    floorplan: { rooms: [FLOOR], walls },
  };
  const office: Office = { id: "actor-test", name: "Actor Test", levels: [level], viewpoints: [] };
  return new Plan(office, { warn: false });
}

describe("office walker actor adapter", () => {
  it("is inert by default and becomes a floor-aware humanoid when activated", () => {
    const actor = createOfficeWalker(makePlan(), {
      levelId: "ground",
      position: { x: 2, z: 2 },
      speed: 1,
      fixedStep: 0.1,
    });
    assert.equal(actor.state().actor, "humanoid");
    assert.equal(actor.root.position.y, 2.5);
    actor.setAction({ x: 2, z: 0 });
    actor.tick(0.2);
    assert.deepEqual(actor.state().position, { x: 2, z: 2 });
    actor.setActive(true);
    assert.deepEqual(actor.action(), { x: 0, z: 0 }, "activation never replays stale input");
    actor.setAction({ x: 2, z: 0 });
    actor.tick(0.2);
    assert.ok(actor.state().position.x > 2.19);
    assert.equal(actor.view.position.y, 2.5);
    assert.ok(Math.abs(actor.root.rotation.y + Math.PI / 2) < 1e-12);
    actor.dispose();
  });

  it("builds an anonymous dog and publishes a defensive chase-camera contract", () => {
    const actor = createOfficeWalker(makePlan(), {
      levelId: "ground",
      position: { x: 4, z: 4 },
      actor: { kind: "anonymous-dog", coatColor: 0x222222 },
      active: true,
    });
    assert.equal(actor.root.userData.actorType, "anonymous-dog");
    const pose = actor.followPose();
    assert.ok(pose.position.z > 4, "camera is behind a -Z-facing actor");
    assert.ok(pose.target.z < 4, "camera aims ahead of the actor");
    pose.position.x = 999;
    assert.notEqual(actor.followPose().position.x, 999);
    actor.dispose();
  });

  it("presents deterministic dog start, office turn, trot and stop semantics", () => {
    const actor = createOfficeWalker(makePlan(), {
      levelId: "ground",
      position: { x: 4, z: 4 },
      actor: { kind: "anonymous-dog" },
      speed: 1.6,
      fixedStep: 0.1,
      active: true,
    });
    const dog = actor.root.getObjectByName("dog") as THREE.Group;
    assert.equal(dog.userData.pose, "idle");

    actor.setAction({ x: 0, z: -1 });
    actor.tick(0.1);
    assert.equal(dog.userData.pose, "walk", "eased first step does not pop directly into a trot");
    assert.equal(dog.userData.acceleration, 1);
    actor.tick(0.1);
    assert.equal(dog.userData.pose, "trot");

    actor.setAction({ x: 1, z: 0 });
    actor.tick(0.1);
    assert.ok(Math.abs(Number(dog.userData.turnLean)) > 0.9);
    const body = dog.getObjectByName("dog.body") as THREE.Group;
    assert.notEqual(body.rotation.z, 0);

    actor.setAction({ x: 0, z: 0 });
    actor.tick(0.1);
    assert.equal(dog.userData.pose, "idle");
    assert.equal(dog.userData.acceleration, -1);
    actor.dispose();
  });

  it("uses resolved door gaps while retaining wall collision and reset state", () => {
    const plan = makePlan([{
      id: "divider",
      from: { x: 5, z: 0 },
      to: { x: 5, z: 8 },
      openings: [{ kind: "door", start: 3.4, width: 1.2, sill: 0, head: 2.1 }],
    }]);
    const actor = createOfficeWalker(plan, {
      levelId: "ground",
      position: { x: 4, z: 4 },
      speed: 2,
      fixedStep: 0.1,
      active: true,
    });
    actor.setAction({ x: 1, z: 0 });
    for (let index = 0; index < 10; index += 1) actor.tick(0.1);
    assert.ok(actor.state().position.x > 5.5);
    const reset = actor.reset({ levelId: "ground", position: { x: 2, z: 2 } });
    assert.deepEqual(reset.position, { x: 2, z: 2 });
    assert.deepEqual(reset.action, { x: 0, z: 0 });
    assert.equal(reset.distance, 0);
    actor.dispose();
  });

  it("removes and disposes its actor root idempotently", () => {
    const actor = createOfficeWalker(makePlan(), { levelId: "ground", position: { x: 2, z: 2 } });
    const parent = new THREE.Group();
    parent.add(actor.root);
    actor.dispose();
    actor.dispose();
    assert.equal(actor.root.parent, null);
  });

  it("refreshes appearance under a stable root without changing walker or camera state", () => {
    const actor = createOfficeWalker(makePlan(), {
      levelId: "ground",
      position: { x: 2, z: 2 },
      speed: 1,
      fixedStep: 0.1,
      active: true,
      camera: { distance: 4, height: 2.8, targetHeight: 1.1, lookAhead: 0.9 },
    });
    const root = actor.root;
    actor.setAction({ x: 1, z: 0 });
    actor.tick(0.3);
    const before = actor.state();
    const cameraBefore = actor.followPose();
    const oldRig = root.children[0];
    const after = actor.setAppearance({ kind: "anonymous-dog", coatColor: 0x222222 });
    assert.equal(actor.root, root);
    assert.equal(oldRig?.parent, null);
    assert.equal(after.actor, "anonymous-dog");
    assert.deepEqual(after.position, before.position);
    assert.deepEqual(after.facing, before.facing);
    assert.equal(after.distance, before.distance);
    assert.equal(after.active, before.active);
    assert.deepEqual(after.action, before.action);
    assert.deepEqual(actor.followPose().position.toArray(), cameraBefore.position.toArray());
    assert.deepEqual(actor.followPose().target.toArray(), cameraBefore.target.toArray());
    assert.equal(root.userData.actorType, "anonymous-dog");
    actor.dispose();
  });

  it("keeps humanoid face texture caller-owned across a skin refresh and clear", () => {
    const actor = createOfficeWalker(makePlan(), {
      levelId: "ground",
      position: { x: 2, z: 2 },
      actor: { kind: "humanoid", outfitColor: 0x112233 },
    });
    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.setAppearance({ kind: "humanoid", outfitColor: 0x334455, bodyShape: "broad" });
    const secondFace = actor.root.getObjectByName("humanoid.face") as THREE.Mesh;
    assert.notEqual(secondFace, firstFace);
    assert.equal(firstMaterial.map, null);
    assert.equal((secondFace.material as THREE.MeshBasicMaterial).map, texture);
    actor.clearFaceTexture();
    assert.equal((secondFace.material as THREE.MeshBasicMaterial).map, null);
    actor.attachFaceTexture(texture);
    actor.dispose();
    assert.equal(textureDisposals, 0);
    assert.equal((secondFace.material as THREE.MeshBasicMaterial).map, null);
  });

  it("does not accept a face texture while the office actor is anonymous", () => {
    const actor = createOfficeWalker(makePlan(), {
      levelId: "ground",
      position: { x: 2, z: 2 },
      actor: { kind: "anonymous-dog" },
    });
    assert.equal(actor.attachFaceTexture(new THREE.Texture()), false);
    actor.dispose();
  });
});
