import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { Plan } from "../interiors/plan.ts";
import {
  createWalker,
  normalizeWalkerAction,
  type WalkerOptions,
} from "../interiors/walker.ts";
import type { Level, Office, Room, Wall } from "../interiors/types.ts";

const ROOM: Room = {
  id: "floor",
  name: "Floor",
  floor: "floor" as never,
  outline: [
    { x: 0, z: 0 },
    { x: 10, z: 0 },
    { x: 10, z: 8 },
    { x: 0, z: 8 },
  ],
};

function planWith(walls: Wall[] = []): Plan {
  const level: Level = {
    id: "ground",
    name: "Ground",
    elevation: 0,
    wallHeight: 3,
    wallThickness: 0.1,
    floorplan: { rooms: [ROOM], walls },
  };
  const office: Office = { id: "walk-test", name: "Walk Test", levels: [level], viewpoints: [] };
  return new Plan(office, { warn: false });
}

function walker(plan: Plan, over: Partial<WalkerOptions> = {}) {
  return createWalker(plan, {
    levelId: "ground",
    position: { x: 2, z: 2 },
    speed: 1,
    fixedStep: 0.1,
    ...over,
  });
}

describe("walker input and clock", () => {
  it("normalizes planar actions without amplifying smaller input", () => {
    assert.deepEqual(normalizeWalkerAction({ x: 0.3, z: -0.4 }), { x: 0.3, z: -0.4 });
    const diagonal = normalizeWalkerAction({ x: 1, z: 1 });
    assert.ok(Math.abs(Math.hypot(diagonal.x, diagonal.z) - 1) < 1e-12);
    assert.deepEqual(normalizeWalkerAction({ x: Number.NaN, z: 1 }), { x: 0, z: 0 });
  });

  it("moves only in fixed steps and is deterministic across frame chunking", () => {
    const plan = planWith();
    const one = walker(plan);
    const many = walker(plan);
    one.tick(0.05, { x: 1, z: 0 });
    assert.deepEqual(one.state().position, { x: 2, z: 2 });
    one.tick(0.35, { x: 1, z: 0 });
    for (let index = 0; index < 4; index += 1) many.tick(0.1, { x: 1, z: 0 });
    assert.deepEqual(one.state(), many.state());
    assert.ok(Math.abs(one.state().distance - 0.4) < 1e-12);
  });

  it("returns defensive state snapshots", () => {
    const controller = walker(planWith());
    const leaked = controller.state();
    leaked.position.x = Number.NaN;
    assert.deepEqual(controller.state().position, { x: 2, z: 2 });
  });

  it("uses and restores a normalized authored arrival facing", () => {
    const controller = walker(planWith(), { facing: { x: 3, z: 4 } });
    assert.deepEqual(controller.state().facing, { x: 0.6, z: 0.8 });
    controller.tick(0.1, { x: -1, z: 0 });
    assert.deepEqual(controller.state().facing, { x: -1, z: 0 });
    controller.reset();
    assert.deepEqual(controller.state().facing, { x: 0.6, z: 0.8 });
  });
});

