/// <reference lib="dom" />
import { expect } from "e2e";
import { readdir, readFile, writeFile } from "node:fs/promises";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { test, WEB } from "./t3.ts";

const pause = (ms: number) => new Promise(r => setTimeout(r, ms));
async function until(check: () => Promise<boolean>, description: string, limit = 20000) {
  const deadline = Date.now() + limit;
  while (!(await check())) {
    if (Date.now() > deadline) throw new Error(`Setup wait: ${description}`);
    await pause(100);
  }
}

test("Cancelling a queued edit releases uploaded files and image blob previews", WEB, async ({ t3, app, browser, screen }) => {
  await browser.addInitScript(() => {
    const state = { live: new Set<string>(), deletes: 0, uploads: 0 };
    (window as any).uploadProof = state;
    const create = URL.createObjectURL.bind(URL);
    const revoke = URL.revokeObjectURL.bind(URL);
    URL.createObjectURL = blob => { const url = create(blob); state.live.add(url); return url; };
    URL.revokeObjectURL = url => { state.live.delete(url); revoke(url); };
    const send = WebSocket.prototype.send;
    WebSocket.prototype.send = function(data) {
      const value = typeof data === "string" ? data : "";
      if (value.includes('attachments.delete')) state.deletes++;
      if (value.includes('attachments.createUploadUrl')) state.uploads++;
      return send.call(this, data);
    };
  });
  const project = await t3.project("Upload cleanup demo");
  // Hold only the completion response of a real replay, without changing app behavior.
  const wrapper = `${T3_HOME}/hold-replay.cjs`;
  await writeFile(wrapper, `
const {spawn} = require('node:child_process');
const {createInterface} = require('node:readline');
const child = spawn(process.execPath, [${JSON.stringify(`${APP_DIR}/apps/server/scripts/dev-replay-agent.ts`)}], {stdio:['pipe','pipe','inherit']});
const held = new Set();
const replies = [];
createInterface({input:process.stdin}).on('line', line => {
  const m = JSON.parse(line);
  if (m.method === 'session/prompt' && JSON.stringify(m.params).includes('HOLD REPLAY OPEN')) held.add(m.id);
  child.stdin.write(line+'\\n');
  if (m.method === 'session/cancel') for (const reply of replies.splice(0)) process.stdout.write(reply+'\\n');
});
createInterface({input:child.stdout}).on('line', line => {
  const m = JSON.parse(line);
  if (!m.method && held.has(m.id)) replies.push(line);
  else process.stdout.write(line+'\\n');
});
process.on('SIGTERM', () => { child.kill(); process.exit(); });
process.on('exit', () => child.kill());
`);
  const settingsPath = `${T3_HOME}/userdata/settings.json`;
  const settings = JSON.parse(await readFile(settingsPath, "utf8"));
  settings.defaultRuntimeMode = "full-access";
  settings.providerInstances.devReplay.config.commandArgs = [wrapper];
  await writeFile(settingsPath, JSON.stringify(settings));
  const thread = await t3.thread({ project, title: "Queued upload cleanup", messages: ["First fixture turn"], model: "multi-turn" });
  await t3.open(thread);
  const composer = screen.getByRole("textbox");
  await composer.fill("HOLD REPLAY OPEN");
  await composer.press("Enter");
  await screen.getByRole("button", "Stop generation").waitFor({ state: "visible" });
  await composer.fill("QUEUED ORIGINAL TEXT");
  await screen.getByRole("button", "Steer message", { exact: true }).tap({ modifiers: ["Control"] });
  await screen.getByRole("button", "Edit queued message").tap();
  await browser.evaluate(async () => {
    const canvas = document.createElement("canvas"); canvas.width = canvas.height = 32;
    const ctx = canvas.getContext("2d")!; ctx.fillStyle = "red"; ctx.fillRect(0, 0, 32, 32);
    const blob = await new Promise<Blob>(r => canvas.toBlob(b => r(b!), "image/png"));
    const files = new DataTransfer();
    files.items.add(new File([blob], "red-square.png", { type: "image/png" }));
    files.items.add(new File(["Synthetic upload fixture.\n"], "notes.txt", { type: "text/plain" }));
    const input = document.querySelector('input[type="file"]') as HTMLInputElement;
    if (!input) throw new Error("Composer file input missing");
    input.files = files.files; input.dispatchEvent(new Event("change", { bubbles: true }));
  });
  const pending = async () => (await readdir(`${T3_HOME}/userdata/attachments`, { recursive: true })).filter(name => name.includes("pending-"));
  await until(async () => (await pending()).length === 2 && await browser.evaluate(() => (window as any).uploadProof.uploads === 2), "both staged uploads on disk");
  await until(async () => !(await browser.evaluate(() => document.body.innerText.includes("Uploading"))), "uploads settled");
  const show = async (phase: string) => {
    const count = (await pending()).length;
    return browser.evaluate(({ phase, count }) => {
      const state = (window as any).uploadProof;
      let panel = document.getElementById("upload-proof");
      if (!panel) { panel = document.createElement("aside"); panel.id = "upload-proof"; document.body.append(panel); }
      panel.style.cssText = "position:fixed;right:24px;top:80px;z-index:99999;background:#111827;color:white;border:2px solid #64748b;padding:18px;font:16px/1.7 monospace;white-space:pre;pointer-events:none";
      panel.textContent = `UPLOAD LIFECYCLE OBSERVER\n${phase}\nPending files on server: ${count}\nUpload URL requests: ${state.uploads}\nDelete requests: ${state.deletes}\nLive blob URLs: ${state.live.size}`;
      return { pending: count, deletes: state.deletes, liveBlobs: state.live.size };
    }, { phase, count });
  };
  await show("Queued edit: uploads finished");
  await app.screenshot("uploaded-before-cancel");
  await screen.getByRole("button", "Cancel editing queued message").tap();
  await pause(3000);
  const result = await show("Cancelled edit: resources remaining");
  await app.screenshot("after-cancel");
  await expect(result).toEqual({ pending: 0, deletes: 2, liveBlobs: 0 });
});
