import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  ActorController,
  normalizeActorActions,
  replayActorInputs,
  type ActorControllerOptions,
  type ActorIdentity,
} from "../actors/index.ts";

const IDENTITY: ActorIdentity = {
  id: "person-42",
  displayName: "River",
  authenticated: true,
  profile: {
    handle: "river",
    pronouns: "they/them",
    faceImageUrl: "/profiles/42/face",
    appearance: { skinTone: "#8f6048", primaryColor: "#17324d", accentColor: "#64b5c8" },
  },
};

function groundOptions(over: Partial<ActorControllerOptions> = {}): ActorControllerOptions {
  return {
    kind: "humanoid",
    identity: IDENTITY,
    position: { x: 2, y: 99, z: 3 },
    fixedStepSeconds: 0.1,
    ...over,
  };
}

describe("playable actor input and ground mode", () => {
  it("normalizes every adapter axis and the ground movement disc", () => {
    const action = normalizeActorActions({
      forward: 3,
      right: 4,
      turn: -9,
      pitch: Number.NaN,
      climb: Infinity,
      sprint: true,
      glide: true,
      kindRequest: "crow",
      modeRequest: "flight",
    });
    assert.ok(Math.abs(Math.hypot(action.forward, action.right) - 1) < 1e-12);
    assert.equal(action.turn, -1);
    assert.equal(action.pitch, 0);
    assert.equal(action.climb, 0);
    assert.equal(action.kindRequest, "crow");
    assert.equal(action.modeRequest, "flight");
    assert.equal(action.sprint, true);
    assert.deepEqual(normalizeActorActions(undefined), {
      forward: 0,
      right: 0,
      turn: 0,
      pitch: 0,
      climb: 0,
      sprint: false,
      glide: false,
      modeRequest: "none",
      kindRequest: "none",
      reset: false,
    });
  });

  it("moves and turns in deterministic fixed steps while requesting a gait phase", () => {
    const one = new ActorController(groundOptions());
    const many = new ActorController(groundOptions());
    one.tick(0.05, { forward: 1 });
    assert.equal(one.state().z, 3);
    one.tick(0.15, { forward: 1, turn: 0.25 });
    one.tick(0.2, { forward: 1, turn: 0.25 });
    for (let index = 0; index < 4; index++) many.tick(0.1, { forward: 1, turn: 0.25 });
    assert.deepEqual(one.snapshot(), many.snapshot());
    assert.ok(one.state().z < 3);
    assert.ok(one.state().yaw > 0);
    assert.ok(one.state().posePhase > 0 && one.state().posePhase < Math.PI * 2);
    assert.equal(one.state().poseAmount, 1);
    assert.equal(one.state().y, 0, "ground datum wins over a stale spawn y");
  });

  it("bounds a ground actor and applies dog gait speed without producing non-finite state", () => {
    const controller = new ActorController(groundOptions({
      kind: "dog",
      position: { x: 0.95, z: 0.95 },
      horizontalBounds: { minX: -1, maxX: 1, minZ: -1, maxZ: 1 },
    }));
    for (let index = 0; index < 20; index++) controller.stepFixed({ right: 1, forward: -1, sprint: true });
    assert.equal(controller.state().x, 1);
    assert.equal(controller.state().z, 1);
    assert.ok(Number.isFinite(controller.state().distanceM));
    assert.equal(controller.state().kind, "dog");
  });
});