describe("walker collision", () => {
  it("sweeps its circular footprint and cannot tunnel through a wall", () => {
    const plan = planWith([{ id: "divider", from: { x: 5, z: 0 }, to: { x: 5, z: 8 } }]);
    const controller = walker(plan, { speed: 100, fixedStep: 0.1 });
    const state = controller.tick(0.1, { x: 1, z: 0 });
    assert.ok(state.position.x < 4.65 && state.position.x > 4.64, `${state.position.x}`);
    assert.equal(plan.blocked("ground", state.position, state.position, 0.3), false);
  });

  it("slides the unblocked component of diagonal movement along a wall", () => {
    const plan = planWith([{ id: "divider", from: { x: 5, z: 0 }, to: { x: 5, z: 8 } }]);
    const controller = walker(plan, { position: { x: 4.6, z: 2 }, speed: 2 });
    for (let index = 0; index < 10; index += 1) controller.tick(0.1, { x: 1, z: 1 });
    const state = controller.state();
    assert.ok(state.position.x < 4.65, `${state.position.x}`);
    assert.ok(state.position.z > 3, `${state.position.z}`);
    assert.equal(plan.blocked("ground", state.position, state.position, 0.3), false);
  });

  it("walks through a resolved door gap without a door-specific exception", () => {
    const plan = planWith([
      {
        id: "divider",
        from: { x: 5, z: 0 },
        to: { x: 5, z: 8 },
        openings: [{ kind: "door", start: 3.4, width: 1.2, sill: 0, head: 2.1 }],
      },
    ]);
    const controller = walker(plan, { position: { x: 4, z: 4 }, speed: 2 });
    for (let index = 0; index < 10; index += 1) controller.tick(0.1, { x: 1, z: 0 });
    assert.ok(controller.state().position.x > 5.5, `${controller.state().position.x}`);
  });
});

describe("walker guardrails", () => {
  it("stays within finite level bounds and ignores invalid time/input", () => {
    const controller = walker(planWith(), { position: { x: 9.6, z: 4 }, speed: 10 });
    controller.tick(Number.NaN, { x: 1, z: 0 });
    controller.tick(1, { x: Number.POSITIVE_INFINITY, z: 0 });
    assert.deepEqual(controller.state().position, { x: 9.6, z: 4 });
    controller.tick(1, { x: 1, z: 0 });
    const state = controller.state();
    assert.equal(state.position.x, 9.7);
    assert.ok(Number.isFinite(state.position.x) && Number.isFinite(state.distance));
  });

  it("resets atomically and rejects invalid spawns/configuration", () => {
    const plan = planWith();
    const controller = walker(plan);
    controller.tick(0.2, { x: 1, z: 0 });
    assert.deepEqual(controller.reset({ levelId: "ground", position: { x: 7, z: 6 } }).position, { x: 7, z: 6 });
    assert.equal(controller.state().distance, 0);
    assert.throws(() => controller.reset({ levelId: "ground", position: { x: Number.NaN, z: 1 } }), RangeError);
    assert.deepEqual(controller.state().position, { x: 7, z: 6 });
    assert.throws(() => walker(plan, { radius: 0 }), RangeError);
    assert.throws(() => walker(plan, { position: { x: 11, z: 2 } }), RangeError);
  });
});

// ---- Cross-level -----------------------------------------------------------

/**
 * Two storeys, hand-authored, with different footprints on purpose.
 *
 * Level 2 is deliberately *smaller* than level 1 and offset from it, so "valid
 * on the level you are on" is a different question from "valid anywhere in the
 * building" and a test can tell the two apart. Nothing here is a stair: the
 * controller knows nothing about transitions, only about being told to be
 * somewhere else.
 */
function twoLevelPlan(): Plan {
  const lower: Level = {
    id: "level-1",
    name: "Ground",
    elevation: 0,
    wallHeight: 3,
    wallThickness: 0.1,
    floorplan: { rooms: [ROOM], walls: [] },
  };
  const upper: Level = {
    id: "level-2",
    name: "Upper",
    elevation: 4,
    wallHeight: 3,
    wallThickness: 0.1,
    floorplan: {
      rooms: [{
        id: "loft",
        name: "Loft",
        floor: "floor" as never,
        outline: [
          { x: 0, z: 0 },
          { x: 6, z: 0 },
          { x: 6, z: 8 },
          { x: 0, z: 8 },
        ],
      }],
      walls: [],
    },
  };
  const office: Office = {
    id: "two-level-test",
    name: "Two Level Test",
    levels: [lower, upper],
    viewpoints: [],
  };
  return new Plan(office, { warn: false });
}

