import assert from "node:assert/strict";
import { describe, it } from "node:test";
import * as THREE from "three";
import {
  createScenePeers,
  scenePeerId,
  type ActorPoseSnapshot,
  type AircraftPoseSnapshot,
  type EntityPoseSnapshot,
  type InterestCell,
  type ScenePeersOptions,
  type VehiclePoseSnapshot,
} from "../realtime/index.ts";

const VELOCITY = { xMps: 0, yMps: 0, zMps: 0, yawDegPerSec: 0 };

function actor(
  id: string,
  kind: "humanoid" | "dog" | "crow",
  sequence: number,
  timestampMs: number,
  xM: number,
  cell: InterestCell = { kind: "floor", officeId: "hq", floorId: "one" },
): ActorPoseSnapshot {
  if (cell.kind === "california-tile") throw new Error("local poses cannot use tile cells");
  return {
    entity: "actor",
    actorId: id,
    kind,
    sequence,
    timestampMs,
    pose: { space: "local", cell, xM, yM: 2, zM: 3, headingDeg: 0, pitchDeg: 0 },
    velocity: { ...VELOCITY, xMps: 2 },
  };
}

function vehicle(sequence: number, timestampMs: number, lat: number): VehiclePoseSnapshot {
  return {
    entity: "vehicle",
    vehicleId: "ev-1",
    kind: "model-x",
    driverActorId: null,
    sequence,
    timestampMs,
    pose: { space: "geographic", lat, lng: -120, altitudeM: 10, headingDeg: 90, pitchDeg: 2 },
    velocity: { ...VELOCITY, xMps: 20 },
    steering: 0.4,
    wheelRadians: 3,
  };
}

function aircraft(sequence: number, timestampMs: number, lat: number): AircraftPoseSnapshot {
  return {
    entity: "aircraft",
    aircraftId: "evtol-1",
    kind: "electric-vtail",
    pilotActorId: "pilot-1",
    sequence,
    timestampMs,
    pose: { space: "geographic", lat, lng: -121, altitudeM: 1_500, headingDeg: 30, pitchDeg: 5 },
    velocity: { xMps: 30, yMps: 2, zMps: 52, yawDegPerSec: 4 },
    rollDeg: 18,
    throttle: 0.72,
    rollInput: 0.4,
    pitchInput: -0.2,
    yawInput: 0.1,
    fanRadians: 2.4,
  };
}

function fixture(over: Partial<ScenePeersOptions> = {}) {
  return createScenePeers({
    project: (lat, lng) => [lng * 2, -lat * 3],
    groundAt: () => 5,
    geographicSceneUnitsPerMetre: 0.1,
    localSceneUnitsPerMetre: 1,
    interpolation: { interpolationDelayMs: 0, maximumExtrapolationMs: 0 },
    ...over,
  });
}

function mesh(root: THREE.Object3D, name: string): THREE.Mesh {
  const found = root.getObjectByName(name);
  if (!(found instanceof THREE.Mesh)) throw new Error(`missing mesh ${name}`);
  return found;
}

