import { execFileSync, spawnSync } from "node:child_process";
import { readdirSync, rmSync, mkdirSync, writeFileSync, copyFileSync } from "node:fs";
import { join } from "node:path";
import { expect } from "e2e";
import { T3_HOME } from "../e2e.config.ts";
import { test, WEB, makeState, runPromise } from "./t3.ts";

test(
  "Recover a leftover checkout on resume and preserve leftovers during cleanup",
  WEB,
  async ({ t3, app, screen, browser }) => {
    const project = await t3.project("checkout-recovery", {
      "README.md":
        "# Checkout recovery demo\nThis tracked file must return when the thread resumes.\n",
      "package.json": JSON.stringify({
        name: "checkout-recovery",
        private: true,
        dependencies: { "fixture-dependency": "file:vendor" },
      }),
      "vendor/package.json": JSON.stringify({ name: "fixture-dependency", version: "1.0.0" }),
      ".gitignore": "node_modules/\n.next/\n",
      "t3.json": JSON.stringify({
        scripts: [
          {
            name: "Install dependencies",
            command: "pnpm install --ignore-scripts",
            runOnWorktreeCreate: true,
          },
        ],
      }),
    });
    const { cli } = makeState(T3_HOME);
    const call = async (args: string[]) => (await runPromise(cli(args))).stdout;
    const launched = JSON.parse(
      await call([
        "thread",
        "start",
        project,
        "Start the demo",
        "--title",
        "Recover leftover checkout",
        "--provider",
        "devReplay",
        "--model",
        "multi-turn",
        "--worktree",
        "main",
        "--branch",
        "recovery-demo",
        "--wait",
        "--json",
      ]),
    );
    const thread = { threadId: launched.threadId };
    const info = JSON.parse(await call(["thread", "show", thread.threadId, "--json"]));
    const cwd = info.thread.worktreePath;
    // Reproduce external removal of a checkout while dependency and build directories survive.
    execFileSync("pnpm", ["install", "--ignore-scripts"], { cwd, stdio: "pipe" });
    mkdirSync(join(cwd, ".next"), { recursive: true });
    writeFileSync(join(cwd, ".next/cache-marker"), "retained build cache");
    for (const entry of readdirSync(cwd))
      if (!["node_modules", ".next"].includes(entry))
        rmSync(join(cwd, entry), { recursive: true, force: true });
    execFileSync("git", ["worktree", "prune"], { cwd: project });
    await t3.open(thread);
    await screen
      .getByRole("textbox")
      .fill("Inspect your working directory and branch after the checkout was removed.");
    await screen.getByRole("textbox").press("Enter");
    await screen
      .getByText("second fixture turn complete", { exact: false })
      .waitFor({ state: "visible", timeout: 60000 });
    await screen.getByRole("button", "Toggle right panel").tap();
    await screen.getByRole("button", "Files F").tap();
    await screen.getByRole("button", "Refresh workspace files").waitFor({ state: "visible" });
    await screen.getByText("Loading files…").waitFor({ state: "hidden" });
    await app.screenshot("resumed-checkout");
    const checkoutRestored = await screen
      .getByRole("treeitem", "README.md", { exact: true })
      .isVisible();
    const restoredBranch = spawnSync("git", ["branch", "--show-current"], {
      cwd,
      encoding: "utf8",
    }).stdout.trim();
    // Make the same pnpm leftovers again for the independent cleanup reproduction.
    mkdirSync(join(cwd, "vendor"), { recursive: true });
    copyFileSync(join(project, "package.json"), join(cwd, "package.json"));
    copyFileSync(join(project, "vendor/package.json"), join(cwd, "vendor/package.json"));
    execFileSync("pnpm", ["install", "--ignore-scripts"], { cwd, stdio: "pipe" });
    mkdirSync(join(cwd, ".next"), { recursive: true });
    writeFileSync(join(cwd, ".next/cache-marker"), "retained build cache");
    for (const entry of readdirSync(cwd))
      if (!["node_modules", ".next"].includes(entry))
        rmSync(join(cwd, entry), { recursive: true, force: true });
    execFileSync("git", ["worktree", "prune"], { cwd: project });
    await screen.getByRole("button", "Thread actions for Recover leftover checkout").tap();
    await screen.getByRole("menuitem", "Archive thread").tap();
    await screen
      .getByText("Archived 1 thread", { exact: false })
      .first()
      .waitFor({ state: "visible" });
    await app.open("/settings/worktrees");
    await browser
      .locator("#storage-worktrees")
      .getByRole("button", "Advanced", { exact: true })
      .tap();
    await screen.getByRole("switch", "Delete unchanged worktrees", { exact: true }).tap();
    await app.open("/settings/storage");
    // Redact synthetic workspace locations before any result is painted or recorded.
    await browser.evaluate(
      ({ project, cwd }) => {
        const scrub = () => {
          const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
          let node;
          while ((node = walker.nextNode())) {
            const old = node.textContent ?? "";
            const clean = old
              .split(cwd)
              .join("[demo worktree]")
              .split(project)
              .join("[demo project]");
            if (old !== clean) node.textContent = clean;
          }
        };
        new MutationObserver(scrub).observe(document.body, {
          childList: true,
          subtree: true,
          characterData: true,
        });
        scrub();
      },
      { project, cwd },
    );
    await screen.getByRole("button", "Delete now", { exact: true }).tap();
    await screen.getByRole("button", "Last run", { exact: false }).tap();
    await browser
      .locator("#storage-cleanup-last-run")
      .getByText("Recover leftover checkout", { exact: true })
      .waitFor({ state: "visible" });
    await browser.locator("#storage-cleanup-last-run").scrollIntoView();
    await app.screenshot("cleanup-leftover-folder");
    await expect(screen.getByText("Not a linked worktree", { exact: true })).toBeVisible();
    expect(readdirSync(cwd).sort()).toEqual([".next", "node_modules"]);
    expect(checkoutRestored).toBe(true);
    expect(restoredBranch).toBe("recovery-demo");
  },
);
