import { test, expect, beforeEach } from "bun:test";
import { Window } from "happy-dom";
import { REALTIME_RUNTIME } from "../src/realtime-runtime.ts";

/* A fake WebSocket that records instances + sent frames and lets tests drive events. */
let sockets: FakeWS[];
class FakeWS {
  url: string;
  readyState = 0;
  sent: string[] = [];
  onopen: (() => void) | null = null;
  onclose: (() => void) | null = null;
  onerror: (() => void) | null = null;
  onmessage: ((e: { data: string }) => void) | null = null;
  constructor(url: string) {
    this.url = url;
    sockets.push(this);
  }
  send(data: string) {
    this.sent.push(data);
  }
  close() {
    this.readyState = 3;
  }
  fireOpen() {
    this.readyState = 1;
    this.onopen?.();
  }
  fireMessage(obj: unknown) {
    this.onmessage?.({ data: JSON.stringify(obj) });
  }
}

function boot(bodyHtml: string) {
  sockets = [];
  const win = new Window({ url: "http://localhost/" }) as unknown as Window &
    Record<string, unknown>;
  win.document.body.innerHTML = bodyHtml;
  const g = globalThis as Record<string, unknown>;
  g.window = win;
  g.document = win.document;
  g.location = win.location;
  g.WebSocket = FakeWS;
  (0, eval)(REALTIME_RUNTIME);
  return win as unknown as Window;
}

beforeEach(() => {
  for (const k of ["window", "document", "location", "WebSocket"]) {
    delete (globalThis as Record<string, unknown>)[k];
  }
});

const CHAT = `
  <div data-room="chat">
    <span data-room-status data-room-status-class="badge" class="badge">connecting…</span>
    <div data-room-log></div>
    <template data-room-item="message"><div class="msg"><strong>%user%</strong>: %text%</div></template>
    <template data-room-item="system"><div class="sys">%text%</div></template>
    <form data-room-send>
      <input name="user" value="Ada">
      <input name="text" value="hello" data-room-reset>
      <button type="submit">Send</button>
    </form>
  </div>`;

test("[data-room] connects to the right URL and reflects status", () => {
  const win = boot(CHAT);
  expect(sockets.length).toBe(1);
  expect(sockets[0]!.url).toBe("ws://localhost/realtime/chat");
  sockets[0]!.fireOpen();
  const status = win.document.querySelector("[data-room-status]")!;
  expect(status.textContent).toBe("connected");
  expect(status.className).toContain("is-connected");
});

test("incoming messages render via the typed template, HTML-escaped", () => {
  const win = boot(CHAT);
  sockets[0]!.fireOpen();
  sockets[0]!.fireMessage({ type: "message", user: "<b>Ada</b>", text: "hi & bye" });
  sockets[0]!.fireMessage({ type: "system", text: "joined" });
  const log = win.document.querySelector("[data-room-log]")!;
  expect(log.querySelector(".msg strong")!.textContent).toBe("<b>Ada</b>"); // escaped, not parsed
  expect(log.querySelector(".msg")!.textContent).toBe("<b>Ada</b>: hi & bye");
  expect(log.querySelector(".sys")!.textContent).toBe("joined");
});

test("submitting [data-room-send] sends JSON and clears reset fields", () => {
  const win = boot(CHAT);
  sockets[0]!.fireOpen();
  const form = win.document.querySelector("form[data-room-send]")! as unknown as HTMLFormElement;
  form.dispatchEvent(
    new (win as unknown as { Event: typeof Event }).Event("submit", {
      bubbles: true,
      cancelable: true,
    }),
  );
  expect(sockets[0]!.sent.length).toBe(1);
  expect(JSON.parse(sockets[0]!.sent[0]!)).toEqual({ user: "Ada", text: "hello" });
  // text field had data-room-reset → cleared; user field kept.
  const inputs = win.document.querySelectorAll("input");
  expect((inputs[0] as unknown as HTMLInputElement).value).toBe("Ada");
  expect((inputs[1] as unknown as HTMLInputElement).value).toBe("");
});

test("programmatic wire.room() sends and receives", () => {
  const win = boot(`<div></div>`) as unknown as Window & {
    wire: {
      room: (n: string) => {
        on: (t: string, cb: (m: unknown) => void) => unknown;
        send: (o: unknown) => void;
      };
    };
  };
  const got: unknown[] = [];
  const room = win.wire.room("lobby");
  room.on("ping", (m: unknown) => got.push(m));
  sockets[0]!.fireOpen();
  room.send({ type: "hello" });
  expect(JSON.parse(sockets[0]!.sent[0]!)).toEqual({ type: "hello" });
  sockets[0]!.fireMessage({ type: "ping", n: 1 });
  expect(got).toEqual([{ type: "ping", n: 1 }]);
});

test("same room with different users opens identity-specific sockets", () => {
  boot(`
    <div data-room="teamspace" data-room-user="asha"></div>
    <div data-room="teamspace" data-room-user="rohan"></div>
  `);
  expect(sockets.map((socket) => socket.url).sort()).toEqual([
    "ws://localhost/realtime/teamspace?user=asha",
    "ws://localhost/realtime/teamspace?user=rohan",
  ]);
});

test("declarative room reconnects when its user identity changes", () => {
  const win = boot(
    `<div id="room" data-room="teamspace" data-room-user="asha"></div>`,
  ) as unknown as Window & {
    wire: { bindRooms: (root?: unknown) => void };
  };
  expect(sockets[0]?.url).toBe("ws://localhost/realtime/teamspace?user=asha");
  win.document.getElementById("room")!.setAttribute("data-room-user", "neha");
  win.wire.bindRooms(win.document);
  expect(sockets[0]?.readyState).toBe(3);
  expect(sockets[1]?.url).toBe("ws://localhost/realtime/teamspace?user=neha");
});
