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

test("A resumed thread publishes its completed catch-up once", WEB, async ({ t3, app, screen, browser }) => {
  // Synthetic 12-step ACP recording, identical for both revisions; no paid provider.
  const entries: unknown[] = [{ type: "expect_outbound", frame: { kind: "request", method: "session/prompt", params: {} } }];
  const update = (value: unknown, afterMs = 650) => entries.push({ type: "emit_inbound", afterMs, frame: { kind: "notification", method: "session/update", params: { sessionId: "dev-replay-session", update: value } } });
  for (let step = 1; step <= 12; step++) {
    update({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: `Step ${step}. ` } });
    update({ sessionUpdate: "tool_call", toolCallId: `echo-${step}`, kind: "execute", title: `Step ${step}: Running echo`, status: "in_progress", rawInput: { command: `echo ${step}` } });
    update({ sessionUpdate: "tool_call_update", toolCallId: `echo-${step}`, status: "completed", content: [{ type: "content", content: { type: "text", text: `${step}` } }] });
  }
  update({ sessionUpdate: "agent_message_chunk", content: { type: "text", text: "Twelve steps complete." } });
  entries.push({ type: "emit_inbound", frame: { kind: "response", method: "session/prompt", result: { stopReason: "end_turn" } } });
  await writeFile(`${APP_DIR}/apps/server/scripts/dev-replay/showcase_transcript.ndjson`, entries.map(x => JSON.stringify(x)).join("\n"));
  const project = await t3.project("Catch-up demo");
  const thread = await t3.thread({ project, title: "Twelve steps", messages: ["First twelve steps"], model: "showcase" });
  await browser.addInitScript(() => {
    const state = { blocked: false, slow: false, sockets: [] as WebSocket[], frames: [] as string[], observed: [] as string[], observing: false };
    (window as any).catchupProof = state;
    const Native = window.WebSocket;
    window.WebSocket = class extends Native {
      constructor(url: string | URL, protocols?: string | string[]) {
        super(url, protocols);
        if (new URL(String(url), location.href).pathname !== "/ws") return;
        state.sockets.push(this);
        let next = 0;
        let pending = "";
        super.addEventListener("open", () => { if (state.blocked) this.close(4000, "offline reproduction"); });
        super.addEventListener("message", (event) => {
          if ((event as any).proofDelivered) return;
          if (state.blocked) { event.stopImmediatePropagation(); return; }
          if (!state.slow) return;
          event.stopImmediatePropagation();
          // Match triage's 16-event replay arrays without inventing or changing events.
          // Reassemble the server's frame-parts protocol before repacketizing its RPC Chunk.
          const part = String(event.data);
          if (part.startsWith("\u001e")) { pending += part.slice(1); return; }
          const whole = pending + part;
          pending = "";
          let packets = [whole];
          try {
            const rpc = JSON.parse(whole);
            if (rpc._tag === "Chunk" && rpc.values?.some((value: any) => value.kind === "event")) {
              packets = [];
              for (let index = 0; index < rpc.values.length; index += 16) {
                packets.push(JSON.stringify({ ...rpc, values: rpc.values.slice(index, index + 16) }));
              }
            }
          } catch {}
          for (const data of packets) {
            state.frames.push(data);
            next = Math.max(next, Date.now()) + 200;
            setTimeout(() => {
              const delivered = new MessageEvent("message", { data });
              (delivered as any).proofDelivered = true;
              this.dispatchEvent(delivered);
            }, next - Date.now());
          }
        });
      }
    };
    new MutationObserver(() => {
      if (!state.observing) return;
      const text = document.body.innerText;
      const last = text.slice(text.lastIndexOf("Second twelve steps"));
      if (last.includes("Step 3.") && !last.includes("Twelve steps complete.")) {
        if (state.observed.at(-1) !== last) state.observed.push(last);
      }
    }).observe(document, { childList: true, subtree: true, characterData: true });
  });
  let offline = false;
  await browser.route("**/api/**", async route => { if (offline) await route.abort(); else await route.continue(); });
  await t3.open(thread);
  await screen.getByRole("textbox").fill("");
  await browser.evaluate(() => { window.scrollTo(0, document.body.scrollHeight); return null; });
  const { cli } = makeState(T3_HOME);
  const second = runPromise(cli(["thread", "send", thread.threadId, "Second twelve steps", "--wait", "--json"]));
  await new Promise(resolve => setTimeout(resolve, 4200));
  await app.screenshot("cached-before-disconnect");
  offline = true;
  await browser.evaluate(() => { const s = (window as any).catchupProof; s.blocked = true; for (const ws of s.sockets) if (ws.readyState < 2) ws.close(4000, "offline reproduction"); return null; });
  await second;
  await app.screenshot("cached-while-offline");
  offline = false;
  await browser.evaluate(() => { const s = (window as any).catchupProof; s.blocked = false; s.slow = true; s.observing = true; return null; });
  // Take the paired capture during replay, before waiting for the final state.
  await browser.evaluate(() => new Promise<null>(resolve => {
    const deadline = Date.now() + 18000;
    const timer = setInterval(() => {
      const s = (window as any).catchupProof;
      const completed = document.body.innerText.split("Twelve steps complete.").length - 1;
      if (s.observed.length || completed >= 2 || Date.now() > deadline) { clearInterval(timer); resolve(null); }
    }, 25);
  }));
  await app.screenshot("resuming");
  await expect(screen.getByText("Twelve steps complete.", { exact: true })).toHaveCount(2, { timeout: 30000 });
  await app.screenshot("caught-up");
  const observed = await browser.evaluate(() => (window as any).catchupProof.observed);
  await browser.evaluate(() => { const s = (window as any).catchupProof; s.observing = false; s.slow = false; return null; });
  await runPromise(cli(["thread", "send", thread.threadId, "Third live turn", "--json"]));
  await expect(screen.getByText("Step 1.", { exact: true })).toBeVisible();
  await app.screenshot("subsequent-live-events");
  console.log("Rendered intermediate catch-up states:", JSON.stringify(observed));
  await expect(observed).toEqual([]);
});