describe("crow flight", () => {
  it("turns, pitches, climbs and glides inside strict altitude bounds", () => {
    const controller = new ActorController(groundOptions({
      kind: "crow",
      mode: "flight",
      position: { x: 0, y: 1, z: 0 },
      minFlightAltitude: 1,
      maxFlightAltitude: 3,
    }));
    for (let index = 0; index < 30; index++) {
      controller.stepFixed({ forward: 1, turn: 0.5, pitch: 1, climb: 1 });
    }
    assert.equal(controller.state().y, 3);
    assert.equal(controller.state().altitudeBoundContact, "maximum");
    assert.ok(controller.state().yaw !== 0);
    assert.ok(controller.state().pitch > 0 && controller.state().pitch < Math.PI / 2);
    const phase = controller.state().posePhase;
    controller.stepFixed({ glide: true, climb: -1, pitch: -1 });
    assert.equal(controller.state().gliding, true);
    assert.equal(controller.state().posePhase, phase, "gliding holds the wing phase");
    for (let index = 0; index < 80; index++) controller.stepFixed({ glide: true, climb: -1, pitch: -1 });
    assert.equal(controller.state().y, 1);
    assert.equal(controller.state().altitudeBoundContact, "minimum");
  });

  it("allows only a crow to enter flight and lands exactly on the ground datum", () => {
    const controller = new ActorController(groundOptions({ groundY: 12 }));
    controller.stepFixed({ modeRequest: "flight" });
    assert.equal(controller.state().mode, "ground");
    controller.stepFixed({ kindRequest: "crow", modeRequest: "flight" });
    assert.equal(controller.state().mode, "flight");
    assert.ok(controller.state().y >= 12.75);
    controller.stepFixed({ modeRequest: "ground" });
    assert.equal(controller.state().mode, "ground");
    assert.equal(controller.state().y, 12);
    assert.equal(controller.state().crowPose, "perch");
    assert.equal(controller.state().perched, true);
  });

  it("models deterministic wind, radial thermal lift, banking and finite energy", () => {
    const options = groundOptions({
      kind: "crow",
      mode: "flight",
      position: { x: 0, y: 20, z: 0 },
      minFlightAltitude: 2,
      maxFlightAltitude: 80,
      crowWind: { xMps: 2, zMps: -0.5 },
      crowThermals: [{ x: 0, z: 0, radiusM: 100, liftMps: 2.5 }],
      crowInitialEnergy: 0.6,
      crowEnergyDrainPerSecond: 0.1,
      crowEnergyRecoveryPerSecond: 0.2,
    });
    const first = new ActorController(options);
    const replay = new ActorController(options);
    for (let index = 0; index < 10; index += 1) {
      const actions = { forward: 1, turn: 0.7, climb: 0.1 };
      first.stepFixed(actions);
      replay.stepFixed(actions);
    }
    assert.deepEqual(first.snapshot(), replay.snapshot());
    assert.ok(first.state().x > 0, "the configured eastward air mass advects the bird");
    assert.ok(first.state().thermalLiftMps > 2, "the bird remains near the thermal core");
    assert.ok(first.state().liftMps > first.state().thermalLiftMps);
    assert.ok(first.state().roll > 0);
    assert.equal(first.state().crowPose, "flap");
    assert.ok(first.state().flightEnergy < 0.6 && first.state().flightEnergy >= 0);

    const spent = first.state().flightEnergy;
    for (let index = 0; index < 10; index += 1) first.stepFixed({ glide: true, turn: 0.7 });
    assert.ok(first.state().flightEnergy > spent);
    assert.equal(first.state().crowPose, "bank");
    assert.equal(first.state().gliding, true);

    first.stepFixed({ forward: -1, glide: true, pitch: -1 });
    assert.equal(first.state().crowPose, "tuck");
    assert.ok(first.state().flightEnergy >= 0 && first.state().flightEnergy <= 1);
  });

  it("replays wind and thermal flight bit-for-bit across powered, glide and tuck states", () => {
    const options = groundOptions({
      kind: "crow",
      mode: "flight",
      position: { x: -10, y: 15, z: 5 },
      minFlightAltitude: 2,
      maxFlightAltitude: 100,
      crowWind: { xMps: -1.25, zMps: 0.8 },
      crowThermals: [
        { x: -10, z: 5, radiusM: 45, liftMps: 3.2 },
        { x: 80, z: -20, radiusM: 25, liftMps: 1.4 },
      ],
      crowInitialEnergy: 0.82,
    });
    const frames = [
      { steps: 90, actions: { forward: 0.8, turn: 0.3, climb: 0.2 } },
      { steps: 45, actions: { glide: true, turn: -0.7 } },
      { steps: 20, actions: { forward: -1, glide: true, pitch: -0.7 } },
      { steps: 30, actions: { forward: 0.4, pitch: 0.2 } },
    ];
    const first = replayActorInputs(options, frames);
    const second = replayActorInputs(options, frames);
    assert.deepEqual(first, second);
    assert.equal(first.final.elapsedSteps, 185);
    assert.ok(first.trajectory.every((state) =>
      state.flightEnergy >= 0 && state.flightEnergy <= 1 &&
      Number.isFinite(state.liftMps) && Number.isFinite(state.roll)));
  });
});

