import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  aircraftActionsFromPlay,
  cameraRelativePlanar,
  crowActionsFromPlay,
  groundActorActionsFromPlay,
  PlayInputRouter,
  sampleStandardPlayGamepad,
  vehicleActionsFromPlay,
} from "../input/play.ts";

function pad(over: { axes?: number[]; buttons?: Record<number, number> } = {}) {
  return {
    axes: over.axes ?? [0, 0, 0, 0],
    buttons: Array.from({ length: 10 }, (_, index) => ({
      pressed: (over.buttons?.[index] ?? 0) > 0.5,
      value: over.buttons?.[index] ?? 0,
    })),
  };
}

describe("multi-source play input", () => {
  it("does not let one source release another source's held control", () => {
    const router = new PlayInputRouter();
    router.setDigital("keyboard", "forward", true);
    router.setDigital("pointer:7", "forward", true);
    router.clearSource("pointer:7");
    assert.equal(router.snapshot().moveY, 1);
    router.setDigital("keyboard", "forward", false);
    assert.deepEqual(router.snapshot(), {
      moveX: 0, moveY: 0, lookX: 0, lookY: 0, throttle: 0, brake: 0,
      ascend: 0, descend: 0, primary: false, secondary: false,
    });
  });

  it("merges strongest analogue intent with independent digital actions", () => {
    const router = new PlayInputRouter();
    router.setAxes("gamepad", { moveX: 0.45, moveY: 0.8, throttle: 0.7 });
    router.setDigital("pointer:1", "left", true);
    router.setDigital("pointer:2", "ascend", true);
    const snapshot = router.snapshot();
    assert.equal(snapshot.moveX, -1);
    assert.equal(snapshot.moveY, 0.8);
    assert.equal(snapshot.throttle, 0.7);
    assert.equal(snapshot.ascend, 1);
  });

  it("clears sources and one-shot requests atomically on mode changes", () => {
    const router = new PlayInputRouter();
    router.setDigital("keyboard", "primary", true);
    router.request("reset");
    assert.equal(router.consumeRequests().has("reset"), true);
    assert.equal(router.consumeRequests().size, 0);
    router.clearAll();
    assert.equal(router.activeSourceCount(), 0);
    assert.equal(router.snapshot().primary, false);
  });

  it("samples both sticks, triggers, shoulders and rising-edge requests", () => {
    const first = sampleStandardPlayGamepad(pad({
      axes: [0.5, -0.7, -0.4, 0.6],
      buttons: { 3: 1, 2: 1, 9: 1, 4: 1, 5: 1, 7: 0.8, 6: 0.2 },
    }));
    assert.ok(first.axes.moveX > 0);
    assert.ok(first.axes.moveY > 0);
    assert.ok(first.axes.lookX < 0);
    assert.ok(first.axes.lookY < 0);
    assert.equal(first.axes.throttle, 0.8);
    assert.equal(first.axes.brake, 0.2);
    assert.equal(first.digital.has("ascend"), true);
    assert.equal(first.digital.has("descend"), true);
    assert.deepEqual([...first.requests].sort(), ["assist", "camera", "reset"]);
    assert.equal(sampleStandardPlayGamepad(pad({ buttons: { 3: 1, 2: 1, 9: 1 } }), first.buttons).requests.size, 0);
  });

  it("maps one snapshot coherently into each deterministic controller", () => {
    const router = new PlayInputRouter();
    router.setAxes("gamepad", {
      moveX: 0.4, moveY: 0.7, lookX: -0.25, lookY: 0.6,
      throttle: 0.5, brake: 0.1,
    });
    router.setDigital("keyboard", "ascend", true);
    router.setDigital("keyboard", "primary", true);
    router.setDigital("keyboard", "secondary", true);
    const input = router.snapshot();
    assert.deepEqual(vehicleActionsFromPlay(input), {
      throttle: 0.7, brake: 0.1, steering: 0.4, handbrake: true,
    });
    assert.deepEqual(aircraftActionsFromPlay(input), {
      throttle: 1, yaw: 0.75, pitch: 0.7, roll: 0.4,
    });
    assert.deepEqual(crowActionsFromPlay(input), {
      forward: 0.7, right: 0, turn: 0.4, pitch: 0.6, climb: 1,
      sprint: false, glide: true,
    });
  });

  it("keeps camera-relative walking normalized and actor-local", () => {
    const input = { moveX: 1, moveY: 1 };
    const world = cameraRelativePlanar(input, { x: 0, z: -1 });
    assert.ok(Math.abs(Math.hypot(world.x, world.z) - 1) < 1e-12);
    const actor = groundActorActionsFromPlay(
      { moveX: 0, moveY: 1, lookX: 0, lookY: 0, throttle: 0, brake: 0,
        ascend: 0, descend: 0, primary: false, secondary: false },
      Math.PI / 2,
      { x: 1, z: 0 },
    );
    assert.ok(Math.abs(actor.forward + 1) < 1e-12);
    assert.ok(Math.abs(actor.right) < 1e-12);
  });
});
