import assert from "node:assert/strict";
import { createHmac } from "node:crypto";
import { after, describe, it } from "node:test";
import { buildApp } from "../app.ts";
import { loadConfig } from "../config.ts";
import { createMediaSignalService, officeHasMediaBinding } from "../media/index.ts";
import LUMBRIDGE_HQ from "../../../src/offices/lumbridge-hq.ts";
import type {
  ScreenShareBinding,
  ScreenShareCreateRequest,
  ScreenShareJoinRequest,
  ScreenShareRevokeRequest,
  ScreenShareSignalRequest,
} from "../../../src/media/signalingTypes.ts";

const BINDING: ScreenShareBinding = {
  officeId: "lumbridge-hq", levelId: "level-1", roomId: "live-work", screenId: "lobby-monitor",
};
const create = (sequence = 1): ScreenShareCreateRequest => ({
  type: "screen-share-create-request", protocolVersion: 1, requestId: "request-create", sequence,
  timestampMs: 1_000, binding: BINDING, role: "presenter",
});
const join = (sequence = 1): ScreenShareJoinRequest => ({
  type: "screen-share-join-request", protocolVersion: 1, requestId: "request-join", sequence,
  timestampMs: 1_000, binding: BINDING, role: "viewer", viewerOptIn: true,
});

function grants(service: ReturnType<typeof createMediaSignalService>) {
  const presenter = service.join(create(), "presenter-subject");
  const viewer = service.join(join(), "viewer-subject");
  assert.equal(presenter.ok, true); assert.equal(viewer.ok, true);
  if (!presenter.ok || !viewer.ok || presenter.value.type !== "screen-share-create-grant" || viewer.value.type !== "screen-share-join-grant") {
    throw new Error("grant setup failed");
  }
  return { presenter: presenter.value, viewer: viewer.value };
}

