import { expect } from "e2e";
import { readFile, writeFile, mkdtemp, symlink, watch } from "node:fs/promises";
import { join } from "node:path";
import { test, WEB, makeState, runPromise } from "./t3.ts";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { run } from "./shell.ts";

// This only changes the synthetic ACP model: real server services remain untouched.
// A replacement ACP session pauses for 20s; a new thread writes a real untracked file.
test("A new overlapping turn preserves its file across checkpoint restoration", WEB, async ({ t3, app, screen }) => {
  const scratch = await mkdtemp("/tmp/factory-pr-23671-a2-evidence-3/provider-");
  const agentPath = join(APP_DIR, "apps/server/scripts/dev-replay-agent.ts");
  const original = await readFile(agentPath, "utf8");
  const marker = join(scratch, "rollback-pending");
  const done = join(scratch, "rollback-replied");
  const waitForFile = async (path: string) => {
    const watcher = watch(scratch);
    try {
      for await (const _ of watcher) {
        try { await readFile(path); return; } catch {}
      }
    } finally { await watcher.return(undefined); }
  };
  const setup = `
        const counterPath = ${JSON.stringify(join(scratch, "sessions"))};
        const sessionCount = Number(NodeFS.existsSync(counterPath) ? NodeFS.readFileSync(counterPath, "utf8") : "0") + 1;
        NodeFS.writeFileSync(counterPath, String(sessionCount));
        if (sessionCount === 2) {
          NodeFS.writeFileSync(${JSON.stringify(marker)}, "Provider rollback is waiting for its replacement ACP session");
          await new Promise(resolve => setTimeout(resolve, 20000));
          NodeFS.writeFileSync(${JSON.stringify(done)}, "Provider replacement session replied");
        }
`;
  const prompt = `
        const promptText = JSON.stringify(params.prompt);
        if (promptText.includes("Write preserved.txt") || promptText.includes("Verify preserved.txt")) {
          const file = session.cwd + "/preserved.txt";
          let text;
          if (promptText.includes("Write preserved.txt")) {
            NodeFS.writeFileSync(file, "Written by overlapping thread B\\n");
            text = "Thread B wrote preserved.txt in the overlapping workspace.";
          } else {
            text = NodeFS.existsSync(file)
              ? "PRESERVED: preserved.txt still exists after checkpoint restoration."
              : "LOST: preserved.txt was removed by checkpoint restoration.";
          }
          send({method: "session/update", params: {sessionId, update: {sessionUpdate: "agent_message_chunk", content: {type: "text", text}}}});
          return {stopReason: "end_turn"};
        }
`;
  const modified = original.replace('const cwd = typeof params.cwd', setup + '\n        const cwd = typeof params.cwd').replace('const turns = loadTurns(session.model);', prompt + '\n        const turns = loadTurns(session.model);');
  if (modified === original) throw new Error("Could not prepare synthetic ACP provider");
  await writeFile(agentPath, modified);
  const { cli } = makeState(T3_HOME);
  try {
    const project = await t3.project("Checkpoint race", { "README.md": "Checkpoint restoration demonstration\n" });
    const started = JSON.parse((await runPromise(cli(["thread", "start", project, "Prepare checkpoint A", "--title", "Thread A", "--worktree", "main", "--provider", "devReplay", "--model", "multi-turn", "--wait", "--json"]))).stdout);
    const worktrees = (await runPromise(run("git", ["worktree", "list", "--porcelain"], {cwd: project}))).stdout;
    const worktree = worktrees.split("\n").filter(line => line.startsWith("worktree ")).map(line => line.slice(9)).find(path => path !== project)!;
    const alias = join(scratch, "overlapping-workspace");
    await symlink(worktree, alias);
    await t3.open();
    await screen.getByRole("button", /^Thread A,/).tap();
    await screen.getByRole("textbox").fill("Make a second checkpoint");
    await screen.getByRole("textbox").press("Enter");
    await screen.getByText("second fixture turn complete", {exact: false}).waitFor();
    await screen.getByRole("button", "Edit from here").first().tap();
    const pending = waitForFile(marker);
    await screen.getByRole("button", "Revert files too", {exact: true}).tap();
    await pending;
    // Register the alias only after isolation was checked and provider rollback is pending.
    await runPromise(cli(["project", "add", alias, "--title", "Overlapping workspace"]));
    const replacementReplied = waitForFile(done);
    const b = JSON.parse((await runPromise(cli(["thread", "start", alias, "Write preserved.txt", "--title", "Thread B", "--provider", "devReplay", "--model", "multi-turn", "--wait", "--json"]))).stdout);
    await replacementReplied;
    // Waiting for A's command to finish via the UI also waits for the file restore.
    await screen.getByRole("button", /^Thread A,/).tap();
    await screen.getByRole("button", "Edit from here").waitFor({state: "hidden"});
    await runPromise(cli(["thread", "send", b.threadId, "Verify preserved.txt", "--wait", "--json"]));
    await screen.getByRole("button", /^Thread B,/).press("Enter");
    await screen.getByText(/(?:PRESERVED|LOST): preserved.txt/, {exact: false}).waitFor();
    // Evidence capture requires two identical screenshots 500 ms apart.
    let previous = await app.screenshot("stability-probe");
    let stable = false;
    for (let attempt = 0; attempt < 8; attempt++) {
      await new Promise(resolve => setTimeout(resolve, 500));
      const current = await app.screenshot("stability-probe");
      if ((await readFile(previous)).equals(await readFile(current))) { stable = true; break; }
      previous = current;
    }
    if (!stable) throw new Error("The final view did not settle for a stable evidence capture");
    await app.screenshot("file-after-checkpoint-restore");
    await expect(screen.getByText("PRESERVED: preserved.txt still exists after checkpoint restoration.", {exact: false})).toBeVisible();
  } finally {
    await writeFile(agentPath, original);
  }
});
