import { expect } from "e2e";
import { readFile, writeFile, readlink } from "node:fs/promises";
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
import { once } from "node:events";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { test, WEB } from "./t3.ts";

const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
async function until(check: () => Promise<boolean>) {
  for (let i = 0; i < 120; i++) { if (await check()) return; await delay(500); }
  throw new Error("The real app did not reach the reproduction milestone");
}

test("Interrupted runs explain server recovery exactly once", WEB, async ({ t3, app, screen, browser }) => {
  const fixture = `${APP_DIR}/apps/server/scripts/dev-replay/showcase_transcript.ndjson`;
  const original = await readFile(fixture, "utf8");
  const update = (value: object) => JSON.stringify({ type: "emit_inbound", afterMs: 2000, frame: {kind: "notification", method: "session/update", params: {sessionId: "replay", update: value}} });
  const frames = [JSON.stringify({type: "expect_outbound", frame: {kind: "request", method: "session/prompt", params: "<any>"}}), update({sessionUpdate: "tool_call", toolCallId: "foreground", kind: "execute", title: "sleep 300", status: "in_progress", rawInput: {command: "sleep 300"}}), ...Array.from({length: 150}, () => update({sessionUpdate: "tool_call_update", toolCallId: "foreground", status: "in_progress"})), JSON.stringify({type: "emit_inbound", frame: {kind: "response", method: "session/prompt", result: {stopReason: "end_turn"}}})];
  await writeFile(fixture, frames.join("\n") + "\n");
  let replacement: ChildProcess | undefined;
  let opened = false;
  const runtime = async () => JSON.parse(await readFile(`${T3_HOME}/userdata/server-runtime.json`, "utf8"));
  async function restart(signal: "SIGKILL" | "SIGTERM") {
    const route = opened ? await browser.evaluate(() => location.pathname + location.search) : undefined;
    const state = await runtime();
    const cwd = await readlink(`/proc/${state.pid}/cwd`);
    if (!cwd.startsWith(APP_DIR + "/")) throw new Error("Server cwd is outside this scenario checkout");
    const sockets = execFileSync("ss", ["-H", "-ltnp", `sport = :${state.port}`], {encoding: "utf8"});
    if (!sockets.includes(`pid=${state.pid},`)) throw new Error("Scenario server does not own its port");
    const args = (await readFile(`/proc/${state.pid}/cmdline`, "utf8")).split("\0").filter(Boolean);
    const env = Object.fromEntries((await readFile(`/proc/${state.pid}/environ`, "utf8")).split("\0").filter(Boolean).map(v => { const i = v.indexOf("="); return [v.slice(0, i), v.slice(i + 1)]; }));
    process.kill(state.pid, signal);
    await until(async () => { try { process.kill(state.pid, 0); return false; } catch { return true; } });
    replacement = spawn(args[0], args.slice(1), {cwd, env: {...env, T3CODE_DEV_SEED: "0"}, stdio: "ignore"});
    await until(async () => { try { const next = await runtime(); return next.pid !== state.pid && (await fetch(`${next.origin}/.well-known/t3/environment`)).ok; } catch { return false; } });
    if (route) {
      await app.open(route);
      await until(async () => { const body = await text(); return body.includes("Ask anything") && !body.includes("is reconnecting"); });
    }
    await delay(1000);
  }
  const text = () => browser.evaluate(() => document.body.innerText);
  try {
    const settingsPath = `${T3_HOME}/userdata/settings.json`;
    const settings = JSON.parse(await readFile(settingsPath, "utf8"));
    await writeFile(settingsPath, JSON.stringify({...settings, continueThreadsAfterServerUpdate: false}));
    await restart("SIGTERM");
    await t3.project("Recovery demo");
    await t3.open();
    opened = true;
    await screen.getByRole("textbox").fill("Run a long foreground command");
    await screen.getByRole("textbox").press("Enter");
    await until(async () => (await text()).includes("Running sleep"));
    await app.screenshot("foreground-running");
    await restart("SIGKILL");
    await until(async () => (await text()).includes("Worked for"));
    await delay(1500);
    await app.screenshot("after-abrupt-restart");
    const abrupt = await text();
    await restart("SIGKILL");
    await delay(1500);
    await app.screenshot("after-second-restart");
    const repeated = await text();
    await screen.getByRole("button", "New thread", {exact: true}).tap();
    await screen.getByRole("textbox").fill("Run a foreground command before graceful shutdown");
    await screen.getByRole("textbox").press("Enter");
    await until(async () => (await text()).includes("Running sleep"));
    await app.screenshot("graceful-foreground-running");
    await restart("SIGTERM");
    await delay(1500);
    await app.screenshot("after-graceful-shutdown");
    const graceful = await text();
    await writeFile(settingsPath, JSON.stringify({...settings, continueThreadsAfterServerUpdate: true}));
    await restart("SIGTERM");
    await screen.getByRole("button", "New thread", {exact: true}).tap();
    await screen.getByRole("textbox").fill("Run a foreground command with automatic continuation");
    await screen.getByRole("textbox").press("Enter");
    await until(async () => (await text()).includes("Running sleep"));
    await app.screenshot("continuation-foreground-running");
    await restart("SIGKILL");
    await until(async () => (await text()).includes("Running sleep") && (await text()).includes("T3 Code restarted and resumed this turn"));
    await app.screenshot("after-restart-with-continuation");
    const reason = "Cancelled because the server restarted before the provider work completed.";
    await expect(screen.getByText(reason, {exact: true})).toBeVisible();
    expect(abrupt.split(reason).length - 1).toBe(1);
    expect(repeated.split(reason).length - 1).toBe(1);
    expect(graceful.split("Cancelled because the server shut down before the provider work completed.").length - 1).toBe(1);
  } finally {
    if (replacement?.pid) { replacement.kill("SIGTERM"); await Promise.race([once(replacement, "exit"), delay(10000)]); }
    await writeFile(fixture, original);
  }
});
