import { beforeAll, describe, expect, it } from "vitest";
import { Vector3 } from "three";
import { PhysicsWorld } from "../engine/PhysicsWorld.js";
import { CharacterMover, DEFAULT_MOVER_CONFIG } from "./CharacterMover.js";
import { FIXED_DT } from "../engine/GameLoop.js";

/**
 * The mover replaces a Box3D feature that box3d-wasm does not bind, so its
 * behaviour is not covered by any upstream test. These run in Node against the
 * real wasm build — box3d-wasm is isomorphic, so this is the actual physics the
 * browser will run, not a stub.
 */

const PLAYER = "player";

async function makeWorld(): Promise<PhysicsWorld> {
  const physics = new PhysicsWorld();
  await physics.init({ x: 0, y: DEFAULT_MOVER_CONFIG.gravity, z: 0 });
  return physics;
}

function addFloor(physics: PhysicsWorld, id = "floor"): void {
  physics.createStatic({
    id,
    position: { x: 0, y: -0.15, z: 0 },
    colliders: [{ kind: "box", halfExtents: { x: 20, y: 0.15, z: 20 } }],
  });
}

function spawn(physics: PhysicsWorld, at: Vector3): CharacterMover {
  const height = DEFAULT_MOVER_CONFIG.cylinderHeight + DEFAULT_MOVER_CONFIG.radius * 2;
  physics.createKinematicCapsule({
    id: PLAYER,
    position: { x: at.x, y: at.y + height / 2, z: at.z },
    radius: DEFAULT_MOVER_CONFIG.radius,
    height: DEFAULT_MOVER_CONFIG.cylinderHeight,
  });
  return new CharacterMover(physics, PLAYER, at.clone());
}

/** Drive the mover for `seconds`, stepping physics exactly as the game loop does. */
function simulate(physics: PhysicsWorld, mover: CharacterMover, velocity: Vector3, seconds: number) {
  const ticks = Math.round(seconds / FIXED_DT);
  for (let i = 0; i < ticks; i++) {
    mover.update(velocity, FIXED_DT);
    physics.step(FIXED_DT);
  }
  return mover.state;
}

