// node --experimental-strip-types --test
//
// Exercises the Room DO's per-peer routing (mesh P2): client→host frames get a sender tag, and
// host→client frames are demuxed by tag to exactly one client. `fetch` isn't tested here (it needs
// the runtime's WebSocketPair); we drive `webSocketMessage` against fake sockets pushed into a fake
// DurableObjectState, which is where the routing logic lives.
import { test } from "node:test";
import assert from "node:assert/strict";
import { Room } from "../src/room.ts";

interface Meta {
  role: "host" | "client";
  deviceId: string;
}

class FakeWS {
  tags: string[] = [];
  sent: Array<ArrayBuffer | string> = [];
  closed: { code: number; reason?: string } | null = null;
  meta: Meta;
  constructor(meta: Meta) {
    this.meta = meta;
  }
  serializeAttachment(a: Meta): void {
    this.meta = a;
  }
  deserializeAttachment(): Meta {
    return this.meta;
  }
  send(m: ArrayBuffer | string): void {
    if (this.closed) throw new Error("closed");
    this.sent.push(m);
  }
  close(code: number, reason?: string): void {
    this.closed = { code, reason };
  }
}

class FakeState {
  sockets: FakeWS[] = [];
  id = { toString: () => "room-do-id" };
  add(ws: FakeWS): void {
    ws.tags = [ws.deserializeAttachment().role, ws.deserializeAttachment().deviceId];
    this.sockets.push(ws);
  }
  getWebSockets(tag?: string): FakeWS[] {
    return tag ? this.sockets.filter((s) => s.tags.includes(tag)) : this.sockets;
  }
  acceptWebSocket(): void {
    /* not exercised here */
  }
}

function room(state: FakeState): Room {
  return new Room(state as unknown as DurableObjectState, {} as never);
}

async function deviceTag(deviceId: string): Promise<Uint8Array> {
  const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(deviceId));
  return new Uint8Array(d).slice(0, 8);
}

function ws(role: "host" | "client", deviceId: string): FakeWS {
  const w = new FakeWS({ role, deviceId });
  return w;
}

test("client→host frame is delivered to the host with a sender tag prepended", async () => {
  const state = new FakeState();
  const host = ws("host", "host-dev");
  const phone = ws("client", "phone-A");
  state.add(host);
  state.add(phone);

  const payload = new Uint8Array([1, 2, 3, 4]).buffer;
  await room(state).webSocketMessage(phone as never, payload);

  assert.equal(host.sent.length, 1);
  const framed = new Uint8Array(host.sent[0] as ArrayBuffer);
  assert.equal(framed[0], 0x01); // envelope version
  assert.deepEqual(framed.subarray(1, 9), await deviceTag("phone-A")); // sender tag
  assert.deepEqual(framed.subarray(9), new Uint8Array([1, 2, 3, 4])); // untouched payload
  assert.equal(phone.sent.length, 0); // not echoed back to the sender
});

test("host→client frame is demuxed by tag to exactly the addressed client", async () => {
  const state = new FakeState();
  const host = ws("host", "host-dev");
  const phoneA = ws("client", "phone-A");
  const phoneB = ws("client", "phone-B");
  state.add(host);
  state.add(phoneA);
  state.add(phoneB);

  const raw = new Uint8Array([9, 8, 7]);
  const tag = await deviceTag("phone-B");
  const framed = new Uint8Array(9 + raw.length);
  framed[0] = 0x01;
  framed.set(tag, 1);
  framed.set(raw, 9);
  await room(state).webSocketMessage(host as never, framed.buffer);

  assert.equal(phoneB.sent.length, 1);
  assert.deepEqual(new Uint8Array(phoneB.sent[0] as ArrayBuffer), raw); // envelope stripped
  assert.equal(phoneA.sent.length, 0); // NOT broadcast to the other client
  assert.equal(host.sent.length, 0);
});

test("host frame with an unknown tag is dropped, not broadcast", async () => {
  const state = new FakeState();
  const host = ws("host", "host-dev");
  const phoneA = ws("client", "phone-A");
  state.add(host);
  state.add(phoneA);

  const framed = new Uint8Array(9 + 2);
  framed[0] = 0x01;
  framed.set(await deviceTag("phone-ghost"), 1);
  framed.set(new Uint8Array([5, 5]), 9);
  await room(state).webSocketMessage(host as never, framed.buffer);

  assert.equal(phoneA.sent.length, 0);
});

test("client→host frame is dropped when no host is present", async () => {
  const state = new FakeState();
  const phoneA = ws("client", "phone-A");
  state.add(phoneA);
  await room(state).webSocketMessage(phoneA as never, new Uint8Array([1]).buffer);
  assert.equal(phoneA.sent.length, 0); // nothing to route to; no throw
});

test("an oversized frame closes the socket with a policy code", async () => {
  const state = new FakeState();
  const host = ws("host", "host-dev");
  const phone = ws("client", "phone-A");
  state.add(host);
  state.add(phone);

  const huge = new ArrayBuffer(20 * 1024 * 1024 + 1);
  await room(state).webSocketMessage(phone as never, huge);
  assert.equal(phone.closed?.code, 1008);
  assert.equal(host.sent.length, 0);
});
