import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  PoseInterpolationBuffer,
  isEntityPoseSnapshot,
  isInterestCell,
  isRealtimeSequence,
  isRealtimeTimestamp,
  validateClientRealtimeMessage,
  validateMotion,
  validateServerRealtimeMessage,
  type ActorPoseSnapshot,
  type InterestCell,
  type VehiclePoseSnapshot,
} from "../realtime/index.ts";

const localActor = (
  sequence: number,
  timestampMs: number,
  xM: number,
  headingDeg = 0,
): ActorPoseSnapshot => ({
  entity: "actor",
  actorId: "actor-karti",
  kind: "humanoid",
  sequence,
  timestampMs,
  pose: {
    space: "local",
    cell: { kind: "floor", officeId: "lumbridge-hq", floorId: "level-1" },
    xM,
    yM: 0,
    zM: 2,
    headingDeg,
    pitchDeg: 0,
  },
  velocity: { xMps: 10, yMps: 0, zMps: 0, yawDegPerSec: 10 },
});

const geographicVehicle = (
  sequence: number,
  timestampMs: number,
  lat: number,
): VehiclePoseSnapshot => ({
  entity: "vehicle",
  vehicleId: "model-x-hero",
  kind: "model-x",
  driverActorId: "actor-karti",
  sequence,
  timestampMs,
  pose: {
    space: "geographic",
    lat,
    lng: -120,
    altitudeM: 100,
    headingDeg: 0,
    pitchDeg: 0,
  },
  velocity: { xMps: 0, yMps: 0, zMps: 30, yawDegPerSec: 0 },
  steering: 0,
  wheelRadians: 2,
});

describe("realtime spatial protocol", () => {
  it("validates all hierarchical interest cell kinds", () => {
    const cells: InterestCell[] = [
      { kind: "california-tile", x: -4, y: 18, level: 6 },
      { kind: "city", cityId: "bay-area" },
      { kind: "office", officeId: "lumbridge-hq" },
      { kind: "floor", officeId: "lumbridge-hq", floorId: "level-1" },
      {
        kind: "room",
        officeId: "lumbridge-hq",
        floorId: "level-1",
        roomId: "commons",
      },
    ];
    assert.ok(cells.every(isInterestCell));
    assert.equal(isInterestCell({ kind: "city", cityId: "new-york" }), false);
    assert.equal(isInterestCell({ kind: "california-tile", x: 0.5, y: 2, level: 4 }), false);
  });

  it("bounds sequence and timestamps before accepting an envelope", () => {
    assert.equal(isRealtimeSequence(0), true);
    assert.equal(isRealtimeSequence(4_294_967_295), true);
    assert.equal(isRealtimeSequence(4_294_967_296), false);
    assert.equal(isRealtimeTimestamp(1_765_000_000_000), true);
    assert.equal(isRealtimeTimestamp(Number.MAX_SAFE_INTEGER), false);
  });

  it("accepts strict join/resume grants and membership revocation", () => {
    const joinRequest = {
      type: "join-request",
      protocolVersion: 1,
      requestId: "request-1",
      actorId: "actor-karti",
      resumeToken: null,
      lastReceivedSequence: null,
      interests: [{ kind: "city", cityId: "bay-area" }],
    };
    assert.equal(validateClientRealtimeMessage(joinRequest).ok, true);

    const grant = {
      type: "join-grant",
      protocolVersion: 1,
      requestId: "request-1",
      sessionId: "session-1",
      actorId: "actor-karti",
      role: "member",
      serverEpoch: "epoch-a",
      serverTimeMs: 1_765_000_000_000,
      nextSequence: 4,
      resumeToken: "rotating-secret",
      interests: joinRequest.interests,
      initial: [localActor(3, 1_765_000_000_000, 0)],
    };
    assert.equal(validateServerRealtimeMessage(grant).ok, true);
    assert.equal(validateClientRealtimeMessage({
      type: "resume-request",
      protocolVersion: 1,
      requestId: "request-2",
      sessionId: "session-1",
      resumeToken: "rotating-secret",
      serverEpoch: "epoch-a",
      lastReceivedSequence: 3,
      interests: joinRequest.interests,
    }).ok, true);
    assert.equal(validateServerRealtimeMessage({
      type: "membership-revoked",
      protocolVersion: 1,
      serverEpoch: "epoch-a",
      sequence: 5,
      timestampMs: 1_765_000_000_010,
      sessionId: "session-1",
      actorId: "actor-karti",
      reason: "membership-revoked",
      reconnectAllowed: false,
    }).ok, true);
  });

  it("rejects malformed, oversized, and non-finite server input", () => {
    const actor = localActor(1, 1000, 0);
    assert.equal(isEntityPoseSnapshot(actor), true);
    assert.equal(isEntityPoseSnapshot({ ...actor, pose: { ...actor.pose, xM: Number.NaN } }), false);
    assert.equal(validateServerRealtimeMessage({
      type: "pose-delta",
      protocolVersion: 1,
      serverEpoch: "epoch-a",
      sequence: 2,
      timestampMs: 1000,
      updates: [actor],
      removedEntityIds: Array.from({ length: 2_049 }, (_, index) => `actor-${index}`),
    }).ok, false);
    assert.equal(validateServerRealtimeMessage({ type: "pose-delta", protocolVersion: 2 }).ok, false);
  });
});

