import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { ActorController, type ActorControllerOptions } from "../actors/controller.ts";
import { AircraftController, type AircraftWaypoint } from "../aircraft/controller.ts";
import { Plan } from "../interiors/plan.ts";
import type { Level, Office, Room, Wall } from "../interiors/types.ts";
import { createWalker } from "../interiors/walker.ts";
import CALIFORNIA_TRANSPORT from "../transport/california.ts";
import { VehicleController } from "../transport/vehicleController.ts";
import { buildRoutePath, VehicleSimulation } from "../transport/vehicleSim.ts";

const SOAK_TIMEOUT_MS = 5_000;
const HOUR_AT_TEN_HZ = 36_000;

function finiteNumbers(value: unknown, path = "state"): void {
  if (typeof value === "number") {
    assert.ok(Number.isFinite(value), `${path} must remain finite`);
    return;
  }
  if (typeof value !== "object" || value === null) return;
  for (const [key, nested] of Object.entries(value)) finiteNumbers(nested, `${path}.${key}`);
}

function inside(value: number, minimum: number, maximum: number, label: string): void {
  assert.ok(value >= minimum && value <= maximum, `${label} ${value} outside [${minimum}, ${maximum}]`);
}

function countForwardWrap(previous: number, next: number): number {
  return previous > 0.9 && next < 0.1 ? 1 : 0;
}