describe("office media signaling service", () => {
  it("issues opaque participants, snapshots peers, and enforces P2P signaling roles", () => {
    const service = createMediaSignalService();
    const { presenter, viewer } = grants(service);
    const p = presenter.grant.credential; const v = viewer.grant.credential;
    assert.notEqual(p.sessionId, p.participantId); assert.notEqual(p.participantId, p.grantToken);
    assert.deepEqual(presenter.participants, []);
    assert.deepEqual(viewer.participants, [{ participantId: p.participantId, role: "presenter" }]);
    assert.equal(JSON.stringify(viewer).includes("subject"), false);

    const received: string[] = [];
    assert.equal(service.subscribe(v, "viewer-subject", (message) => received.push(message.type)).ok, true);
    const offer: ScreenShareSignalRequest = {
      type: "screen-share-signal-request", protocolVersion: 1, sequence: 2, timestampMs: 1_001,
      binding: BINDING, credential: p, targetParticipantId: v.participantId,
      signal: { kind: "sdp", descriptionType: "offer", sdp: "v=0" },
    };
    assert.equal(service.signal(offer, "presenter-subject").ok, true);
    assert.equal(received.includes("screen-share-signal-relay"), true);
    assert.equal(service.signal({ ...offer, sequence: 3, credential: v, targetParticipantId: p.participantId }, "viewer-subject").ok, false);
    assert.equal(service.signal({ ...offer, sequence: 4, credential: p }, "wrong-subject").ok, false);
  });

  it("rotates short-lived hashed grants and rejects stale capabilities", () => {
    let now = 10_000;
    const service = createMediaSignalService({ now: () => now, grantTtlMs: 100 });
    const { presenter } = grants(service);
    const old = presenter.grant.credential;
    const resumed = service.resume({
      type: "screen-share-resume-request", protocolVersion: 1, requestId: "request-resume", sequence: 2,
      timestampMs: now, binding: BINDING, credential: old, lastReceivedSequence: 0,
    }, "presenter-subject");
    assert.equal(resumed.ok, true);
    if (!resumed.ok) return;
    assert.notEqual(resumed.value.grant.credential.grantToken, old.grantToken);
    assert.equal(service.revalidate(old, "presenter-subject").ok, false);
    now += 101;
    assert.equal(service.revalidate(resumed.value.grant.credential, "presenter-subject").ok, false);
    assert.equal(service.sessionCount(), 0);
  });

  it("authorizes ICE only for a live grant owner on its exact screen binding", () => {
    let now = 10_000;
    const service = createMediaSignalService({ now: () => now, grantTtlMs: 100 });
    const presenter = service.join(create(), "presenter-subject");
    assert.equal(presenter.ok, true);
    if (!presenter.ok || presenter.value.type !== "screen-share-create-grant") return;
    const credential = presenter.value.grant.credential;
    assert.deepEqual(service.authorizeIce(credential, BINDING, "presenter-subject"), {
      ok: true, value: { expiresAtMs: 10_100 },
    });
    assert.equal(service.authorizeIce(credential, BINDING, "wrong-subject").ok, false);
    assert.equal(service.authorizeIce(credential, { ...BINDING, screenId: "another-screen" }, "presenter-subject").ok, false);
    now = 10_101;
    assert.equal(service.authorizeIce(credential, BINDING, "presenter-subject").ok, false,
      "expired signaling grants cannot mint fresh TURN credentials");
  });

  it("delivers a queued offer after resume with a sequence newer than the grant", () => {
    const service = createMediaSignalService();
    const { presenter, viewer } = grants(service);
    const offered = service.signal({
      type: "screen-share-signal-request", protocolVersion: 1, sequence: 2, timestampMs: 1_001,
      binding: BINDING, credential: presenter.grant.credential,
      targetParticipantId: viewer.grant.credential.participantId,
      signal: { kind: "sdp", descriptionType: "offer", sdp: "v=0\r\na=ice-options:trickle" },
    }, "presenter-subject");
    assert.equal(offered.ok, true);

    const resumed = service.resume({
      type: "screen-share-resume-request", protocolVersion: 1, requestId: "request-viewer-resume",
      sequence: 2, timestampMs: 1_002, binding: BINDING,
      credential: viewer.grant.credential, lastReceivedSequence: viewer.sequence,
    }, "viewer-subject");
    assert.equal(resumed.ok, true);
    if (!resumed.ok) return;

    const delivered: Array<{ type: string; sequence: number }> = [];
    const subscribed = service.subscribe(
      resumed.value.grant.credential,
      "viewer-subject",
      (message) => delivered.push({ type: message.type, sequence: message.sequence }),
    );
    assert.equal(subscribed.ok, true);
    const relay = delivered.find((message) => message.type === "screen-share-signal-relay");
    assert.ok(relay, "queued offer relay is delivered after token rotation");
    assert.ok(relay.sequence > resumed.value.sequence, "relay follows the resume grant cursor");
    assert.ok(delivered.every((message) => message.sequence > resumed.value.sequence));
  });

  it("rejects exhausted client sequences without orphaning sessions or grants", () => {
    const service = createMediaSignalService();
    const exhaustedCreate = service.join(create(Number.MAX_SAFE_INTEGER), "presenter-subject");
    assert.equal(exhaustedCreate.ok, false);
    assert.equal(service.sessionCount(), 0, "invalid create does not leave an unreachable session");

    const presenter = service.join(create(), "presenter-subject");
    assert.equal(presenter.ok, true);
    const exhaustedJoin = service.join(join(Number.MAX_SAFE_INTEGER), "viewer-subject");
    assert.equal(exhaustedJoin.ok, false);
    const viewer = service.join(join(), "viewer-subject");
    assert.equal(viewer.ok, true, "invalid join does not leave an orphan participant");
    if (!viewer.ok || viewer.value.type !== "screen-share-join-grant") return;

    const exhaustedResume = service.resume({
      type: "screen-share-resume-request", protocolVersion: 1, requestId: "request-exhausted-resume",
      sequence: Number.MAX_SAFE_INTEGER, timestampMs: 1_002, binding: BINDING,
      credential: viewer.value.grant.credential, lastReceivedSequence: viewer.value.sequence,
    }, "viewer-subject");
    assert.equal(exhaustedResume.ok, false);
    assert.equal(service.revalidate(viewer.value.grant.credential, "viewer-subject").ok, true,
      "rejected resume does not rotate away the usable grant");
    const correctedResume = service.resume({
      type: "screen-share-resume-request", protocolVersion: 1, requestId: "request-corrected-resume",
      sequence: 2, timestampMs: 1_003, binding: BINDING,
      credential: viewer.value.grant.credential, lastReceivedSequence: viewer.value.sequence,
    }, "viewer-subject");
    assert.equal(correctedResume.ok, true, "rejected resume does not consume its client sequence");
  });

  it("commits a signaling sequence only after the request is authorized", () => {
    const service = createMediaSignalService();
    const { presenter, viewer } = grants(service);
    const received: string[] = [];
    service.subscribe(viewer.grant.credential, "viewer-subject", (message) => received.push(message.type));
    const offer: ScreenShareSignalRequest = {
      type: "screen-share-signal-request", protocolVersion: 1, sequence: 2, timestampMs: 1_002,
      binding: BINDING, credential: presenter.grant.credential,
      targetParticipantId: "missing-opaque-participant",
      signal: { kind: "sdp", descriptionType: "offer", sdp: "v=0" },
    };
    assert.equal(service.signal(offer, "presenter-subject").ok, false);
    assert.equal(service.signal({
      ...offer,
      targetParticipantId: viewer.grant.credential.participantId,
    }, "presenter-subject").ok, true, "corrected request can reuse the rejected sequence");
    assert.deepEqual(received.filter((type) => type === "screen-share-signal-relay"), ["screen-share-signal-relay"]);
  });

  it("bounds the mesh and sends viewer lifecycle snapshots", () => {
    const service = createMediaSignalService({ maximumParticipantsPerSession: 2 });
    const { presenter, viewer } = grants(service);
    assert.equal(service.join(join(2), "third-viewer").ok, false);
    const snapshots: number[] = [];
    service.subscribe(presenter.grant.credential, "presenter-subject", (message) => {
      if (message.type === "screen-share-participants") snapshots.push(message.participants.length);
    });
    const leave: ScreenShareRevokeRequest = {
      type: "screen-share-revoke-request", protocolVersion: 1, requestId: "request-leave", sequence: 2,
      timestampMs: 1_002, binding: BINDING, credential: viewer.grant.credential,
      scope: "participant", targetParticipantId: viewer.grant.credential.participantId, reason: "viewer-left",
    };
    assert.equal(service.leave(leave, "viewer-subject", false).ok, true);
    assert.equal(snapshots.at(-1), 0);
  });

  it("validates the exact authored level, room and screen prop", () => {
    assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, BINDING), true);
    assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, { ...BINDING, roomId: "kitchen" }), false);
    assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, { ...BINDING, screenId: "not-authored" }), false);
  });
});