describe("actor identity, reset and replay", () => {
  it("switches actor kind without losing a detached serializable identity", () => {
    const controller = new ActorController(groundOptions());
    const identityBefore = JSON.stringify(controller.snapshot().identity);
    controller.setActorKind("dog");
    controller.setActorKind("crow");
    assert.equal(JSON.stringify(controller.snapshot().identity), identityBefore);
    const detached = controller.snapshot();
    detached.identity.profile.handle = "tampered";
    assert.equal(controller.state().identity.profile.handle, "river");
    assert.doesNotThrow(() => JSON.stringify(controller.snapshot()));
  });

  it("resets exactly and caps a resumed background tab", () => {
    const controller = new ActorController(groundOptions());
    const spawn = controller.snapshot();
    controller.stepFixed({ kindRequest: "crow", modeRequest: "flight", climb: 1 });
    controller.setIdentity({ id: "anon-9", displayName: "Guest", authenticated: false, profile: {} });
    controller.tick(Number.NaN, { forward: 1 });
    controller.tick(-4, { forward: 1 });
    const steps = controller.tick(600, { forward: 1 });
    assert.ok(steps <= 3);
    controller.stepFixed({ reset: true });
    assert.deepEqual(controller.snapshot(), spawn);
  });

  it("replays timed cross-kind input bit-for-bit", () => {
    const frames = [
      { steps: 12, actions: { forward: 1, turn: 0.2 } },
      { steps: 1, actions: { kindRequest: "crow" as const, modeRequest: "flight" as const } },
      { steps: 30, actions: { forward: 0.7, turn: -0.4, climb: 0.5 } },
      { steps: 15, actions: { glide: true, pitch: -0.2 } },
      { steps: 1, actions: { kindRequest: "dog" as const } },
      { steps: 8, actions: { right: 1, sprint: true } },
    ];
    const first = replayActorInputs(groundOptions(), frames);
    const second = replayActorInputs(groundOptions(), frames);
    assert.deepEqual(first, second);
    assert.equal(first.trajectory.length, 68);
    assert.equal(first.final.elapsedSteps, 67);
    assert.equal(first.final.kind, "dog");
    assert.equal(first.final.mode, "ground");
  });

  it("rejects invalid configuration and identity before creating state", () => {
    assert.throws(() => new ActorController(groundOptions({ minFlightAltitude: 5, maxFlightAltitude: 2 })), RangeError);
    assert.throws(() => new ActorController(groundOptions({ walkSpeedMps: 0 })), RangeError);
    assert.throws(() => new ActorController(groundOptions({ crowEnergyDrainPerSecond: 0 })), RangeError);
    assert.throws(() => new ActorController(groundOptions({
      crowThermals: [{ x: 0, z: 0, radiusM: 0, liftMps: 2 }],
    })), RangeError);
    assert.throws(() => new ActorController(groundOptions({
      horizontalBounds: { minX: 2, maxX: 1, minZ: 0, maxZ: 1 },
    })), RangeError);
    assert.throws(() => new ActorController(groundOptions({ identity: { ...IDENTITY, id: "" } })), RangeError);
  });
});
