import assert from "node:assert/strict";
import { createHmac } from "node:crypto";
import { after, describe, it } from "node:test";
import type {
  ActorPoseSnapshot,
  AircraftPoseSnapshot,
  FloorInterest,
  InterestCell,
  PoseDelta,
  VehiclePoseSnapshot,
} from "../../../src/realtime/types.ts";
import { buildApp } from "../app.ts";
import { loadConfig } from "../config.ts";
import { createRealtimeService } from "../realtime/index.ts";
import { BUNDLED_REALTIME_OFFICE_IDS } from "../routes/realtime.ts";

const FLOOR: FloorInterest = {
  kind: "floor",
  officeId: "lumbridge-hq",
  floorId: "level-1",
};
const OTHER_FLOOR: FloorInterest = {
  kind: "floor",
  officeId: "mateo-court",
  floorId: "level-1",
};

function actorPose(
  actorId: string,
  sequence: number,
  timestampMs: number,
  xM: number,
  cell: InterestCell = FLOOR,
): ActorPoseSnapshot {
  if (cell.kind === "california-tile") throw new Error("fixture needs local interest");
  return {
    entity: "actor",
    actorId,
    kind: "humanoid",
    sequence,
    timestampMs,
    pose: {
      space: "local",
      cell,
      xM,
      yM: 0,
      zM: 0,
      headingDeg: 0,
      pitchDeg: 0,
    },
    velocity: { xMps: 0, yMps: 0, zMps: 0, yawDegPerSec: 0 },
  };
}

function geographicActor(
  actorId: string,
  sequence: number,
  timestampMs: number,
  lat = 37.7,
): ActorPoseSnapshot {
  return {
    entity: "actor",
    actorId,
    kind: "humanoid",
    sequence,
    timestampMs,
    pose: {
      space: "geographic",
      lat,
      lng: -122.4,
      altitudeM: 10,
      headingDeg: 0,
      pitchDeg: 0,
    },
    velocity: { xMps: 0, yMps: 0, zMps: 0, yawDegPerSec: 0 },
  };
}

function vehiclePose(
  driverActorId: string,
  sequence: number,
  timestampMs: number,
): VehiclePoseSnapshot {
  return {
    entity: "vehicle",
    vehicleId: `vehicle-${driverActorId}`,
    kind: "model-x",
    driverActorId,
    sequence,
    timestampMs,
    pose: {
      space: "local",
      cell: FLOOR,
      xM: 0,
      yM: 0,
      zM: 0,
      headingDeg: 0,
      pitchDeg: 0,
    },
    velocity: { xMps: 0, yMps: 0, zMps: 0, yawDegPerSec: 0 },
    steering: 0,
    wheelRadians: 0,
  };
}

function aircraftPose(
  pilotActorId: string,
  sequence: number,
  timestampMs: number,
  lat = 37.7,
): AircraftPoseSnapshot {
  return {
    entity: "aircraft",
    aircraftId: `aircraft-${pilotActorId}`,
    kind: "electric-vtail",
    pilotActorId,
    sequence,
    timestampMs,
    pose: {
      space: "geographic",
      lat,
      lng: -122.4,
      altitudeM: 1_500,
      headingDeg: 320,
      pitchDeg: 2,
    },
    velocity: { xMps: -40, yMps: 2, zMps: 48, yawDegPerSec: 2 },
    rollDeg: -12,
    throttle: 0.7,
    rollInput: -0.25,
    pitchInput: 0.1,
    yawInput: 0,
    fanRadians: 3,
  };
}

function joined(
  service: ReturnType<typeof createRealtimeService>,
  actorId: string,
  interests: readonly InterestCell[] = [FLOOR],
  subject = `auth-subject-for-${actorId}`,
) {
  const result = service.join({
    requestId: `request-${actorId}`,
    actorId,
    subject,
    role: "member",
    interests,
  });
  assert.equal(result.ok, true);
  if (!result.ok) throw new Error("realtime fixture could not join");
  return result.value;
}

