import { describe, expect, it } from "vitest";
import { Vector3 } from "three";
import { validateClientMessage } from "@psx/shared";
import { PhysicsWorld } from "../engine/PhysicsWorld.js";
import { FIXED_DT } from "../engine/GameLoop.js";
import { DEFAULT_MOVER_CONFIG } from "../systems/CharacterMover.js";
import { HostAuthority } from "./HostAuthority.js";

/**
 * The host is the only thing standing between a modified client and the world
 * state, so these tests are about what a hostile guest *cannot* do.
 */

async function makeHost() {
  const physics = new PhysicsWorld();
  await physics.init({ x: 0, y: DEFAULT_MOVER_CONFIG.gravity, z: 0 });
  physics.createStatic({
    id: "floor",
    position: { x: 0, y: -0.15, z: 0 },
    colliders: [{ kind: "box", halfExtents: { x: 30, y: 0.15, z: 30 } }],
  });
  return { physics, authority: new HostAuthority(physics) };
}

/** Runs the host loop for `seconds`, exactly as `Game.fixedUpdate` does. */
function run(physics: PhysicsWorld, authority: HostAuthority, seconds: number) {
  for (let i = 0; i < Math.round(seconds / FIXED_DT); i++) {
    authority.update(FIXED_DT);
    physics.step(FIXED_DT);
  }
}

describe("HostAuthority", () => {
  it("simulates a guest from movement intent", async () => {
    const { physics, authority } = await makeHost();
    authority.addPlayer(1, new Vector3(0, 0, 0));

    authority.applyInput(1, { t: "input", tick: 1, move: { x: 0, z: -1 }, run: false, yaw: 0 });
    run(physics, authority, 1);

    const state = authority.getPlayer(1)!.mover.state;
    expect(state.position.z).toBeLessThan(-1);
    expect(state.grounded).toBe(true);
    physics.dispose();
  });

  it("collides a guest against walls it does not control", async () => {
    const { physics, authority } = await makeHost();
    physics.createStatic({
      id: "wall",
      position: { x: 0, y: 1.5, z: -3 },
      colliders: [{ kind: "box", halfExtents: { x: 10, y: 1.5, z: 0.15 } }],
    });
    authority.addPlayer(1, new Vector3(0, 0, 0));

    authority.applyInput(1, { t: "input", tick: 1, move: { x: 0, z: -1 }, run: true, yaw: 0 });
    run(physics, authority, 4);

    // The guest asked to keep walking; the host stopped it at the wall.
    expect(authority.getPlayer(1)!.mover.state.position.z).toBeGreaterThan(-2.9);
    physics.dispose();
  });

  it("gains no speed from a diagonal input claiming length greater than one", async () => {
    const { physics, authority } = await makeHost();
    authority.addPlayer(1, new Vector3(0, 0, 0));
    authority.addPlayer(2, new Vector3(10, 0, 0));

    // Peer 1 moves straight; peer 2 tries to out-run it diagonally.
    authority.applyInput(1, { t: "input", tick: 1, move: { x: 0, z: -1 }, run: false, yaw: 0 });
    authority.applyInput(2, { t: "input", tick: 1, move: { x: 1, z: -1 }, run: false, yaw: 0 });
    run(physics, authority, 1);

    const straight = authority.getPlayer(1)!.mover.state.position;
    const diagonal = authority.getPlayer(2)!.mover.state.position;

    const straightDistance = Math.hypot(straight.x, straight.z);
    const diagonalDistance = Math.hypot(diagonal.x - 10, diagonal.z);

    expect(diagonalDistance).toBeLessThanOrEqual(straightDistance + 0.05);
    physics.dispose();
  });

  it("clamps a hostile input vector rather than teleporting", async () => {
    const { physics, authority } = await makeHost();
    authority.addPlayer(1, new Vector3(0, 0, 0));

    // What a modified client would send to cross the map in one tick.
    const hostile = validateClientMessage({
      t: "input",
      tick: 1,
      move: { x: 0, z: -1e9 },
      run: true,
      yaw: 0,
    });
    expect(hostile.ok).toBe(true);
    authority.applyInput(1, hostile.ok ? (hostile.value as never) : (undefined as never));
    run(physics, authority, 1);

    // One second of running is a few metres, not a few million.
    expect(authority.getPlayer(1)!.mover.state.position.z).toBeGreaterThan(-10);
    physics.dispose();
  });

  it("discards inputs that arrive out of order", async () => {
    const { physics, authority } = await makeHost();
    authority.addPlayer(1, new Vector3(0, 0, 0));

    authority.applyInput(1, { t: "input", tick: 10, move: { x: 0, z: -1 }, run: false, yaw: 1 });
    // Stale frame reordered by the unreliable channel.
    authority.applyInput(1, { t: "input", tick: 4, move: { x: 1, z: 0 }, run: false, yaw: 99 });

    expect(authority.getPlayer(1)!.yaw).toBe(1);
    expect(authority.getPlayer(1)!.lastInputTick).toBe(10);
    physics.dispose();
  });

  it("coasts a silent peer to a stop", async () => {
    const { physics, authority } = await makeHost();
    authority.addPlayer(1, new Vector3(0, 0, 0));
    authority.applyInput(1, { t: "input", tick: 1, move: { x: 0, z: -1 }, run: true, yaw: 0 });

    run(physics, authority, 5); // well past the silence limit
    const settled = authority.getPlayer(1)!.mover.state.position.z;
    run(physics, authority, 2);

    expect(authority.getPlayer(1)!.mover.state.position.z).toBeCloseTo(settled, 1);
    physics.dispose();
  });

  it("emits snapshots at the advertised rate", async () => {
    const { physics, authority } = await makeHost();
    let emitted = 0;
    for (let i = 0; i < 60; i++) {
      if (authority.shouldSnapshot(FIXED_DT)) emitted++;
    }
    // 60 ticks is one second, so SNAPSHOT_HZ snapshots.
    expect(emitted).toBe(15);
    physics.dispose();
  });

  it("removes a departed peer's body from the world", async () => {
    const { physics, authority } = await makeHost();
    authority.addPlayer(1, new Vector3(0, 0, 0));
    expect(physics.get("player:1")).toBeDefined();

    authority.removePlayer(1);
    expect(physics.get("player:1")).toBeUndefined();
    expect(authority.getPlayer(1)).toBeUndefined();
    physics.dispose();
  });
});