describe("accelerated long-duration deterministic simulation", () => {
  it("runs both California routes and ambient traffic through repeated completions", { timeout: SOAK_TIMEOUT_MS }, () => {
    for (const routeId of ["la-sf-us-101", "la-sf-i-5"] as const) {
      const path = buildRoutePath(CALIFORNIA_TRANSPORT, routeId);
      const first = new VehicleSimulation(CALIFORNIA_TRANSPORT, { routeId, count: 24, seed: 115, timeScale: 900 });
      const replay = new VehicleSimulation(CALIFORNIA_TRANSPORT, { routeId, count: 24, seed: 115, timeScale: 900 });
      let wraps = 0;
      let prior = first.poses()[0]?.progress ?? 0;

      // 15 rendered minutes at the production 900x traffic scale is more than
      // nine simulated days, advanced in bounded 20 Hz fixed steps.
      for (let frame = 0; frame < 3_600; frame += 1) {
        first.tick(0.25);
        replay.tick(0.25);
        const hero = first.poses()[0];
        assert.ok(hero);
        wraps += countForwardWrap(prior, hero.progress);
        prior = hero.progress;
        if (frame % 60 !== 0) continue;
        for (const pose of first.poses()) {
          finiteNumbers(pose, `${routeId}.${pose.id}`);
          inside(pose.progress, 0, 1, `${pose.id}.progress`);
          inside(pose.distanceM, 0, path.lengthM, `${pose.id}.distanceM`);
          inside(pose.lat, 32, 43, `${pose.id}.lat`);
          inside(pose.lng, -125, -113, `${pose.id}.lng`);
        }
      }
      assert.ok(wraps >= 10, `${routeId} hero completed only ${wraps} circuits`);
      assert.deepEqual(first.poses(), replay.poses(), `${routeId} seeded traffic drifted on replay`);
    }
  });

  it("drives the playable vehicle for an accelerated hour without drift or escape", { timeout: SOAK_TIMEOUT_MS }, () => {
    const options = {
      routeId: "la-sf-us-101",
      fixedStepSeconds: 0.1,
      initialSpeedMps: 20,
      travelScale: 900,
      guardrailOffsetM: 5,
    } as const;
    const first = new VehicleController(CALIFORNIA_TRANSPORT, options);
    const replay = new VehicleController(CALIFORNIA_TRANSPORT, options);
    const path = buildRoutePath(CALIFORNIA_TRANSPORT, options.routeId);
    let prior = first.state().progress;
    let completions = 0;

    for (let step = 0; step < HOUR_AT_TEN_HZ; step += 1) {
      const actions = step % 1_200 < 60
        ? { throttle: 0.75, steering: Math.sin(step / 20) * 0.35 }
        : step % 1_200 === 60 ? { modeRequest: "assisted" as const } : undefined;
      first.stepFixed(actions);
      replay.stepFixed(actions);
      const state = first.state();
      completions += countForwardWrap(prior, state.progress);
      prior = state.progress;
      if (step % 300 !== 0) continue;
      finiteNumbers(state);
      inside(state.progress, 0, 1, "vehicle.progress");
      inside(state.distanceM, 0, path.lengthM, "vehicle.distanceM");
      inside(state.lateralOffsetM, -5, 5, "vehicle.lateralOffsetM");
      inside(state.speedMps, 0, 58, "vehicle.speedMps");
    }
    assert.ok(completions >= 20, `playable vehicle completed only ${completions} route circuits`);
    assert.ok(first.state().elapsedSteps === HOUR_AT_TEN_HZ);
    assert.deepEqual(first.snapshot(), replay.snapshot());
  });

  it("flies a crow for an accelerated hour inside strict 3D bounds", { timeout: SOAK_TIMEOUT_MS }, () => {
    const options: ActorControllerOptions = {
      kind: "crow",
      mode: "flight",
      identity: { id: "soak-crow", displayName: "Soak Crow", authenticated: false, profile: {} },
      position: { x: 0, y: 25, z: 0 },
      groundY: 0,
      minFlightAltitude: 5,
      maxFlightAltitude: 50,
      horizontalBounds: { minX: -200, maxX: 200, minZ: -200, maxZ: 200 },
      fixedStepSeconds: 0.1,
    };
    const first = new ActorController(options);
    const replay = new ActorController(options);
    const visited = new Set<string>();

    for (let step = 0; step < HOUR_AT_TEN_HZ; step += 1) {
      const phase = step % 1_800;
      const actions = {
        forward: 0.65 + 0.3 * Math.sin(step / 173),
        turn: phase < 900 ? 0.23 : -0.31,
        pitch: Math.sin(step / 257) * 0.7,
        climb: Math.sin(step / 401) * 0.8,
        glide: phase >= 1_500,
      };
      first.stepFixed(actions);
      replay.stepFixed(actions);
      const state = first.state();
      if (step % 120 !== 0) continue;
      finiteNumbers(state);
      inside(state.x, -200, 200, "crow.x");
      inside(state.z, -200, 200, "crow.z");
      inside(state.y, 5, 50, "crow.y");
      inside(state.posePhase, 0, Math.PI * 2, "crow.posePhase");
      visited.add(`${Math.round(state.x / 20)}:${Math.round(state.z / 20)}:${Math.round(state.y / 5)}`);
    }
    assert.ok(first.state().distanceM > 20_000,
      `crow accumulated only ${first.state().distanceM.toFixed(1)} m of flight distance`);
    assert.ok(visited.size > 25, `crow explored only ${visited.size} coarse cells`);
    assert.deepEqual(first.snapshot(), replay.snapshot());
  });

  it("walks against office walls for an accelerated hour without tunnelling or sticking", { timeout: SOAK_TIMEOUT_MS }, () => {
    const room: Room = {
      id: "soak-room", name: "Soak Room", floor: "floor" as never,
      outline: [{ x: 0, z: 0 }, { x: 30, z: 0 }, { x: 30, z: 20 }, { x: 0, z: 20 }],
    };
    const walls: Wall[] = [
      { id: "vertical", from: { x: 15, z: 0 }, to: { x: 15, z: 20 },
        openings: [{ kind: "door", start: 8.5, width: 3, sill: 0, head: 2.2 }] },
      { id: "horizontal", from: { x: 0, z: 10 }, to: { x: 12, z: 10 } },
    ];
    const level: Level = {
      id: "ground", name: "Ground", elevation: 0, wallHeight: 3, wallThickness: 0.12,
      floorplan: { rooms: [room], walls },
    };
    const plan = new Plan({ id: "soak-office", name: "Soak Office", levels: [level], viewpoints: [] } as Office,
      { warn: false });
    const create = () => createWalker(plan, {
      levelId: "ground", position: { x: 4, z: 4 }, radius: 0.3,
      speed: 2.4, fixedStep: 0.1, maxCatchUpSteps: 1,
    });
    const first = create();
    const replay = create();
    const directions = [
      { x: 1, z: 0.23 }, { x: 0.18, z: 1 }, { x: -1, z: -0.17 }, { x: -0.11, z: -1 },
    ] as const;
    const visited = new Set<string>();

    for (let step = 0; step < HOUR_AT_TEN_HZ; step += 1) {
      const action = directions[Math.floor(step / 450) % directions.length]!;
      const a = first.tick(0.1, action);
      replay.tick(0.1, action);
      if (step % 30 === 0) visited.add(`${Math.round(a.position.x)}:${Math.round(a.position.z)}`);
      if (step % 120 !== 0) continue;
      finiteNumbers(a);
      inside(a.position.x, 0.3, 29.7, "walker.x");
      inside(a.position.z, 0.3, 19.7, "walker.z");
      assert.equal(plan.blocked("ground", a.position, a.position, 0.3), false, "walker entered collision geometry");
    }
    assert.ok(first.state().distance > 200,
      `walker accumulated only ${first.state().distance.toFixed(1)} m of distance`);
    assert.ok(visited.size > 20, `walker visited only ${visited.size} cells`);
    assert.deepEqual(first.state(), replay.state());
  });

  it("circuits an assisted aircraft route for an accelerated hour within its envelope", { timeout: SOAK_TIMEOUT_MS }, () => {
    const route: readonly AircraftWaypoint[] = [
      { id: "west", lat: 34.05, lng: -118.28, altitudeM: 1_200 },
      { id: "north", lat: 34.13, lng: -118.20, altitudeM: 1_650 },
      { id: "east", lat: 34.05, lng: -118.12, altitudeM: 1_350 },
    ];
    const options = {
      route,
      initialPosition: route[0],
      initialAltitudeM: 1_200,
      initialHeadingDeg: 45,
      initialSpeedMps: 55,
      fixedStepSeconds: 0.1,
    } as const;
    const first = new AircraftController(options);
    const replay = new AircraftController(options);
    let priorIndex = first.state().routeWaypointIndex;
    let waypointTransitions = 0;
    let routeCompletions = 0;

    for (let step = 0; step < HOUR_AT_TEN_HZ; step += 1) {
      first.stepFixed();
      replay.stepFixed();
      const state = first.state();
      if (state.routeWaypointIndex !== priorIndex) {
        waypointTransitions += 1;
        if (priorIndex === route.length - 1 && state.routeWaypointIndex === 0) routeCompletions += 1;
        priorIndex = state.routeWaypointIndex;
      }
      if (step % 120 !== 0) continue;
      finiteNumbers(state);
      inside(state.lat, 32.4, 42.1, "aircraft.lat");
      inside(state.lng, -124.6, -114, "aircraft.lng");
      inside(state.altitudeM, 75, 6_000, "aircraft.altitudeM");
      inside(state.speedMps, 20, 95, "aircraft.speedMps");
      inside(state.headingDeg, 0, 360, "aircraft.headingDeg");
    }
    assert.ok(waypointTransitions >= 6, `aircraft made only ${waypointTransitions} waypoint transitions`);
    assert.ok(routeCompletions >= 2, `aircraft completed only ${routeCompletions} circuits`);
    assert.equal(first.state().elapsedSteps, HOUR_AT_TEN_HZ);
    assert.deepEqual(first.snapshot(), replay.snapshot());
  });
});