describe("authoritative realtime service", () => {
  it("stores only hashed credentials and authenticates opaque sessions", () => {
    const service = createRealtimeService();
    const session = joined(service, "member-a");
    assert.notEqual(session.sessionId, session.token);
    assert.equal(service.authenticate(session.sessionId, session.token).ok, true);
    assert.equal(service.authenticate(session.sessionId, `${session.token}x`).ok, false);
    assert.equal(session.grant.resumeToken, session.token);
    assert.deepEqual(session.grant.initial, []);
  });

  it("keeps auth subjects private while reserving each opaque actor id", () => {
    const service = createRealtimeService();
    const first = service.join({
      requestId: "request",
      actorId: "page-f4f9445d",
      subject: "private-auth-subject",
      role: "member",
      interests: [FLOOR],
    });
    assert.equal(first.ok, true);
    if (!first.ok) return;
    assert.equal(JSON.stringify(first.value.grant).includes("private-auth-subject"), false);
    const view = service.authenticate(first.value.sessionId, first.value.token);
    assert.equal(view.ok, true);
    if (view.ok) assert.equal(JSON.stringify(view.value).includes("private-auth-subject"), false);

    const collision = service.join({
      requestId: "request-2",
      actorId: "page-f4f9445d",
      subject: "another-private-subject",
      role: "member",
      interests: [FLOOR],
    });
    assert.deepEqual(collision, {
      ok: false,
      code: "conflict",
      message: "Realtime actor id is already active.",
    });
  });

  it("broadcasts poses only to listeners sharing the exact interest cell", () => {
    let now = 10_000;
    const service = createRealtimeService({ now: () => now });
    const a = joined(service, "member-a");
    const b = joined(service, "member-b");
    const c = joined(service, "member-c", [OTHER_FLOOR]);
    const receivedA: unknown[] = [];
    const receivedB: unknown[] = [];
    const receivedC: unknown[] = [];
    service.connect(a.sessionId, a.token, (message) => receivedA.push(message));
    service.connect(b.sessionId, b.token, (message) => receivedB.push(message));
    service.connect(c.sessionId, c.token, (message) => receivedC.push(message));
    const submitted = service.submitPose(
      a.sessionId,
      a.token,
      actorPose("member-a", 1, now, 0),
    );
    assert.equal(submitted.ok, true);
    assert.equal(receivedA.length, 1);
    assert.equal(receivedB.length, 1);
    assert.equal(receivedC.length, 0);
    now += 1_000;
    const second = service.submitPose(a.sessionId, a.token, actorPose("member-a", 2, now, 5));
    assert.equal(second.ok, true);
    const late = joined(service, "member-late");
    assert.equal(late.grant.initial.length, 1, "a late join receives the current cell snapshot");
  });

  it("allows geographic city poses and scopes them to the exact detailed city", () => {
    const now = 15_000;
    const service = createRealtimeService({ now: () => now });
    const bay = { kind: "city" as const, cityId: "bay-area" as const };
    const socal = { kind: "city" as const, cityId: "socal" as const };
    const source = joined(service, "page-bay-source", [bay]);
    const bayPeer = joined(service, "page-bay-peer", [bay]);
    const socalPeer = joined(service, "page-socal-peer", [socal]);
    const bayMessages: unknown[] = [];
    const socalMessages: unknown[] = [];
    service.connect(bayPeer.sessionId, bayPeer.token, (message) => bayMessages.push(message));
    service.connect(socalPeer.sessionId, socalPeer.token, (message) => socalMessages.push(message));
    const sent = service.submitPose(
      source.sessionId,
      source.token,
      geographicActor("page-bay-source", 1, now),
    );
    assert.equal(sent.ok, true);
    assert.equal(bayMessages.length, 1);
    assert.equal(socalMessages.length, 0);
  });

  it("converges two clients on an owned aircraft and rejects authority, space, and speed attacks", () => {
    let now = 16_000;
    const service = createRealtimeService({ now: () => now });
    const bay = { kind: "city" as const, cityId: "bay-area" as const };
    const pilot = joined(service, "opaque-pilot", [bay], "private-pilot-subject");
    const observer = joined(service, "opaque-observer", [bay], "private-observer-subject");
    const received: PoseDelta[] = [];
    service.connect(observer.sessionId, observer.token, (message) => {
      if (message.type === "pose-delta") received.push(message);
    });

    const first = service.submitPose(
      pilot.sessionId,
      pilot.token,
      aircraftPose("opaque-pilot", 1, now),
    );
    assert.equal(first.ok, true);
    assert.equal(received.length, 1);
    assert.equal(received[0]?.updates[0]?.entity, "aircraft");
    assert.equal(JSON.stringify(received[0]).includes("private-pilot-subject"), false);

    const stolen = service.submitPose(
      observer.sessionId,
      observer.token,
      aircraftPose("opaque-pilot", 1, now),
    );
    assert.equal(stolen.ok, false);
    if (!stolen.ok) assert.equal(stolen.code, "ownership");

    const local = aircraftPose("opaque-pilot", 2, now);
    const invalidSpace = service.submitPose(pilot.sessionId, pilot.token, {
      ...local,
      pose: { space: "local", cell: bay, xM: 0, yM: 0, zM: 0, headingDeg: 0, pitchDeg: 0 },
    });
    assert.equal(invalidSpace.ok, false);
    if (!invalidSpace.ok) assert.equal(invalidSpace.code, "interest");

    now += 1_000;
    assert.equal(service.submitPose(
      pilot.sessionId,
      pilot.token,
      aircraftPose("opaque-pilot", 2, now, 37.7005),
    ).ok, true);
    now += 100;
    const speedHack = service.submitPose(
      pilot.sessionId,
      pilot.token,
      aircraftPose("opaque-pilot", 3, now, 38.7),
    );
    assert.equal(speedHack.ok, false);
    if (!speedHack.ok) assert.equal(speedHack.code, "motion");

    const lateObserver = joined(service, "opaque-late-observer", [bay]);
    assert.equal(lateObserver.grant.initial.length, 1);
    assert.equal(lateObserver.grant.initial[0]?.entity, "aircraft");
  });

  it("removes the previous entity when one session switches actor to vehicle", () => {
    let now = 17_000;
    const service = createRealtimeService({ now: () => now });
    const source = joined(service, "page-switch");
    const peer = joined(service, "page-peer");
    const received: PoseDelta[] = [];
    service.connect(peer.sessionId, peer.token, (message) => {
      if (message.type === "pose-delta") received.push(message);
    });
    service.submitPose(source.sessionId, source.token, actorPose("page-switch", 1, now, 0));
    now += 100;
    const switched = service.submitPose(
      source.sessionId,
      source.token,
      vehiclePose("page-switch", 2, now),
    );
    assert.equal(switched.ok, true);
    if (!switched.ok) return;
    assert.deepEqual(switched.value.removedEntityIds, ["actor:page-switch"]);
    assert.equal(switched.value.updates[0]?.entity, "vehicle");
    assert.ok(switched.value.sequence > (received[0]?.sequence ?? -1));
    assert.deepEqual(received.at(-1)?.removedEntityIds, ["actor:page-switch"]);
  });

  it("broadcasts removals to old-cell peers on interest move and leave", () => {
    let now = 18_000;
    const service = createRealtimeService({ now: () => now });
    const source = joined(service, "page-moving");
    const oldPeer = joined(service, "page-old-peer");
    const received: PoseDelta[] = [];
    service.connect(oldPeer.sessionId, oldPeer.token, (message) => {
      if (message.type === "pose-delta") received.push(message);
    });
    service.submitPose(source.sessionId, source.token, actorPose("page-moving", 1, now, 0));
    const moved = service.resume({
      type: "resume-request",
      protocolVersion: 1,
      requestId: "move-request",
      sessionId: source.sessionId,
      resumeToken: source.token,
      serverEpoch: source.grant.serverEpoch,
      lastReceivedSequence: received.at(-1)?.sequence ?? 0,
      interests: [OTHER_FLOOR],
    }, "auth-subject-for-page-moving");
    assert.equal(moved.ok, true);
    assert.deepEqual(received.at(-1)?.removedEntityIds, ["actor:page-moving"]);

    const leaving = joined(service, "page-leaving");
    now += 100;
    service.submitPose(leaving.sessionId, leaving.token, actorPose("page-leaving", 1, now, 0));
    const beforeLeave = received.at(-1)?.sequence ?? -1;
    assert.equal(service.leave(leaving.sessionId, leaving.token).ok, true);
    assert.deepEqual(received.at(-1)?.removedEntityIds, ["actor:page-leaving"]);
    assert.ok((received.at(-1)?.sequence ?? -1) > beforeLeave);
  });

  it("rejects stale, unowned, off-interest and impossible motion", () => {
    let now = 20_000;
    const service = createRealtimeService({ now: () => now });
    const session = joined(service, "member-a");
    assert.equal(service.submitPose(
      session.sessionId,
      session.token,
      actorPose("member-b", 1, now, 0),
    ).ok, false);
    assert.equal(service.submitPose(
      session.sessionId,
      session.token,
      actorPose("member-a", 1, now, 0, OTHER_FLOOR),
    ).ok, false);
    assert.equal(service.submitPose(
      session.sessionId,
      session.token,
      actorPose("member-a", 1, now, 0),
    ).ok, true);
    assert.equal(service.submitPose(
      session.sessionId,
      session.token,
      actorPose("member-a", 1, now, 1),
    ).ok, false);
    now += 100;
    const hacked = service.submitPose(
      session.sessionId,
      session.token,
      actorPose("member-a", 2, now, 1_000),
    );
    assert.equal(hacked.ok, false);
    if (!hacked.ok) assert.equal(hacked.code, "motion");
  });

  it("caps session/cell occupancy and pose rate", () => {
    let now = 30_000;
    const service = createRealtimeService({
      now: () => now,
      maximumSessionsPerCell: 1,
      maximumPoseUpdatesPerWindow: 1,
    });
    const session = joined(service, "member-a");
    const refused = service.join({
      requestId: "request-b",
      actorId: "member-b",
      subject: "member-b",
      role: "member",
      interests: [FLOOR],
    });
    assert.equal(refused.ok, false);
    assert.equal(service.submitPose(
      session.sessionId,
      session.token,
      actorPose("member-a", 1, now, 0),
    ).ok, true);
    now += 50;
    const limited = service.submitPose(
      session.sessionId,
      session.token,
      actorPose("member-a", 2, now, 0),
    );
    assert.equal(limited.ok, false);
    if (!limited.ok) assert.equal(limited.code, "rate-limited");
  });

  it("honours disconnect grace, absolute TTL, leave and membership revocation", () => {
    let now = 40_000;
    const service = createRealtimeService({
      now: () => now,
      disconnectGraceMs: 500,
      sessionTtlMs: 2_000,
    });
    const session = joined(service, "member-a");
    const messages: unknown[] = [];
    const connected = service.connect(session.sessionId, session.token, (message) => messages.push(message));
    assert.equal(connected.ok, true);
    if (!connected.ok) return;
    connected.value.disconnect();
    now += 400;
    assert.equal(service.cleanup(), 0);
    assert.equal(service.authenticate(session.sessionId, session.token).ok, true);
    now += 600;
    assert.equal(service.cleanup(), 1);

    const revoked = joined(service, "member-b");
    service.connect(revoked.sessionId, revoked.token, (message) => messages.push(message));
    assert.equal(service.revokeSubject("auth-subject-for-member-b"), 1);
    assert.equal(messages.length, 1);
    assert.equal(service.authenticate(revoked.sessionId, revoked.token).ok, false);

    const left = joined(service, "member-c");
    assert.equal(service.leave(left.sessionId, left.token).ok, true);
    assert.equal(service.sessionCount(), 0);
  });

  it("strictly resumes, changes interest, and rotates the usable token", () => {
    const service = createRealtimeService({ now: () => 50_000 });
    const session = joined(service, "member-a");
    const resumed = service.resume({
      type: "resume-request",
      protocolVersion: 1,
      requestId: "resume-1",
      sessionId: session.sessionId,
      resumeToken: session.token,
      serverEpoch: session.grant.serverEpoch,
      lastReceivedSequence: session.grant.nextSequence === 0 ? 0 : session.grant.nextSequence - 1,
      interests: [OTHER_FLOOR],
    }, "auth-subject-for-member-a");
    assert.equal(resumed.ok, true);
    if (!resumed.ok) return;
    assert.equal(resumed.value.type, "resume-grant");
    assert.equal(resumed.value.requestId, "resume-1");
    assert.notEqual(resumed.value.resumeToken, session.token);
    assert.equal(service.authenticate(session.sessionId, session.token).ok, false);
    const current = service.authenticate(session.sessionId, resumed.value.resumeToken);
    assert.equal(current.ok, true);
    if (current.ok) assert.deepEqual(current.value.interests, [OTHER_FLOOR]);
  });
});

