import { expect } from "e2e";
import { spawn, execFileSync } from "node:child_process";
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { run } from "./shell.ts";
import { runPromise, test, WEB } from "./t3.ts";

const pause = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

test("An exhausted startup failure settles and the next message runs after storage recovers", WEB, async ({t3, app, screen}) => {
  const project = await t3.project("Startup recovery");
  await t3.thread({project, title:"Healthy Replay control", messages:["healthy turn first"]});
  const scratch = `${process.env.T3_PROOF_SCRATCH!}/${new Date().toISOString().replaceAll(":", "-")}`;
  mkdirSync(scratch, {recursive:true});
  const flag = `${scratch}/crash-flag`;
  if(existsSync(flag)) unlinkSync(flag);
  const wrapper = `${scratch}/replay-wrapper.cjs`;
  writeFileSync(wrapper, `const fs=require('node:fs'); const cp=require('node:child_process');
if(fs.existsSync(${JSON.stringify(flag)})) { setTimeout(()=>{process.stderr.write('Replay startup deliberately failed\\n');process.exit(1)},1500); }
else {const c=cp.spawn(process.execPath,[${JSON.stringify(`${APP_DIR}/apps/server/scripts/dev-replay-agent.ts`)},...process.argv.slice(2)],{stdio:'inherit'});c.on('exit',code=>process.exit(code??1));process.on('SIGTERM',()=>c.kill('SIGTERM'));}
`);
  const settingsPath = `${T3_HOME}/userdata/settings.json`;
  const settings = JSON.parse(readFileSync(settingsPath,"utf8"));
  settings.providerInstances.devReplay.config.commandPath = process.execPath;
  settings.providerInstances.devReplay.config.commandArgs = [wrapper];
  writeFileSync(settingsPath, JSON.stringify(settings));
  await pause(2500);
  // Confirm the exact wrapper works before enabling its startup crash.
  await t3.thread({project, title:"Wrapper control",messages:["healthy wrapper turn"]});
  writeFileSync(flag,"crash");
  const cli = async (...args:string[]) => (await runPromise(run(process.execPath,["apps/server/src/bin.ts",...args,"--base-dir",T3_HOME],{cwd:APP_DIR, env:{PATH:process.env.PATH, HOME:process.env.HOME}}))).stdout;
  const started = JSON.parse(await cli("thread","start",project,"first message (start will fail)","--title","Startup failure recovery","--provider","devReplay","--model","multi-turn","--json"));
  await t3.open(started);
  const db = `${T3_HOME}/userdata/statev2.sqlite`;
  const query = (sql:string) => execFileSync("sqlite3",["-readonly","-json",db,sql],{encoding:"utf8"}).trim();
  const row = () => JSON.parse(query(`select status,attempt_count from orchestration_v2_effect_outbox where thread_id='${started.threadId}' and effect_type='provider-turn.start' order by created_at limit 1`))[0];
  const deadline = Date.now()+150000;
  while(row()?.attempt_count < 5 && Date.now()<deadline) await pause(40);
  if(row()?.attempt_count !== 5) throw new Error("Did not reach fifth claimed startup attempt");
  const locker = spawn("python3",["-u","-c","import sqlite3,sys\nc=sqlite3.connect(sys.argv[1],timeout=20)\nc.execute('BEGIN EXCLUSIVE')\nprint('locked',flush=True)\nsys.stdin.readline()\nc.rollback()",db],{stdio:["pipe","pipe","pipe"]});
  try {
    await new Promise<void>((resolve,reject)=> {locker.stdout.once("data",()=>resolve());locker.once("error",reject);});
    await pause(12000);
  } finally {locker.stdin.end("release\n");}
  await new Promise<void>(resolve=>locker.once("exit",()=>resolve()));
  await pause(6000);
  unlinkSync(flag);
  await pause(2500);
  await cli("thread","send",started.threadId,"second message after storage recovery");
  await pause(90000);
  await app.screenshot("after-storage-recovery");
  const state = {outbox:JSON.parse(query(`select effect_type,status,attempt_count from orchestration_v2_effect_outbox where thread_id='${started.threadId}'`)), runs:JSON.parse(query(`select status,completed_at from orchestration_v2_projection_runs where thread_id='${started.threadId}'`)), attempts:JSON.parse(query(`select status,json_extract(payload_json, '$.completedAt') as completed_at from orchestration_v2_projection_run_attempts where thread_id='${started.threadId}'`)), nodes:JSON.parse(query(`select status,completed_at from orchestration_v2_projection_nodes where thread_id='${started.threadId}'`))};
  writeFileSync(`${scratch}/state-${APP_DIR.split("-").at(-1)}.json`,JSON.stringify(state,null,2));
  await expect(screen.getByText("Turn failed to start",{exact:false})).toBeVisible();
  await expect(screen.getByText("first fixture turn complete",{exact:false})).toBeVisible({timeout:15000});
});