const SECRET = "media-route-test-secret-that-is-long-enough";
function bearer(subject: string) {
  const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
  const signed = `${encode({ alg: "HS256", typ: "JWT" })}.${encode({ sub: subject, exp: Math.floor(Date.now() / 1000) + 600 })}`;
  return { authorization: `Bearer ${signed}.${createHmac("sha256", SECRET).update(signed).digest("base64url")}` };
}

describe("office media signaling routes", () => {
  it("authenticates before exact lookup and keeps capabilities in POST bodies", async () => {
    const config = loadConfig({ TERA_AUTH_MODE: "jwt", TERA_AUTH_JWT_SECRET: SECRET }); config.logLevel = "silent";
    const app = buildApp(config); after(() => app.close());
    const anonymous = await app.inject({ method: "POST", url: "/api/v1/media/join", payload: create() });
    assert.equal(anonymous.statusCode, 401);
    const locator = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("presenter"), payload: { ...create(), sourceUrl: "https://private.invalid/stream" } });
    assert.equal(locator.statusCode, 400);
    const wrongRoom = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("viewer"), payload: { ...join(), binding: { ...BINDING, roomId: "not-the-room" } } });
    assert.equal(wrongRoom.statusCode, 404);
    const presenter = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("presenter"), payload: create() });
    const viewer = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("viewer"), payload: join() });
    assert.equal(presenter.statusCode, 201); assert.equal(viewer.statusCode, 201);
    assert.equal(presenter.headers["cache-control"], "private, no-store"); assert.equal(presenter.headers.location, undefined);
    const p = presenter.json<{ grant: { credential: ScreenShareSignalRequest["credential"] } }>();
    const v = viewer.json<{ grant: { credential: ScreenShareSignalRequest["credential"] } }>();
    const sent = await app.inject({ method: "POST", url: "/api/v1/media/signal", headers: bearer("presenter"), payload: {
      type: "screen-share-signal-request", protocolVersion: 1, sequence: 2, timestampMs: 1_002,
      binding: BINDING, credential: p.grant.credential, targetParticipantId: v.grant.credential.participantId,
      signal: { kind: "sdp", descriptionType: "offer", sdp: "v=0" },
    } });
    assert.equal(sent.statusCode, 204);
    const stolen = await app.inject({ method: "POST", url: "/api/v1/media/signal", headers: bearer("viewer"), payload: {
      type: "screen-share-signal-request", protocolVersion: 1, sequence: 3, timestampMs: 1_003,
      binding: BINDING, credential: p.grant.credential, targetParticipantId: v.grant.credential.participantId,
      signal: { kind: "ice-complete" },
    } });
    assert.equal(stolen.statusCode, 401);
  });

  it("opens SSE with a POST-body resume grant and rotates the capability", async () => {
    const config = loadConfig({ TERA_AUTH_MODE: "jwt", TERA_AUTH_JWT_SECRET: SECRET }); config.logLevel = "silent";
    const app = buildApp(config); after(() => app.close());
    const origin = await app.listen({ host: "127.0.0.1", port: 0 });
    const created = await fetch(`${origin}/api/v1/media/join`, {
      method: "POST", headers: { ...bearer("presenter"), "content-type": "application/json" }, body: JSON.stringify(create()),
    });
    const body = await created.json() as { grant: { credential: ScreenShareSignalRequest["credential"] } };
    const controller = new AbortController();
    const streamed = await fetch(`${origin}/api/v1/media/events`, {
      method: "POST", signal: controller.signal,
      headers: { ...bearer("presenter"), "content-type": "application/json" },
      body: JSON.stringify({
        type: "screen-share-resume-request", protocolVersion: 1, requestId: "request-events", sequence: 2,
        timestampMs: 1_003, binding: BINDING, credential: body.grant.credential, lastReceivedSequence: 0,
      }),
    });
    assert.equal(streamed.status, 200);
    assert.match(streamed.headers.get("content-type") ?? "", /^text\/event-stream/);
    const first = await streamed.body?.getReader().read();
    const text = new TextDecoder().decode(first?.value);
    assert.match(text, /screen-share-resume-grant/);
    assert.equal(text.includes(body.grant.credential.grantToken), false);
    assert.equal(streamed.url.includes("grantToken"), false);
    controller.abort();
  });

  it("streams a pre-subscription offer after a newer viewer resume grant", async () => {
    const config = loadConfig({ TERA_AUTH_MODE: "jwt", TERA_AUTH_JWT_SECRET: SECRET }); config.logLevel = "silent";
    const app = buildApp(config); after(() => app.close());
    const origin = await app.listen({ host: "127.0.0.1", port: 0 });
    const post = async (path: string, subject: string, payload: unknown) => fetch(`${origin}${path}`, {
      method: "POST", headers: { ...bearer(subject), "content-type": "application/json" }, body: JSON.stringify(payload),
    });
    const presenterResponse = await post("/api/v1/media/join", "presenter", create());
    const viewerResponse = await post("/api/v1/media/join", "viewer", join());
    const presenter = await presenterResponse.json() as {
      grant: { credential: ScreenShareSignalRequest["credential"] };
    };
    const viewer = await viewerResponse.json() as {
      sequence: number;
      grant: { credential: ScreenShareSignalRequest["credential"] };
    };
    const offered = await post("/api/v1/media/signal", "presenter", {
      type: "screen-share-signal-request", protocolVersion: 1, sequence: 2, timestampMs: 1_002,
      binding: BINDING, credential: presenter.grant.credential,
      targetParticipantId: viewer.grant.credential.participantId,
      signal: { kind: "sdp", descriptionType: "offer", sdp: "v=0\r\na=ice-options:trickle" },
    });
    assert.equal(offered.status, 204);

    const controller = new AbortController();
    const streamed = await fetch(`${origin}/api/v1/media/events`, {
      method: "POST", signal: controller.signal,
      headers: { ...bearer("viewer"), "content-type": "application/json" },
      body: JSON.stringify({
        type: "screen-share-resume-request", protocolVersion: 1, requestId: "request-viewer-events",
        sequence: 2, timestampMs: 1_003, binding: BINDING,
        credential: viewer.grant.credential, lastReceivedSequence: viewer.sequence,
      }),
    });
    assert.equal(streamed.status, 200);
    const reader = streamed.body?.getReader();
    assert.ok(reader);
    let text = "";
    for (let reads = 0; reads < 3 && !text.includes("screen-share-signal-relay"); reads += 1) {
      const chunk = await reader.read();
      if (chunk.done) break;
      text += new TextDecoder().decode(chunk.value);
    }
    controller.abort();
    const messages = text.split("\n")
      .filter((line) => line.startsWith("data: "))
      .map((line) => JSON.parse(line.slice(6)) as { type: string; sequence: number });
    const grant = messages.find((message) => message.type === "screen-share-resume-grant");
    const relay = messages.find((message) => message.type === "screen-share-signal-relay");
    assert.ok(grant);
    assert.ok(relay, "queued offer reaches the fetch-stream after resume");
    assert.ok(relay.sequence > grant.sequence);
  });
});
