import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { createRobotActivity } from "../interiors/robotActivity.ts";
import { resolveRobotOperations } from "../interiors/robotOperations.ts";
import { createRobotRouter } from "../interiors/robotRoutes.ts";
import { Plan } from "../interiors/plan.ts";
import { LUMBRIDGE_HQ } from "../offices/lumbridge-hq.ts";
import { MATEO_COURT } from "../offices/mateo-court.ts";
import { LUMBRIDGE_HQ_ROBOT_OPERATIONS } from "../offices/operations/lumbridge-hq.ts";
import { MATEO_COURT_ROBOT_OPERATIONS } from "../offices/operations/mateo-court.ts";

const CASES = [
  [LUMBRIDGE_HQ, LUMBRIDGE_HQ_ROBOT_OPERATIONS],
  [MATEO_COURT, MATEO_COURT_ROBOT_OPERATIONS],
] as const;

describe("robot operations and routes", () => {
  it("resolves explicit Plan anchors and keeps every authored job on one routable floor", () => {
    for (const [office, definition] of CASES) {
      const plan = new Plan(office, { depth: "public", warn: false });
      const operations = resolveRobotOperations(plan, definition);
      const router = createRobotRouter(plan);
      for (const robot of operations.robots.values()) {
        let previous = operations.stations.get(robot.spawnStationId)!;
        for (const jobId of robot.schedule) {
          const job = operations.jobs.get(jobId)!;
          for (const stationId of job.stationIds) {
            const station = operations.stations.get(stationId)!;
            assert.equal(station.levelId, robot.levelId, `${robot.id}/${job.id}`);
            const route = router.route(robot.levelId, previous.position, station.position);
            assert.ok(route, `${robot.id}/${job.id}/${station.id}`);
            assert.ok(route.lengthM >= 0);
            previous = station;
          }
        }
      }
    }
  });

  it("rejects operations that imply real activity or invent a vertical route", () => {
    const plan = new Plan(LUMBRIDGE_HQ, { warn: false });
    assert.throws(
      () => resolveRobotOperations(plan, {
        ...LUMBRIDGE_HQ_ROBOT_OPERATIONS,
        disclosure: "Live operations",
      }),
      /simulated/,
    );
    const laPlan = new Plan(MATEO_COURT, { warn: false });
    assert.throws(
      () => resolveRobotOperations(laPlan, {
        ...MATEO_COURT_ROBOT_OPERATIONS,
        jobs: [{
          id: "bad-route",
          label: "Bad route",
          kind: "deliver",
          stationIds: ["la-l1-parcel-pickup", "la-l2-parcel-drop"],
          dwellTicks: 1,
        }],
        robots: [],
      }),
      /crosses levels/,
    );
  });
});

describe("robot activity determinism", () => {
  it("is cadence-independent at the fixed-step boundary and seed-stable", () => {
    const plan = new Plan(LUMBRIDGE_HQ, { warn: false });
    const operations = resolveRobotOperations(plan, LUMBRIDGE_HQ_ROBOT_OPERATIONS);
    const stepped = createRobotActivity(plan, operations, { seed: 73 });
    const ticked = createRobotActivity(plan, operations, { seed: 73 });
    for (let index = 0; index < 600; index += 1) stepped.step();
    for (let index = 0; index < 300; index += 1) ticked.tick(0.2);
    assert.deepEqual(ticked.snapshot(), stepped.snapshot());

    const different = createRobotActivity(plan, operations, { seed: 74 });
    assert.notDeepEqual(
      different.states().map((robot) => [robot.id, robot.scheduleCursor, robot.idleTicks]),
      stepped.trace().initial.robots.map((robot) => [robot.id, robot.scheduleCursor, robot.idleTicks]),
    );
  });

  it("restores the exact next transition and replays JSON-safe traces", () => {
    const plan = new Plan(MATEO_COURT, { warn: false });
    const operations = resolveRobotOperations(plan, MATEO_COURT_ROBOT_OPERATIONS);
    const activity = createRobotActivity(plan, operations, { seed: 0xdecafbad });
    for (let index = 0; index < 420; index += 1) activity.step();
    const checkpoint = activity.snapshot();
    const expected = activity.step();
    activity.restore(JSON.parse(JSON.stringify(checkpoint)));
    assert.deepEqual(activity.step(), expected);

    for (let index = 0; index < 60; index += 1) activity.step();
    const trace = JSON.parse(JSON.stringify(activity.trace()));
    assert.deepEqual(activity.replay(trace), trace.final);
  });

  it("rejects tampered snapshots before state mutation", () => {
    const plan = new Plan(LUMBRIDGE_HQ, { warn: false });
    const operations = resolveRobotOperations(plan, LUMBRIDGE_HQ_ROBOT_OPERATIONS);
    const activity = createRobotActivity(plan, operations, { seed: 4 });
    const before = activity.snapshot();
    const tampered = structuredClone(before);
    tampered.robots[0]!.battery = 9;
    assert.throws(() => activity.restore(tampered), /invalid/);
    assert.deepEqual(activity.snapshot(), before);
  });

  it("exposes deterministic blocked recovery for a controlled robot", () => {
    const plan = new Plan(LUMBRIDGE_HQ, { warn: false });
    const operations = resolveRobotOperations(plan, LUMBRIDGE_HQ_ROBOT_OPERATIONS);
    const id = "sf-studio-activity-01";
    const activity = createRobotActivity(plan, operations, {
      seed: 1,
      robotIds: [id],
      controlledRobotIds: [id],
    });
    let recovered = false;
    for (let index = 0; index < 80; index += 1) {
      const robot = activity.step({ [id]: { x: 0, z: 1, interact: false } }).robots[0]!;
      if (robot.mode === "blocked-recovery") recovered = true;
    }
    assert.equal(recovered, true);
  });
});

describe("robot activity soak", () => {
  it("runs thirty deterministic simulated minutes without a floor escape or terminal", () => {
    for (const [office, definition] of CASES) {
      const plan = new Plan(office, { warn: false });
      const operations = resolveRobotOperations(plan, definition);
      const activity = createRobotActivity(plan, operations, { seed: 0x5eed1234 });
      const seen = new Set<string>();
      for (let tick = 0; tick < 18_000; tick += 1) {
        for (const robot of activity.step().robots) {
          seen.add(robot.mode);
          assert.equal(robot.terminalReason, null, `${office.id}/${robot.id}/${tick}`);
          assert.ok(plan.roomAt(robot.levelId, robot.position), `${office.id}/${robot.id}/${tick}`);
          assert.equal(plan.blocked(robot.levelId, robot.position, robot.position, 0.28), false);
        }
      }
      assert.ok(seen.has("patrol"));
      assert.ok(seen.has("deliver"));
      assert.ok(seen.has("inspect"));
      assert.ok(seen.has("charge"));
      assert.ok(activity.states().every((robot) => robot.completedJobs > 20));
    }
  });
});
