import { expect } from "e2e";
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync, existsSync, realpathSync, utimesSync } from "node:fs";
import { join } from "node:path";
import { test, WEB, makeState, runPromise } from "./t3.ts";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";

const shell = (cmd: string, args: string[], cwd = APP_DIR) => execFileSync(cmd, args, { cwd, encoding: "utf8" }).trim();
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

test("Cleanup preserves tracked edits hidden from git status", WEB, async ({ t3, app, screen, browser }) => {
  const project = await t3.project("cleanup-demo", { "config.txt": "committed configuration\n" });
  const { cli } = makeState(T3_HOME);
  const paths: string[] = [];
  for (const name of ["assume-unchanged-edit", "skip-worktree-edit", "clean-control"]) {
    await runPromise(cli(["thread", "start", project, "Finish the fixture turn.", "--title", name,
      "--worktree", "main", "--branch", name, "--provider", "devReplay", "--model", "multi-turn", "--wait", "--json"]));
    const entries = shell("git", ["worktree", "list", "--porcelain"], project).split("\n\n");
    const entry = entries.find(e => e.includes(`branch refs/heads/${name}`));
    if (!entry) throw new Error(`Missing fixture worktree ${name}`);
    paths.push(entry.split("\n")[0]!.slice(9));
  }
  for (let i = 0; i < 2; i++) {
    shell("git", ["update-index", i === 0 ? "--assume-unchanged" : "--skip-worktree", "config.txt"], paths[i]);
    writeFileSync(join(paths[i]!, "config.txt"), `valuable hidden edit ${i + 1}\n`);
  }
  for (const path of paths) {
    if (shell("git", ["status", "--porcelain"], path) !== "") throw new Error("Fixture is not status-clean");
  }
  const runtimeFile = join(T3_HOME, "userdata/server-runtime.json");
  const config = JSON.parse(readFileSync(runtimeFile, "utf8"));
  const listener = shell("ss", ["-H", "-ltnp", `sport = :${config.port}`]);
  if (!listener.includes(`pid=${config.pid},`) || realpathSync(`/proc/${config.pid}/cwd`) !== realpathSync(join(APP_DIR, "apps/server"))) throw new Error("Fixture server ownership mismatch");
  process.kill(config.pid, "SIGTERM");
  for (let i = 0; i < 100 && existsSync(`/proc/${config.pid}`); i++) await delay(100);
  if (existsSync(`/proc/${config.pid}`)) throw new Error("Fixture server did not stop");
  const database = join(T3_HOME, "userdata/statev2.sqlite");
  shell("python3", ["-c", `
import sqlite3,json,sys
c=sqlite3.connect(sys.argv[1]); old="2026-11-01T00:00:00.000Z"
def age(x):
 if isinstance(x,dict):
  return {k:(old if k.endswith("At") and isinstance(v,str) and v.startswith("2026-") else age(v)) for k,v in x.items()}
 if isinstance(x,list): return [age(v) for v in x]
 return x
for (table,) in c.execute("select name from sqlite_master where type='table' and name like 'orchestration_v2_projection_%'").fetchall():
 cols=[r[1] for r in c.execute('pragma table_info("'+table+'")')]
 dates=[x for x in cols if x.endswith('_at')]
 for col in dates: c.execute('update "'+table+'" set "'+col+'"=? where "'+col+'" is not null',(old,))
 if 'payload_json' in cols:
  for rowid,payload in c.execute('select rowid,payload_json from "'+table+'"').fetchall():
   c.execute('update "'+table+'" set payload_json=? where rowid=?',(json.dumps(age(json.loads(payload))),rowid))
c.commit();c.close()
`, database]);
  const settingsFile = join(T3_HOME, "userdata/settings.json");
  const settings = JSON.parse(readFileSync(settingsFile, "utf8"));
  settings.storageCleanup = { ...settings.storageCleanup, worktreeAfterDays: 1, worktreeKeepWhen: "any-local-files", worktreeOnMerge: false, worktreeUnchanged: false, worktreeOnDelete: false };
  writeFileSync(settingsFile, JSON.stringify(settings));
  const entry = join(APP_DIR, "apps/server/src/bin.ts");
  const now = new Date(); utimesSync(entry, now, now);
  for (let i = 0; i < 200; i++) {
    if (existsSync(runtimeFile)) {
      const next = JSON.parse(readFileSync(runtimeFile, "utf8"));
      if (next.pid !== config.pid) { try { await fetch(next.origin + "/.well-known/t3/environment"); break; } catch {} }
    }
    await delay(100);
  }
  const paired = await runPromise(cli(["pair", "--scope", "orchestration:read", "--scope", "environment:maintain", "--scope", "settings:write"]));
  const match = paired.stdout.match(/https?:\/\/[^\s]+\/pair[^\s]*/);
  if (!match) throw new Error("No new pairing URL");
  const url = new URL(match[0]);
  await app.open(url.pathname + url.search + url.hash);
  await browser.waitForURL(/\/$/);
  await app.open("/settings/worktrees");
  // The startup sweep uses the same cleanup service as Delete now.
  await screen.getByRole("button", "Last run", { exact: false }).tap();
  await browser.evaluate(() => {
    const style = document.createElement("style");
    style.textContent = "#storage-cleanup-last-run code { visibility: hidden }";
    document.head.append(style);
    document.querySelector("#storage-cleanup-last-run")?.scrollIntoView({ block: "start" });
  });
  await app.screenshot("cleanup-result");
  await expect(screen.getByText("hidden", { exact: false }).first()).toBeVisible();
  await expect(existsSync(join(paths[0]!, "config.txt"))).toBe(true);
  await expect(existsSync(join(paths[1]!, "config.txt"))).toBe(true);
  await expect(existsSync(paths[2]!)).toBe(false);
  await screen.getByRole("button", "Delete now", { exact: true }).tap();
  await app.screenshot("manual-cleanup-result");
  await expect(existsSync(join(paths[0]!, "config.txt"))).toBe(true);
  await expect(existsSync(join(paths[1]!, "config.txt"))).toBe(true);
});
