import { writeFile } from "node:fs/promises";
import { expect } from "e2e";
import { test, WEB } from "./t3.ts";

test("A send survives a lost dispatch and a lost acknowledgement", WEB, async ({ t3, app, screen, browser }) => {
  await browser.addInitScript(() => {
    const state = { mode: "", frames: [] as { commandId: string; text: string }[], dropped: 0, connections: 0 };
    (window as any).socketProof = state;
    const Native = window.WebSocket;
    window.WebSocket = class extends Native {
      pending: string | undefined;
      constructor(url: string | URL, protocols?: string | string[]) {
        super(url, protocols);
        state.connections++;
        this.addEventListener("message", (event) => {
          if (!this.pending) return;
          let frames;
          try { frames = JSON.parse(event.data); } catch { return; }
          const list = Array.isArray(frames) ? frames : [frames];
          if (list.some((frame: any) => String(frame.requestId) === this.pending && frame._tag === "Exit")) {
            event.stopImmediatePropagation();
            this.pending = undefined;
            state.dropped++;
            this.close(4000, "Injected acknowledgement loss");
          }
        });
      }
      send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
        const raw = typeof data === "string" ? data : new TextDecoder().decode(data as ArrayBuffer);
        if (raw.includes('"message.dispatch"')) {
          const parsed = JSON.parse(raw);
          const frames = Array.isArray(parsed) ? parsed : [parsed];
          for (const frame of frames) {
            const command = frame.payload;
            if (command?.type !== "message.dispatch") continue;
            state.frames.push({ commandId: command.commandId, text: command.text ?? "" });
            if (state.mode === "drop") {
              state.mode = "";
              state.dropped++;
              this.close(4000, "Injected dispatch loss");
              return;
            }
            if (state.mode === "ack") {
              state.mode = "";
              this.pending = String(frame.id);
            }
          }
        }
        super.send(data);
      }
    };
  });
  const project = await t3.project("Socket recovery");
  await t3.thread({ project, title: "Reconnect proof", messages: ["Prepare this conversation"] });
  await t3.open();
  await screen.getByRole("button", "Reconnect proof, Done, Socket recovery").tap();
  const results: boolean[] = [];
  for (const [mode, message, label] of [
    ["drop", "Deliver this message after connection loss", "dispatch-loss-recovered"],
    ["ack", "Keep exactly one message after acknowledgement loss", "acknowledgement-loss-recovered"],
  ]) {
    await browser.evaluate((mode) => { (window as any).socketProof.mode = mode; }, mode);
    await screen.getByRole("textbox").fill(message);
    await screen.getByRole("textbox").press("Enter");
    await browser.evaluate(() => new Promise<void>((resolve) => setTimeout(resolve, 12000)));
    await app.screenshot(label);
    results.push(await browser.evaluate((message) => {
      const composer = document.querySelector('[contenteditable="true"]') as HTMLElement | null;
      const textbox = document.querySelector('textarea') as HTMLTextAreaElement | null;
      const empty = !(composer?.innerText.trim() || textbox?.value.trim());
      const state = (window as any).socketProof;
      const frames = state.frames.filter((frame: any) => frame.text?.includes(message));
      return empty && document.body.innerText.includes(message) && frames.length >= 2 && new Set(frames.map((frame: any) => frame.commandId)).size === 1;
    }, message));
  }
  const transport = await browser.evaluate(() => (window as any).socketProof);
  const path = await browser.evaluate(() => location.pathname);
  await app.open(path);
  await browser.evaluate(() => new Promise<void>((resolve) => setTimeout(resolve, 6000)));
  await app.screenshot("durable-after-reload");
  const durable = await browser.evaluate(() => {
    const texts = [...document.querySelectorAll('[data-message-role="user"]')].map((node) => node.textContent?.trim());
    return ["Deliver this message after connection loss", "Keep exactly one message after acknowledgement loss"].map((message) => texts.filter((text) => text?.includes(message)).length);
  });
  await writeFile(`transport-${Date.now()}.json`, JSON.stringify({ transport, results, durable }, null, 2));
  expect(durable).toEqual([1, 1]);
  expect(results).toEqual([true, true]);
});