describe("authoritative motion validation", () => {
  const limits = {
    maximumHorizontalSpeedMps: 15,
    maximumVerticalSpeedMps: 8,
    maximumTurnRateDegPerSec: 180,
    positionSlackM: 0.25,
  };

  it("accepts plausible actor movement and reports measured rates", () => {
    const result = validateMotion(localActor(1, 1_000, 0), localActor(2, 2_000, 10), limits);
    assert.equal(result.ok, true);
    if (result.ok) {
      assert.equal(result.horizontalSpeedMps, 9.75);
      assert.equal(result.verticalSpeedMps, 0);
      assert.equal(result.turnRateDegPerSec, 0);
    }
  });

  it("rejects speed hacks, stale sequences, identity and interest-cell jumps", () => {
    assert.deepEqual(validateMotion(localActor(1, 1_000, 0), localActor(2, 1_100, 50), limits), {
      ok: false,
      reason: "speed-exceeded",
    });
    assert.deepEqual(validateMotion(localActor(2, 2_000, 0), localActor(2, 3_000, 1), limits), {
      ok: false,
      reason: "time-invalid",
    });
    const another = { ...localActor(2, 2_000, 1), actorId: "actor-other" };
    assert.deepEqual(validateMotion(localActor(1, 1_000, 0), another, limits), {
      ok: false,
      reason: "identity-mismatch",
    });
    const anotherFloor = localActor(2, 2_000, 1);
    if (anotherFloor.pose.space === "local" && anotherFloor.pose.cell.kind === "floor") {
      anotherFloor.pose.cell.floorId = "level-2";
    }
    assert.deepEqual(validateMotion(localActor(1, 1_000, 0), anotherFloor, limits), {
      ok: false,
      reason: "space-mismatch",
    });
  });

  it("uses real geographic distance for statewide vehicle checks", () => {
    const start = geographicVehicle(10, 10_000, 34);
    const plausible = geographicVehicle(11, 11_000, 34.0002);
    const result = validateMotion(start, plausible, {
      maximumHorizontalSpeedMps: 40,
      maximumVerticalSpeedMps: 5,
      maximumTurnRateDegPerSec: 90,
      positionSlackM: 1,
    });
    assert.equal(result.ok, true);
  });
});

describe("pose interpolation buffer", () => {
  it("interpolates deterministically at delayed server time", () => {
    const a = new PoseInterpolationBuffer({ interpolationDelayMs: 100 });
    const b = new PoseInterpolationBuffer({ interpolationDelayMs: 100 });
    for (const buffer of [a, b]) {
      assert.equal(buffer.push(localActor(1, 1_000, 0, 350)), true);
      assert.equal(buffer.push(localActor(2, 1_200, 2, 10)), true);
    }
    const first = a.sample(1_200);
    const second = b.sample(1_200);
    assert.deepEqual(first, second);
    assert.equal(first?.mode, "interpolated");
    assert.equal(first?.timestampMs, 1_100);
    assert.ok(first?.pose.space === "local");
    if (first?.pose.space === "local") {
      assert.equal(first.pose.xM, 1);
      assert.equal(first.pose.headingDeg, 360, "heading follows the short arc through north");
    }
  });

  it("caps extrapolation and then holds the capped predicted pose", () => {
    const buffer = new PoseInterpolationBuffer({
      interpolationDelayMs: 0,
      maximumExtrapolationMs: 150,
    });
    buffer.push(localActor(1, 1_000, 0));
    const predicted = buffer.sample(1_100);
    assert.equal(predicted?.mode, "extrapolated");
    assert.ok(predicted?.pose.space === "local");
    if (predicted?.pose.space === "local") assert.equal(predicted.pose.xM, 1);
    const capped = buffer.sample(10_000);
    assert.equal(capped?.mode, "held");
    assert.equal(capped?.timestampMs, 1_150);
    assert.ok(capped?.pose.space === "local");
    if (capped?.pose.space === "local") assert.equal(capped.pose.xM, 1.5);
  });

  it("rejects stale, duplicate, or mixed-entity input and caps capacity", () => {
    const buffer = new PoseInterpolationBuffer({ capacity: 2 });
    assert.equal(buffer.push(localActor(1, 1_000, 0)), true);
    assert.equal(buffer.push(localActor(1, 1_100, 1)), false);
    assert.equal(buffer.push(localActor(2, 1_200, 2)), true);
    assert.equal(buffer.push(localActor(3, 1_300, 3)), true);
    assert.equal(buffer.size(), 2);
    assert.equal(buffer.push({ ...localActor(4, 1_400, 4), actorId: "other" }), false);
    buffer.clear();
    assert.equal(buffer.size(), 0);
    assert.equal(buffer.sample(1_500), null);
  });
});
