import { execFileSync } from 'node:child_process';
import { readFileSync, writeFileSync, lstatSync, existsSync, utimesSync, mkdirSync, chmodSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import { expect } from 'e2e';
import { T3_HOME } from '../e2e.config.ts';
import { makeState, runPromise, test, WEB } from './t3.ts';

// Every observation below is taken from the real server or Git, then shown in the app's file viewer.
test('git-annex removal, cleanup, orphan reporting and retained links', WEB, async ({ t3, app, screen, browser }) => {
  try {
  const git = (cwd: string, ...args: string[]) => execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
  const project = await t3.project('annex-demo', { 'README.md': 'Synthetic git-annex evidence\n' });
  git(project, 'config', 'user.name', 'Scenario');
  git(project, 'config', 'user.email', 'scenario@example.invalid');
  git(project, 'annex', 'init', 'scenario');
  writeFileSync(join(project, 'asset.txt'), 'Annex content is readable\n');
  git(project, 'annex', 'add', 'asset.txt');
  git(project, 'commit', '-m', 'Add annex content');
  const state = makeState(T3_HOME);
  const start = async (title: string, branch: string) => {
    const result = await runPromise(state.cli(['thread', 'start', project, 'Start', '--title', title,
      '--provider', 'devReplay', '--model', 'multi-turn', '--worktree', 'main', '--branch', branch, '--wait', '--json']));
    const thread = JSON.parse(result.stdout);
    const records = git(project, 'worktree', 'list', '--porcelain').split('\n\n');
    const path = records.find((r) => r.includes(`branch refs/heads/${branch}`))!.split('\n')[0]!.slice(9);
    return { threadId: thread.threadId, path, title };
  };
  const manual = await start('Manual removal', 't3/annex-a');
  const cleanup = await start('Cleanup unchanged', 't3/annex-b');
  const dirty = await start('Dirty protection', 't3/annex-dirty');
  const locked = await start('Locked protection', 't3/annex-locked');
  const observer = await t3.thread({ project, title: 'Runtime verification', messages: ['Start'] });
  writeFileSync(join(dirty.path, 'untracked.txt'), 'Keep this work\n');
  git(project, 'worktree', 'lock', '--reason', 'Evidence protection', locked.path);
  const orphanAnnex = join(T3_HOME, 'worktrees', 'orphan-annex');
  const orphanPlain = join(T3_HOME, 'worktrees', 'orphan-plain');
  git(project, 'worktree', 'add', '-b', 'orphan-annex', orphanAnnex, 'main');
  git(project, 'worktree', 'add', '-b', 'orphan-plain', orphanPlain, 'main');
  const plainAdmin = git(orphanPlain, 'rev-parse', '--absolute-git-dir');
  unlinkSync(join(orphanPlain, '.git'));
  writeFileSync(join(orphanPlain, '.git'), `gitdir: ${plainAdmin}\n`);
  const old = new Date(Date.now() - 3 * 60 * 60 * 1000);
  utimesSync(orphanAnnex, old, old); utimesSync(orphanPlain, old, old);
  // Observe the exact .git representation handed to unmodified system Git.
  const traceDir = join(T3_HOME, 'git-removal-trace');
  mkdirSync(traceDir);
  const realGit = execFileSync('which', ['git'], {encoding:'utf8'}).trim();
  const wrapper = join(T3_HOME, 'bin', 'git');
  writeFileSync(wrapper, `#!/bin/sh
case " $* " in
  *" worktree remove "*)
    for target do :; done
    name=$(basename "$target")
    log='${traceDir}'/"$name"
    kind=missing
    if [ -L "$target/.git" ]; then kind=symlink; elif [ -f "$target/.git" ]; then kind=gitdir-file; fi
    printf '%s\\n' "$kind" > "$log.kind"
    '${realGit}' "$@" 2> "$log.stderr"
    code=$?
    printf '%s\\n' "$code" > "$log.exit"
    cat "$log.stderr" >&2
    exit "$code"
    ;;
  *) exec '${realGit}' "$@" ;;
esac
`);
  chmodSync(wrapper, 0o755);
  const trace = (path: string) => {
    const name = path.split('/').at(-1)!;
    const prefix = join(traceDir, name);
    if (!existsSync(prefix+'.kind')) return {kind:'NOT CALLED', exit:'NOT CALLED', stderr:'NOT CALLED'};
    return {kind:readFileSync(prefix+'.kind','utf8').trim(), exit:readFileSync(prefix+'.exit','utf8').trim(),
      stderr:readFileSync(prefix+'.stderr','utf8').trim().replaceAll(path, '<worktree>').replaceAll(project,'<repository>').replaceAll(T3_HOME,'<scenario-home>')};
  };
  const initial = [manual, cleanup, dirty, locked].every((x) => lstatSync(join(x.path, '.git')).isSymbolicLink());
  await t3.open(cleanup);
  const environmentId = new URL(await browser.evaluate(() => location.href)).pathname.split('/')[1]!;
  const rpcRemove = async (path: string, force: boolean) => browser.evaluate(async (data) => {
    const { appAtomRegistry } = await import('/src/rpc/atomRegistry.ts');
    const { vcsEnvironment } = await import('/src/state/vcs.ts');
    const result = await vcsEnvironment.removeWorktree.run(appAtomRegistry, {environmentId: data.environmentId, input: {cwd: data.project, path: data.path, force: data.force}});
    return JSON.parse(JSON.stringify(result));
  }, {environmentId, project, path, force});
  // Settle through the same context menu a person uses; allow the documented 60s grace.
  await screen.getByRole('button', 'Cleanup unchanged', {exact: false}).first().secondaryTap();
  await screen.getByRole('menuitem', 'Settle thread', {exact: true}).tap();
  const settledAt = Date.now();
  await t3.open('/settings/worktrees');
  await screen.getByRole('button', 'Advanced', {exact: true}).last().tap();
  await screen.getByRole('switch', 'Delete unchanged worktrees').check();
  await app.screenshot('cleanup-rule');
  const dirtyResult = await rpcRemove(dirty.path, false);
  const lockedResult = await rpcRemove(locked.path, true);
  const retained = (entry: typeof dirty, result: any) => ({
    refused: result._tag === 'Failure',
    symlink: lstatSync(join(entry.path, '.git')).isSymbolicLink(),
    content: readFileSync(join(entry.path, 'asset.txt'), 'utf8').trim(),
    registered: git(project, 'worktree', 'list', '--porcelain').includes(entry.path),
    trace: trace(entry.path),
  });
  const dirtyCheck = retained(dirty, dirtyResult);
  const lockedCheck = retained(locked, lockedResult);
  const show = async (label: string, text: string) => {
    writeFileSync(join(project, `${label}.txt`), text);
    await t3.open(observer);
    await screen.getByRole('button', 'Attach files', {exact:true}).waitFor({state:'visible'});
    await browser.keyboard.press('Control+p');
    await screen.getByPlaceholder('Search files…').fill(`${label}.txt`);
    await screen.getByRole('option', `${label}.txt`, {exact: false}).tap();
    await screen.getByText(text.split('\n')[0]!, {exact:false}).first().waitFor({state:'visible'});
    const maximize = screen.getByRole('button', 'Maximize panel', {exact:true});
    if (await maximize.count()) await maximize.tap();
    await new Promise((resolve) => setTimeout(resolve, 600));
    await app.screenshot(label);
    await browser.keyboard.press('Escape');
  };
  await show('refusal-restores-annex', [
    'RUNTIME CHECK: refused removals preserve annex links',
    'Real T3 vcs.removeWorktree RPC; dirty force=false, locked force=true',
    '',
    `Dirty worktree: refused=${dirtyCheck.refused}; .git symlink=${dirtyCheck.symlink}`,
    `Registered=${dirtyCheck.registered}; annex read: ${dirtyCheck.content}`,
    `Untracked content: ${readFileSync(join(dirty.path,'untracked.txt'),'utf8').trim()}`,
    '',
    `Locked worktree: refused=${lockedCheck.refused}; .git symlink=${lockedCheck.symlink}`,
    `Registered=${lockedCheck.registered}; annex read: ${lockedCheck.content}`,
    '',
    `Dirty: .git at system Git call=${dirtyCheck.trace.kind}; exit=${dirtyCheck.trace.exit}`,
    dirtyCheck.trace.stderr,
    '',
    `Locked: .git at system Git call=${lockedCheck.trace.kind}; exit=${lockedCheck.trace.exit}`,
    lockedCheck.trace.stderr,
  ].join('\n'));
  await t3.open(manual);
  await screen.getByRole('button', 'Manual removal', {exact:false}).first().secondaryTap();
  await screen.getByRole('menuitem', 'Delete', {exact:false}).tap();
  await screen.getByRole('button', 'Confirm', {exact:true}).tap();
  await screen.getByRole('button', 'Confirm', {exact:true}).tap();
  await screen.getByRole('dialog').waitFor({state:'hidden'});
  // The server's completed removal RPC is reflected by either a toast or disappearance on disk.
  await new Promise((resolve) => setTimeout(resolve, 1500));
  await app.screenshot('manual-removal');
  const manualExists = existsSync(manual.path);
  const manualRegistered = git(project,'worktree','list','--porcelain').includes(manual.path);
  await show('manual-registration', [
    'RUNTIME CHECK: manual worktree removal',
    'After Delete thread > Confirm > Delete worktree too > Confirm',
    `Initially all four T3 worktrees had annex .git symlinks: ${initial}`,
    `annex-a folder exists: ${manualExists}`,
    `annex-a registered in git worktree list: ${manualRegistered}`,
    `System Git saw .git=${trace(manual.path).kind}; exit=${trace(manual.path).exit}`,
    trace(manual.path).stderr,
    '', 'git worktree list --porcelain (branches only):',
    ...git(project,'worktree','list','--porcelain').split('\n').filter((line) => line.startsWith('branch ') || line.startsWith('locked')),
  ].join('\n'));
  const remaining = 62_000 - (Date.now()-settledAt);
  if (remaining > 0) await new Promise((resolve) => setTimeout(resolve, remaining));
  await t3.open('/settings/maintenance');
  await screen.getByRole('button','Delete now',{exact:true}).tap();
  await screen.getByRole('button','Delete now',{exact:true}).waitFor({state:'visible'});
  await new Promise((resolve) => setTimeout(resolve, 1500));
  await app.screenshot('maintenance-cleanup');
  const report = await browser.evaluate(async (environmentId) => {
    const { appAtomRegistry } = await import('/src/rpc/atomRegistry.ts');
    const { serverEnvironment } = await import('/src/state/server.ts');
    const atom = serverEnvironment.storageCleanupReport({environmentId,input:{}});
    const off = appAtomRegistry.mount(atom);
    try {
      for (let n=0;n<100;n++) {
        const result = appAtomRegistry.get(atom);
        if (result._tag==='Success') return result.value;
        await new Promise(r=>setTimeout(r,100));
      }
      throw new Error('Cleanup report did not load');
    } finally {off();}
  }, environmentId);
  const entries = report.entries.filter((x:any)=> x.kind==='worktree');
  const cleanupEntry = entries.find((x:any)=> x.path===cleanup.path);
  const annexOrphan = entries.find((x:any)=> x.path===orphanAnnex);
  const plainOrphan = entries.find((x:any)=> x.path===orphanPlain);
  await screen.getByRole('button', 'Last run', {exact:false}).tap();
  await browser.locator('[data-slot="settings-row"]').filter({hasText:cleanup.path.split('/').at(-1)!}).scrollIntoView();
  await new Promise((resolve) => setTimeout(resolve, 600));
  await app.screenshot('cleanup-native-results');
  await browser.locator('[data-slot="settings-row"]').filter({hasText:orphanPlain}).scrollIntoView();
  await new Promise((resolve) => setTimeout(resolve, 600));
  await app.screenshot('orphan-native-results');
  const cleanupExists = existsSync(cleanup.path);
  const cleanupRegistered = git(project,'worktree','list','--porcelain').includes(cleanup.path);
  await show('cleanup-and-orphans', [
    'RUNTIME CHECK: storage cleanup and orphan report',
    'Delete unchanged worktrees enabled; thread settled; 60s grace elapsed.',
    'Settings > Maintenance > Delete now. Real server report:',
    '',
    `annex-b outcome: ${cleanupEntry?.outcome ?? 'MISSING'}`,
    `annex-b reason: ${cleanupEntry?.reason ?? 'MISSING'}`,
    `annex-b folder exists: ${cleanupExists}; registered: ${cleanupRegistered}`,
    `System Git saw .git=${trace(cleanup.path).kind}; exit=${trace(cleanup.path).exit}`,
    '',
    `orphan-annex: ${annexOrphan?.reason ?? 'MISSING FROM REPORT'}`,
    `orphan-annex size: ${annexOrphan?.bytes ?? 'MISSING'} bytes`,
    `orphan-plain control: ${plainOrphan?.reason ?? 'MISSING FROM REPORT'}`,
    '',
    `Dirty still linked/readable: ${lstatSync(join(dirty.path,'.git')).isSymbolicLink()} / ${readFileSync(join(dirty.path,'asset.txt'),'utf8').trim()}`,
    `Locked still linked/readable: ${lstatSync(join(locked.path,'.git')).isSymbolicLink()} / ${readFileSync(join(locked.path,'asset.txt'),'utf8').trim()}`,
  ].join('\n'));
  expect({manualExists, manualRegistered, cleanupExists, cleanupRegistered, cleanupOutcome: cleanupEntry?.outcome,
    orphanReason: annexOrphan?.reason, plainOrphanReason: plainOrphan?.reason,
    dirtyRefused:dirtyCheck.refused, dirtyLinked:dirtyCheck.symlink, dirtyContent:dirtyCheck.content,
    lockedRefused:lockedCheck.refused, lockedLinked:lockedCheck.symlink, lockedContent:lockedCheck.content}).toEqual({
      manualExists:false,manualRegistered:false,cleanupExists:false,cleanupRegistered:false,cleanupOutcome:'removed',
      orphanReason:'No thread uses this worktree',plainOrphanReason:'No thread uses this worktree',
      dirtyRefused:true,dirtyLinked:true,dirtyContent:'Annex content is readable',
      lockedRefused:true,lockedLinked:true,lockedContent:'Annex content is readable',
    });
  } finally { execFileSync('chmod', ['-R', 'u+w', T3_HOME]); }
});