describe("CharacterMover", () => {
  beforeAll(() => {
    // Fail loudly rather than silently testing a half-initialised world.
    expect(DEFAULT_MOVER_CONFIG.stepHeight).toBeGreaterThan(0);
  });

  it("falls and settles on the floor", async () => {
    const physics = await makeWorld();
    addFloor(physics);
    const mover = spawn(physics, new Vector3(0, 3, 0));

    const state = simulate(physics, mover, new Vector3(0, 0, 0), 2);

    expect(state.grounded).toBe(true);
    expect(state.position.y).toBeCloseTo(0, 2);
    physics.dispose();
  });

  it("walks across open floor", async () => {
    const physics = await makeWorld();
    addFloor(physics);
    const mover = spawn(physics, new Vector3(0, 0, 0));

    simulate(physics, mover, new Vector3(0, 0, 0), 0.5); // settle
    const state = simulate(physics, mover, new Vector3(0, 0, -2), 1);

    expect(state.position.z).toBeLessThan(-1.5);
    expect(state.grounded).toBe(true);
    physics.dispose();
  });

  it("is stopped by a wall instead of passing through it", async () => {
    const physics = await makeWorld();
    addFloor(physics);
    physics.createStatic({
      id: "wall",
      position: { x: 0, y: 1.5, z: -3 },
      colliders: [{ kind: "box", halfExtents: { x: 5, y: 1.5, z: 0.15 } }],
    });
    const mover = spawn(physics, new Vector3(0, 0, 0));

    simulate(physics, mover, new Vector3(0, 0, 0), 0.5);
    const state = simulate(physics, mover, new Vector3(0, 0, -3), 3);

    // Stopped at the wall face (z = -2.85) plus the capsule radius.
    expect(state.position.z).toBeGreaterThan(-2.9);
    expect(state.position.z).toBeLessThan(-2.0);
    physics.dispose();
  });

  it("slides along a wall hit at an angle rather than sticking", async () => {
    const physics = await makeWorld();
    addFloor(physics);
    physics.createStatic({
      id: "wall",
      position: { x: 0, y: 1.5, z: -3 },
      colliders: [{ kind: "box", halfExtents: { x: 8, y: 1.5, z: 0.15 } }],
    });
    const mover = spawn(physics, new Vector3(0, 0, 0));

    simulate(physics, mover, new Vector3(0, 0, 0), 0.5);
    // Push diagonally into the wall; the -Z component is absorbed, +X survives.
    const state = simulate(physics, mover, new Vector3(2, 0, -2), 2);

    expect(state.position.x).toBeGreaterThan(1.5);
    physics.dispose();
  });

  it("steps up a ledge below step height", async () => {
    const physics = await makeWorld();
    addFloor(physics);
    physics.createStatic({
      id: "step",
      // 0.3 tall, under the 0.35 step height.
      position: { x: 0, y: 0.15, z: -3 },
      colliders: [{ kind: "box", halfExtents: { x: 4, y: 0.15, z: 2 } }],
    });
    const mover = spawn(physics, new Vector3(0, 0, 0));

    simulate(physics, mover, new Vector3(0, 0, 0), 0.5);
    const state = simulate(physics, mover, new Vector3(0, 0, -2), 2);

    expect(state.position.y).toBeCloseTo(0.3, 1);
    expect(state.position.z).toBeLessThan(-2);
    physics.dispose();
  });

  it("is blocked by a ledge above step height", async () => {
    const physics = await makeWorld();
    addFloor(physics);
    physics.createStatic({
      id: "block",
      // 1.2 tall, well over the step height.
      position: { x: 0, y: 0.6, z: -3 },
      colliders: [{ kind: "box", halfExtents: { x: 4, y: 0.6, z: 2 } }],
    });
    const mover = spawn(physics, new Vector3(0, 0, 0));

    simulate(physics, mover, new Vector3(0, 0, 0), 0.5);
    const state = simulate(physics, mover, new Vector3(0, 0, -2), 2);

    expect(state.position.y).toBeLessThan(0.1);
    expect(state.position.z).toBeGreaterThan(-1.5);
    physics.dispose();
  });

  it("walks up a hull ramp and gains height", async () => {
    const physics = await makeWorld();
    addFloor(physics);
    physics.createStatic({
      id: "ramp",
      position: { x: 0, y: 0, z: -3 },
      colliders: [
        {
          kind: "hull",
          points: [
            { x: -2, y: 0, z: 2 },
            { x: 2, y: 0, z: 2 },
            { x: -2, y: 0, z: -2 },
            { x: 2, y: 0, z: -2 },
            { x: -2, y: 1.4, z: -2 },
            { x: 2, y: 1.4, z: -2 },
          ],
        },
      ],
    });
    const mover = spawn(physics, new Vector3(0, 0, 0));

    simulate(physics, mover, new Vector3(0, 0, 0), 0.5);
    const state = simulate(physics, mover, new Vector3(0, 0, -1.5), 3);

    expect(state.position.y).toBeGreaterThan(0.5);
    expect(state.grounded).toBe(true);
    physics.dispose();
  });
});

describe("PhysicsWorld sensors", () => {
  it("reports the player entering and leaving a trigger volume", async () => {
    const physics = await makeWorld();
    addFloor(physics);
    physics.createSensor({
      id: "zone:test",
      position: { x: 0, y: 1, z: -3 },
      halfExtents: { x: 1.5, y: 1.5, z: 1.5 },
    });
    const mover = spawn(physics, new Vector3(0, 0, 0));

    let entered = false;
    let left = false;

    for (let i = 0; i < 400; i++) {
      mover.update(new Vector3(0, 0, -2), FIXED_DT);
      physics.step(FIXED_DT);
      const events = physics.drainSensorEvents();
      if (events.begin.some((e) => e.sensorId === "zone:test" && e.visitorId === PLAYER)) {
        entered = true;
      }
      if (entered && events.end.some((e) => e.sensorId === "zone:test")) {
        left = true;
        break;
      }
    }

    expect(entered).toBe(true);
    expect(left).toBe(true);
    physics.dispose();
  });
});
