import { execFileSync } from "node:child_process";
import { readdirSync, readlinkSync, readFileSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { expect } from "e2e";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { test, WEB } from "./t3.ts";

test("A rejected send leaves the conversation and keeps both texts recoverable", WEB, async ({ t3, app, screen, browser }) => {
  const project = await t3.project("Send recovery");
  await t3.thread({ project, title: "Failed send recovery", messages: ["Hello"], model: "multi-turn" });
  await t3.open();
  await screen.getByRole("button", "Failed send recovery, Done, Send recovery", { exact: true }).tap();
  const composer = screen.getByRole("textbox");
  await composer.fill("rejected message with image");
  const uploaded = browser.waitForResponse(/attachments/);
  await browser.evaluate(() => {
    const bytes = Uint8Array.from(atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aV1cAAAAASUVORK5CYII="), c => c.charCodeAt(0));
    const transfer = new DataTransfer();
    transfer.items.add(new File([bytes], "sample.png", { type: "image/png" }));
    const input = document.querySelector('input[type="file"]');
    if (!(input instanceof HTMLInputElement)) throw new Error("Attachment input missing");
    input.files = transfer.files;
    input.dispatchEvent(new Event("change", { bubbles: true }));
    return null;
  });
  await uploaded;
  const attachments = join(T3_HOME, "userdata/attachments");
  const pending = readdirSync(attachments).filter(name => name.startsWith("pending-") && name.endsWith(".png"));
  if (pending.length !== 1) throw new Error("Expected exactly one uploaded synthetic attachment");
  unlinkSync(join(attachments, pending[0]!));
  const listeners = execFileSync("ss", ["-H", "-ltnp"], { encoding: "utf8" });
  const runtime = JSON.parse(readFileSync(join(T3_HOME, "userdata/server-runtime.json"), "utf8"));
  const pids = [...new Set([...listeners.split("\n").filter(line => line.includes(`:${runtime.port} `)).join("\n").matchAll(/pid=(\d+)/g)].map(match => Number(match[1])))];
  const server = pids.filter(pid => {
    try {
      return readlinkSync(`/proc/${pid}/cwd`) === join(APP_DIR, "apps/server") && readFileSync(`/proc/${pid}/cmdline`, "utf8").includes("src/bin.ts");
    } catch { return false; }
  });
  if (server.length !== 1) throw new Error(`Expected one server listener, found ${server.length}`);
  process.kill(server[0]!, "SIGSTOP");
  try {
    await composer.press("Enter");
    await composer.fill("my next message draft");
    await app.screenshot("send-pending-new-draft");
  } finally {
    process.kill(server[0]!, "SIGCONT");
  }
  // Wait for the real server rejection, without asserting on an unrelated control.
  await screen.getByText(/Attachment 'sample.png' cannot be sent/).first().waitFor({ state: "visible" });
  const findCapture = (root: string, suffix: string): string | undefined => {
    for (const entry of readdirSync(root, { withFileTypes: true })) {
      const path = join(root, entry.name);
      if (entry.isDirectory()) { const found = findCapture(path, suffix); if (found) return found; }
      else if (path.endsWith(suffix)) return path;
    }
  };
  const stableCapture = async (label: string) => {
    let previous: Buffer | undefined;
    for (let attempt = 0; attempt < 10; attempt++) {
      const relative = await app.screenshot(label);
      const capture = findCapture(process.env.EVIDENCE_OUTPUT!, relative);
      if (!capture) throw new Error("Capture file missing");
      const pixels = readFileSync(capture);
      if (previous?.equals(pixels)) return;
      previous = pixels;
      await new Promise(resolve => setTimeout(resolve, 500));
    }
    throw new Error("Capture did not settle");
  };
  await stableCapture("rejected-send-new-draft");
  await expect(screen.getByRole("region", "Conversation", { exact: true }).getByText(/rejected message with image/).first()).toBeHidden();
  await expect(composer).toHaveText("my next message draft");
  await expect(screen.getByRole("dialog", "Message not sent", { exact: true }).getByRole("button", "Copy message", { exact: true })).toBeVisible();
  await new Promise(resolve => setTimeout(resolve, 8000));
  await stableCapture("recovery-still-available");
  await screen.getByRole("dialog", "Message not sent", { exact: true }).getByRole("button", "Copy message", { exact: true }).tap();
  const recovered = await browser.evaluate(async () => await navigator.clipboard.readText());
  const recoveredText = recovered.split(" ![sample.png](t3-context://")[0];
  await composer.fill("my next message draft | Recovered: " + recoveredText);
  await expect(composer).toHaveText("my next message draft | Recovered: rejected message with image");
  await stableCapture("failed-text-recovered");
});