describe("walker levels", () => {
  it("crosses to another storey without zeroing the odometer", () => {
    const plan = twoLevelPlan();
    const controller = walker(plan, { levelId: "level-1", position: { x: 2, z: 2 }, speed: 1 });

    for (let index = 0; index < 10; index += 1) controller.tick(0.1, { x: 1, z: 0 });
    const before = controller.state();
    assert.equal(before.levelId, "level-1");
    assert.ok(Math.abs(before.distance - 1) < 1e-9, `${before.distance}`);

    // The transition path. `reset` would have been the easy way to write this
    // and it is the wrong one: it zeroes `distance` and restores the authored
    // facing, so every flight of stairs would silently reset the odometer.
    const crossed = controller.enterLevel("level-2", { x: 3, z: 2 });
    assert.equal(crossed.levelId, "level-2");
    assert.deepEqual(crossed.position, { x: 3, z: 2 });
    assert.equal(crossed.distance, before.distance);
    assert.deepEqual(crossed.facing, before.facing);

    for (let index = 0; index < 10; index += 1) controller.tick(0.1, { x: 1, z: 0 });
    const after = controller.state();
    assert.equal(after.levelId, "level-2");
    assert.ok(after.distance > before.distance, `${after.distance}`);
    assert.ok(Math.abs(after.distance - 2) < 1e-9, `${after.distance}`);

    // And the upper storey's own bounds hold: level 2 stops at x = 6 where
    // level 1 runs to 10, so walking east on the loft stops sooner.
    for (let index = 0; index < 60; index += 1) controller.tick(0.1, { x: 1, z: 0 });
    assert.ok(controller.state().position.x <= 6 - 0.3 + 1e-9, `${controller.state().position.x}`);
  });

  it("refuses a crossing to an unknown level or an invalid position on a known one", () => {
    const controller = walker(twoLevelPlan(), { levelId: "level-1", position: { x: 2, z: 2 } });
    assert.throws(() => controller.enterLevel("level-3", { x: 2, z: 2 }), RangeError);
    // Inside level 1 and outside level 2: the check is per level, not per building.
    assert.throws(() => controller.enterLevel("level-2", { x: 8, z: 2 }), RangeError);
    assert.throws(() => controller.enterLevel("level-2", { x: Number.NaN, z: 2 }), RangeError);
    assert.equal(controller.state().levelId, "level-1");
    assert.deepEqual(controller.state().position, { x: 2, z: 2 });
  });

  it("restores a snapshot from another storey, and still refuses an invalid one", () => {
    const plan = twoLevelPlan();
    const controller = walker(plan, { levelId: "level-1", position: { x: 2, z: 2 } });

    // The rule is "a level this plan resolves, at a position valid on it" — not
    // "the level you spawned on". `src/arena/officeNav.ts` restores through this.
    const restored = controller.restore({
      levelId: "level-2",
      position: { x: 4, z: 5 },
      facing: { x: 0, z: 1 },
      distance: 12.5,
    });
    assert.equal(restored.levelId, "level-2");
    assert.deepEqual(restored.position, { x: 4, z: 5 });
    assert.equal(restored.distance, 12.5);

    // Valid on level 1, outside level 2. Relaxing the level check must not have
    // relaxed the position check with it.
    assert.throws(() => controller.restore({
      levelId: "level-2",
      position: { x: 8, z: 5 },
      facing: { x: 0, z: 1 },
      distance: 1,
    }), RangeError);
    assert.throws(() => controller.restore({
      levelId: "level-9",
      position: { x: 4, z: 5 },
      facing: { x: 0, z: 1 },
      distance: 1,
    }), RangeError);
    assert.throws(() => controller.restore({
      levelId: "level-2",
      position: { x: 4, z: 5 },
      facing: { x: 0, z: 1 },
      distance: -1,
    }), RangeError);
    assert.equal(controller.state().levelId, "level-2");

    // `reset()` still returns to the configured spawn, which is on level 1 —
    // a crossing changes where you are, never where the episode began.
    assert.equal(controller.reset().levelId, "level-1");
  });
});
