import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import { createSceneActor, type ActorIdentity } from "../actors/index.ts";
import { createOfficeWalker, type OfficeActorAppearance } from "../interiors/officeWalker.ts";
import { Plan } from "../interiors/plan.ts";
import type { Level, Office, Room } from "../interiors/types.ts";
import { createJourney, journeyReducer, type JourneyActor } from "../journey/index.ts";
import {
  actorKindForPresence,
  createDefaultLocalProfile,
  resolveHumanoidAppearance,
} from "../profile/index.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 officePlan(): Plan {
  const level: Level = {
    id: "ground",
    name: "Ground",
    elevation: 0,
    wallHeight: 3,
    wallThickness: 0.12,
    floorplan: {
      rooms: [FLOOR],
      walls: [{
        id: "dividing-wall",
        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 office: Office = {
    id: "handoff-office",
    name: "Handoff Office",
    levels: [level],
    viewpoints: [],
  };
  return new Plan(office, { warn: false });
}

function materialColor(root: THREE.Object3D, meshName: string): string {
  const mesh = root.getObjectByName(meshName);
  assert.ok(mesh instanceof THREE.Mesh, `${meshName} exists`);
  assert.ok(mesh.material instanceof THREE.MeshStandardMaterial, `${meshName} has a standard material`);
  return `#${mesh.material.color.getHexString()}`;
}

describe("city and office actor acceptance handoff", () => {
  it("hands an anonymous crow to an office dog, walks through doors, and returns to the same crow", () => {
    const journeyActor: JourneyActor = {
      id: "anonymous",
      kind: "crow",
      signedIn: false,
      profile: { displayName: "Guest" },
    };
    const identity: ActorIdentity = {
      id: journeyActor.id,
      displayName: journeyActor.profile.displayName,
      authenticated: false,
      profile: {},
    };
    let journey = createJourney({ actor: journeyActor });

    assert.equal(actorKindForPresence(false, "outdoors"), "crow");
    const cityActor = createSceneActor({
      kind: "crow",
      mode: "flight",
      identity,
      position: { x: 12, y: 30, z: -8 },
      active: true,
      fixedStepSeconds: 0.1,
      minFlightAltitude: 5,
      maxFlightAltitude: 60,
    });
    cityActor.setActions({ forward: 1, climb: 0.2 });
    cityActor.tick(0.5);
    cityActor.setActive(false);
    const parkedCityState = cityActor.state();

    journey = journeyReducer(journey, { type: "enter-office", officeId: "lumbridge-hq" });
    assert.equal(journey.location.scale, "office");
    assert.deepEqual(journey.actor, journeyActor, "entering never rewrites the serializable identity");
    assert.equal(actorKindForPresence(false, "office"), "dog");

    const walker = createOfficeWalker(officePlan(), {
      levelId: "ground",
      position: { x: 4, z: 4 },
      actor: { kind: "anonymous-dog" },
      active: true,
      speed: 2,
      fixedStep: 0.1,
    });
    walker.setAction({ x: 1, z: 0 });
    for (let step = 0; step < 10; step += 1) walker.tick(0.1);
    assert.ok(walker.state().position.x > 5.5, "the possessed dog passes through the authored door gap");

    walker.reset({ levelId: "ground", position: { x: 4, z: 1 } });
    walker.setAction({ x: 1, z: 0 });
    for (let step = 0; step < 10; step += 1) walker.tick(0.1);
    assert.ok(walker.state().position.x < 5, "the same controller cannot cross the solid wall");

    journey = journeyReducer(journey, { type: "leave-office" });
    assert.equal(journey.location.scale, "california");
    assert.deepEqual(journey.actor, journeyActor);
    assert.deepEqual(cityActor.state(), parkedCityState, "the outdoor actor stays parked during the office visit");
    assert.deepEqual(cityActor.state().identity, identity);

    cityActor.setActive(true);
    cityActor.setActions({ forward: 1 });
    cityActor.tick(0.2);
    assert.ok(cityActor.state().distanceM > parkedCityState.distanceM, "the returned crow is playable");
    walker.dispose();
    cityActor.dispose();
  });

  it("renders one signed-in humanoid appearance on both sides of the office door", () => {
    const profile = createDefaultLocalProfile("member-acceptance", "Morgan");
    const appearance = resolveHumanoidAppearance(profile.appearance);
    const identity: ActorIdentity = {
      id: "member-acceptance",
      displayName: profile.displayName,
      authenticated: true,
      profile: {
        appearance: {
          skinTone: appearance.skinTone,
          primaryColor: appearance.outfitColor,
          accentColor: appearance.accentColor,
          hairColor: appearance.hairColor,
          bodyShape: appearance.bodyShape,
        },
      },
    };
    const officeAppearance: OfficeActorAppearance = {
      kind: "humanoid",
      skinTone: appearance.skinTone,
      outfitColor: appearance.outfitColor,
      accentColor: appearance.accentColor,
      hairColor: appearance.hairColor,
      bodyShape: appearance.bodyShape,
    };

    assert.equal(actorKindForPresence(true, "outdoors"), "humanoid");
    assert.equal(actorKindForPresence(true, "office"), "humanoid");
    const cityActor = createSceneActor({ kind: "humanoid", identity });
    const walker = createOfficeWalker(officePlan(), {
      levelId: "ground",
      position: { x: 2, z: 2 },
      actor: officeAppearance,
    });

    assert.equal(cityActor.state().identity.id, "member-acceptance");
    assert.equal(cityActor.state().identity.displayName, "Morgan");
    assert.ok(cityActor.root.getObjectByName("humanoid"));
    assert.equal(walker.root.userData.actorType, "humanoid");
    assert.equal(materialColor(cityActor.root, "humanoid.chest"), appearance.outfitColor);
    assert.equal(materialColor(walker.root, "humanoid.chest"), appearance.outfitColor);
    assert.equal(materialColor(cityActor.root, "humanoid.chest.accent"), appearance.accentColor);
    assert.equal(materialColor(walker.root, "humanoid.chest.accent"), appearance.accentColor);
    assert.equal(materialColor(cityActor.root, "humanoid.hair"), appearance.hairColor);
    assert.equal(materialColor(walker.root, "humanoid.hair"), appearance.hairColor);

    walker.dispose();
    cityActor.dispose();
  });

  it("moves one caller-owned live face across the office door and appearance rebuilds", () => {
    const identity: ActorIdentity = {
      id: "member-camera",
      displayName: "Camera Member",
      authenticated: true,
      profile: { appearance: { primaryColor: "#18222d", accentColor: "#55b8c8" } },
    };
    const cityActor = createSceneActor({ kind: "humanoid", identity });
    const walker = createOfficeWalker(officePlan(), {
      levelId: "ground",
      position: { x: 2, z: 2 },
      actor: { kind: "humanoid", outfitColor: "#18222d", accentColor: "#55b8c8" },
    });
    const texture = new THREE.Texture();
    let disposals = 0;
    texture.addEventListener("dispose", () => { disposals += 1; });

    assert.equal(cityActor.attachFaceTexture(texture), true);
    assert.equal((cityActor.root.getObjectByName("humanoid.face") as THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicMaterial>).material.map, texture);
    cityActor.clearFaceTexture();
    assert.equal(walker.attachFaceTexture(texture), true);
    walker.setAppearance({ kind: "humanoid", outfitColor: "#53616d", accentColor: "#db6d62" });
    assert.equal((walker.root.getObjectByName("humanoid.face") as THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicMaterial>).material.map, texture);

    walker.clearFaceTexture();
    assert.equal(cityActor.attachFaceTexture(texture), true);
    cityActor.setIdentity({
      ...identity,
      profile: { appearance: { primaryColor: "#53616d", accentColor: "#db6d62" } },
    });
    assert.equal((cityActor.root.getObjectByName("humanoid.face") as THREE.Mesh<THREE.BufferGeometry, THREE.MeshBasicMaterial>).material.map, texture);
    walker.dispose();
    cityActor.dispose();
    assert.equal(disposals, 0, "actor adapters never dispose the app-owned live texture");
  });
});