describe("remote scene peers", () => {
  it("maps local poses directly and keeps one identity-free stable root per peer", () => {
    const peers = fixture();
    const first = actor("private-member-id", "humanoid", 1, 1_000, 1);
    assert.equal(peers.upsert(first), true);
    assert.deepEqual(peers.ids(), ["actor:private-member-id"]);
    assert.equal(peers.count(), 1);
    const root = peers.root.children[0] as THREE.Group;
    assert.equal(root.name, "remote-peer");
    assert.equal(JSON.stringify(root.userData).includes("private-member-id"), false);
    assert.equal(root.getObjectByName("humanoid.face") instanceof THREE.Mesh, true);
    assert.equal((mesh(root, "humanoid.face").material as THREE.MeshBasicMaterial).map, null);
    peers.tick(1_000);
    assert.deepEqual(root.position.toArray(), [1, 2, 3]);

    assert.equal(peers.upsert(actor("private-member-id", "humanoid", 2, 2_000, 5)), true);
    peers.tick(1_500);
    assert.equal(peers.root.children[0], root);
    assert.equal(root.position.x, 3);
    assert.equal(peers.upsert(actor("private-member-id", "humanoid", 2, 2_100, 8)), false);
    peers.dispose();
  });

  it("resets interpolation at local interest-cell boundaries without replacing the root", () => {
    const peers = fixture();
    peers.upsert(actor("a", "dog", 1, 1_000, 0));
    peers.upsert(actor("a", "dog", 2, 2_000, 10));
    peers.tick(1_500);
    const root = peers.root.children[0] as THREE.Group;
    assert.equal(root.position.x, 5);
    const nextCell = { kind: "floor", officeId: "hq", floorId: "two" } as const;
    assert.equal(peers.upsert(actor("a", "dog", 3, 3_000, 100, nextCell)), true);
    peers.tick(2_500);
    assert.equal(peers.root.children[0], root);
    assert.equal(root.position.x, 100, "new coordinate frame is held, never mixed with old floor");
    peers.dispose();
  });

  it("derives deterministic dog gait and turn lean from the existing peer snapshot", () => {
    const peers = fixture();
    const snapshot = actor("dog", "dog", 1, 1_000, 0);
    snapshot.velocity.xMps = 3.2;
    snapshot.velocity.yawDegPerSec = -80;
    assert.equal(peers.upsert(snapshot), true);
    assert.equal(peers.tick(1_000), 1);
    const dog = peers.root.children[0]?.getObjectByName("dog") as THREE.Group;
    assert.equal(dog.userData.pose, "run");
    assert.equal(dog.userData.turnLean, -0.5);
    const body = dog.getObjectByName("dog.body") as THREE.Group;
    assert.ok(body.rotation.z > 0);
    peers.dispose();
  });

  it("projects geographic vehicles onto caller terrain and renders a generic black EV", () => {
    const peers = fixture();
    const snapshot = vehicle(1, 1_000, 34);
    assert.equal(scenePeerId(snapshot), "vehicle:ev-1");
    peers.upsert(snapshot);
    assert.equal(peers.tick(1_000), 1);
    const root = peers.root.children[0] as THREE.Group;
    assert.deepEqual(root.position.toArray(), [-240, 6, -102]);
    assert.equal(root.scale.x, 0.1);
    assert.ok(Math.abs(root.rotation.y + Math.PI / 2) < 1e-12);
    assert.equal(root.getObjectByName("generic-black-ev")?.userData.vehicleModel, "generic-black-ev");
    assert.equal(root.getObjectByName("frontLeft.spin")?.rotation.x, -snapshot.wheelRadians);
    const paint = root.getObjectByName("model-x.body:model-x.paint") as THREE.Mesh;
    assert.equal((paint.material as THREE.MeshStandardMaterial).color.getHex(), 0x050607);
    peers.dispose();
  });

  it("renders a remote piloted aircraft with authoritative bank and rig state", () => {
    const peers = fixture();
    const snapshot = aircraft(1, 1_000, 37.7);
    assert.equal(scenePeerId(snapshot), "aircraft:evtol-1");
    assert.equal(peers.upsert(snapshot), true);
    assert.equal(peers.tick(1_000), 1);
    const root = peers.root.children[0] as THREE.Group;
    assert.deepEqual(root.position.toArray(), [-242, 155, -113.10000000000001]);
    assert.ok(Math.abs(root.rotation.x - 5 * Math.PI / 180) < 1e-12);
    assert.ok(Math.abs(root.rotation.y + 30 * Math.PI / 180) < 1e-12);
    assert.ok(Math.abs(root.rotation.z + 18 * Math.PI / 180) < 1e-12);
    assert.ok(root.getObjectByName("electric-aircraft"));
    assert.equal(root.getObjectByName("electric-aircraft.fan-left")?.rotation.z, 2.4);
    assert.ok(Math.abs((root.getObjectByName("electric-aircraft.aileron-left")?.rotation.x ?? 0) - 0.18) < 1e-12);
    peers.dispose();
  });

  it("shares prototype resources while kind changes retain the entity root", () => {
    const peers = fixture();
    peers.upsert(actor("one", "humanoid", 1, 1_000, 0));
    peers.upsert(actor("two", "humanoid", 1, 1_000, 2));
    const one = peers.root.children[0] as THREE.Group;
    const two = peers.root.children[1] as THREE.Group;
    assert.equal(mesh(one, "humanoid.chest").geometry, mesh(two, "humanoid.chest").geometry);
    assert.equal(mesh(one, "humanoid.chest").material, mesh(two, "humanoid.chest").material);
    const oldRig = one.children[0];
    peers.upsert(actor("one", "crow", 2, 2_000, 1));
    peers.tick(2_000);
    assert.equal(peers.root.children[0], one);
    assert.equal(oldRig?.parent, null);
    assert.ok(one.getObjectByName("crow"));
    peers.dispose();
  });

  it("bounds nearby peers and removes/clears without prematurely disposing shared assets", () => {
    const peers = fixture({ maximumPeers: 2 });
    peers.upsert(actor("one", "humanoid", 1, 1_000, 0));
    peers.upsert(actor("two", "dog", 1, 1_000, 1));
    assert.equal(peers.upsert(actor("three", "crow", 1, 1_000, 2)), false);
    const firstPeer = peers.root.children[0];
    if (!firstPeer) throw new Error("missing first peer");
    const geometry = mesh(firstPeer, "humanoid.chest").geometry;
    let disposals = 0;
    geometry.addEventListener("dispose", () => disposals++);
    assert.equal(peers.remove("actor:one"), true);
    assert.equal(peers.remove("actor:one"), false);
    assert.equal(disposals, 0, "removing one clone cannot release shared prototype resources");
    peers.clear();
    assert.equal(peers.count(), 0);
    assert.deepEqual(peers.ids(), []);
    assert.equal(disposals, 0);
    peers.dispose();
    peers.dispose();
    assert.equal(disposals, 1);
    assert.equal(peers.upsert(actor("late", "humanoid", 1, 1_000, 0)), false);
  });

  it("contains invalid snapshots and projection failures", () => {
    const peers = fixture({ project: () => [Number.NaN, 0] });
    assert.equal(peers.upsert({} as EntityPoseSnapshot), false);
    peers.upsert(vehicle(1, 1_000, 34));
    assert.equal(peers.tick(1_000), 0);
    assert.equal(peers.root.children[0]?.visible, false);
    assert.equal(peers.tick(Number.NaN), 0);
    peers.dispose();
    assert.throws(() => fixture({ maximumPeers: 0 }), RangeError);
  });
});