const SECRET = "realtime-test-secret-that-is-long-enough";

function jwt(subject: string): string {
  const encode = (value: unknown): string => Buffer.from(JSON.stringify(value)).toString("base64url");
  const signed = `${encode({ alg: "HS256", typ: "JWT" })}.${encode({
    sub: subject,
    exp: Math.floor(Date.now() / 1_000) + 3_600,
  })}`;
  return `${signed}.${createHmac("sha256", SECRET).update(signed).digest("base64url")}`;
}

function app() {
  const config = loadConfig({ TERA_AUTH_MODE: "jwt", TERA_AUTH_JWT_SECRET: SECRET });
  config.logLevel = "silent";
  return buildApp(config);
}

function joinBody(actorId: string, interests: readonly InterestCell[]) {
  return {
    type: "join-request",
    protocolVersion: 1,
    requestId: `request-${actorId}`,
    actorId,
    resumeToken: null,
    lastReceivedSequence: null,
    interests,
  };
}

describe("realtime HTTP routes", () => {
  it("declares exactly the three public bundled office fallbacks", () => {
    assert.deepEqual([...BUNDLED_REALTIME_OFFICE_IDS].sort(), [
      "frontier-valley",
      "lumbridge-hq",
      "mateo-court",
    ]);
  });

  it("never accepts stream credentials in a URL", async () => {
    const server = app();
    after(() => server.close());
    const response = await server.inject({
      method: "GET",
      url: "/api/v1/realtime/events?sessionId=leak&token=must-not-be-logged",
    });
    assert.equal(response.statusCode, 404);
  });

  it("authenticates before joining and rejects opaque actor-id collisions", async () => {
    const server = app();
    after(() => server.close());
    const anonymous = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/join",
      payload: joinBody("member-a", [{ kind: "city", cityId: "bay-area" }]),
    });
    assert.equal(anonymous.statusCode, 401);
    const opaque = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/join",
      headers: { authorization: `Bearer ${jwt("member-a")}` },
      payload: joinBody("page-random-opaque", [{ kind: "city", cityId: "bay-area" }]),
    });
    assert.equal(opaque.statusCode, 201);
    assert.equal(JSON.stringify(opaque.json()).includes("member-a"), false);
    const collision = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/join",
      headers: { authorization: `Bearer ${jwt("member-b")}` },
      payload: joinBody("page-random-opaque", [{ kind: "city", cityId: "bay-area" }]),
    });
    assert.equal(collision.statusCode, 409);
    const absentOffice = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/join",
      headers: { authorization: `Bearer ${jwt("member-a")}` },
      payload: joinBody("member-a", [{ kind: "office", officeId: "invented-private-office" }]),
    });
    assert.equal(absentOffice.statusCode, 400);
    assert.deepEqual(absentOffice.json(), {
      error: "invalid",
      message: "Realtime interest is unavailable.",
    });
  });

  it("allows bundled demo office envelopes without disk packs but not invented floors", async () => {
    const server = app();
    after(() => server.close());
    const authorization = `Bearer ${jwt("member-office")}`;
    const office = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/join",
      headers: { authorization },
      payload: joinBody("member-office", [{ kind: "office", officeId: "lumbridge-hq" }]),
    });
    assert.equal(office.statusCode, 201);
    const floor = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/join",
      headers: { authorization },
      payload: joinBody("member-office", [{
        kind: "floor",
        officeId: "lumbridge-hq",
        floorId: "invented-floor",
      }]),
    });
    assert.equal(floor.statusCode, 400);
  });

  it("joins, accepts an owned pose, and leaves without persistence", async () => {
    const server = app();
    after(() => server.close());
    const joinedResponse = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/join",
      headers: { authorization: `Bearer ${jwt("member-a")}` },
      payload: joinBody("member-a", [{ kind: "california-tile", level: 6, x: 1, y: 2 }]),
    });
    assert.equal(joinedResponse.statusCode, 201);
    const grant = joinedResponse.json<{ sessionId: string; resumeToken: string }>();
    const timestampMs = Date.now();
    const pose = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/pose",
      payload: {
        sessionId: grant.sessionId,
        token: grant.resumeToken,
        snapshot: {
          entity: "actor",
          actorId: "member-a",
          kind: "humanoid",
          sequence: 1,
          timestampMs,
          pose: {
            space: "geographic",
            lat: 37.7,
            lng: -122.4,
            altitudeM: 10,
            headingDeg: 0,
            pitchDeg: 0,
          },
          velocity: { xMps: 0, yMps: 0, zMps: 0, yawDegPerSec: 0 },
        },
      },
    });
    assert.equal(pose.statusCode, 202);
    assert.equal(pose.json<{ type: string }>().type, "pose-delta");
    const left = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/leave",
      payload: { sessionId: grant.sessionId, token: grant.resumeToken },
    });
    assert.equal(left.statusCode, 204);
    const gone = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/pose",
      payload: {
        sessionId: grant.sessionId,
        token: grant.resumeToken,
        snapshot: {
          entity: "actor",
          actorId: "member-a",
          kind: "humanoid",
          sequence: 2,
          timestampMs: timestampMs + 100,
          pose: {
            space: "geographic",
            lat: 37.7,
            lng: -122.4,
            altitudeM: 10,
            headingDeg: 0,
            pitchDeg: 0,
          },
          velocity: { xMps: 0, yMps: 0, zMps: 0, yawDegPerSec: 0 },
        },
      },
    });
    assert.equal(gone.statusCode, 401);
  });

  it("streams the immediate snapshot from a credential-body POST", async () => {
    const server = app();
    const address = await server.listen({ host: "127.0.0.1", port: 0 });
    const controller = new AbortController();
    try {
      const joinResponse = await fetch(`${address}/api/v1/realtime/join`, {
        method: "POST",
        headers: {
          authorization: `Bearer ${jwt("member-stream")}`,
          "content-type": "application/json",
        },
        body: JSON.stringify(joinBody("member-stream", [{ kind: "city", cityId: "bay-area" }])),
      });
      assert.equal(joinResponse.status, 201);
      const grant = await joinResponse.json() as {
        sessionId: string;
        resumeToken: string;
        serverEpoch: string;
        nextSequence: number;
      };
      const events = await fetch(`${address}/api/v1/realtime/events`, {
        method: "POST",
        headers: {
          authorization: `Bearer ${jwt("member-stream")}`,
          "content-type": "application/json",
        },
        body: JSON.stringify({
          type: "resume-request",
          protocolVersion: 1,
          requestId: "stream-resume",
          sessionId: grant.sessionId,
          resumeToken: grant.resumeToken,
          serverEpoch: grant.serverEpoch,
          lastReceivedSequence: grant.nextSequence === 0 ? 0 : grant.nextSequence - 1,
          interests: [{ kind: "city", cityId: "bay-area" }],
        }),
        signal: controller.signal,
      });
      assert.equal(events.status, 200);
      assert.match(events.headers.get("content-type") ?? "", /^text\/event-stream/);
      const reader = events.body?.getReader();
      assert.ok(reader);
      const first = await reader.read();
      assert.equal(first.done, false);
      const event = new TextDecoder().decode(first.value);
      assert.match(event, /event: resume-grant/);
      assert.match(event, /"type":"resume-grant"/);
      controller.abort();
      await reader.cancel().catch(() => undefined);
    } finally {
      controller.abort();
      await server.close();
    }
  });

  it("accepts the root client's exact move-interest resume shape on join", async () => {
    const server = app();
    after(() => server.close());
    const authorization = `Bearer ${jwt("member-client")}`;
    const first = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/join",
      headers: { authorization },
      payload: joinBody("member-client", [{ kind: "city", cityId: "bay-area" }]),
    });
    assert.equal(first.statusCode, 201);
    const grant = first.json<{
      sessionId: string;
      resumeToken: string;
      serverEpoch: string;
      nextSequence: number;
    }>();
    const moved = await server.inject({
      method: "POST",
      url: "/api/v1/realtime/join",
      headers: { authorization },
      payload: {
        type: "resume-request",
        protocolVersion: 1,
        requestId: "tera-client-2",
        sessionId: grant.sessionId,
        resumeToken: grant.resumeToken,
        serverEpoch: grant.serverEpoch,
        lastReceivedSequence: grant.nextSequence === 0 ? 0 : grant.nextSequence - 1,
        interests: [{ kind: "city", cityId: "socal" }],
      },
    });
    assert.equal(moved.statusCode, 200);
    const resume = moved.json<{ type: string; requestId: string; resumeToken: string }>();
    assert.equal(resume.type, "resume-grant");
    assert.equal(resume.requestId, "tera-client-2");
    assert.notEqual(resume.resumeToken, grant.resumeToken);
  });
});
