import { expect } from "e2e";
import { promises as fs } from "node:fs";
import { spawn, execFileSync } from "node:child_process";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { test, WEB, makeState, runPromise } from "./t3.ts";

const delay = (ms: number) => new Promise(r => setTimeout(r, ms));
async function until(f: () => Promise<boolean>, timeout = 120000) {
  const end = Date.now() + timeout;
  while (!await f()) { if (Date.now() > end) throw new Error("Startup fault milestone was not reached"); await delay(50); }
}
test("An exhausted startup failure settles after storage recovers and later messages run", WEB, async ({t3, app, screen}) => {
  const project = await t3.project("Startup recovery");
  const flag = `${T3_HOME}/crash-flag`;
  const count = `${T3_HOME}/crash-count`;
  const gate = `${T3_HOME}/lock-ready`;
  const wrapper = `${T3_HOME}/replay-wrapper.mjs`;
  await fs.writeFile(wrapper, `import fs from 'node:fs';
const flag=${JSON.stringify(flag)}, count=${JSON.stringify(count)}, gate=${JSON.stringify(gate)};
if(fs.existsSync(flag)) {
 const n=Number(fs.existsSync(count)?fs.readFileSync(count,'utf8'):0)+1; fs.writeFileSync(count,String(n));
 if(n===5) { while(!fs.existsSync(gate)) await new Promise(r=>setTimeout(r,20)); }
 process.stderr.write('Synthetic replay agent startup failure\\n'); process.exit(1);
}
const {spawn}=await import('node:child_process');
const child=spawn(process.execPath,[${JSON.stringify(`${APP_DIR}/apps/server/scripts/dev-replay-agent.ts`)}],{stdio:'inherit'});
child.on('exit',code=>process.exit(code??1));
`);
  const settingsPath = `${T3_HOME}/userdata/settings.json`;
  const settings = JSON.parse(await fs.readFile(settingsPath, "utf8"));
  settings.providerInstances.devReplay.config.commandArgs = [wrapper];
  await fs.writeFile(settingsPath, JSON.stringify(settings));
  await delay(1500);
  await t3.thread({project, title:"Healthy control", messages:["healthy turn first"]});
  await fs.writeFile(flag,"crash");
  const {cli} = makeState(T3_HOME);
  const started = JSON.parse((await runPromise(cli(["thread","start",project,"first message (start will fail)","--title","Startup failure","--provider","devReplay","--model","multi-turn","--json"]))).stdout);
  await t3.open(started);
  await until(async()=> Number(await fs.readFile(count,"utf8").catch(()=>"0")) >= 5);
  const db = `${T3_HOME}/userdata/statev2.sqlite`;
  const locker=spawn("python3",["-u","-c", "import sqlite3,time,sys\nc=sqlite3.connect(sys.argv[1]); c.execute('BEGIN EXCLUSIVE'); print('locked',flush=True); time.sleep(12); c.rollback(); print('released',flush=True)",db],{stdio:["ignore","pipe","pipe"]});
  try {
    await new Promise<void>((resolve,reject)=>{locker.stdout.once("data",()=>resolve());locker.once("error",reject);});
    await fs.writeFile(gate,"locked");
    await new Promise<void>((resolve,reject)=>{locker.once("exit",code=>code===0?resolve():reject(new Error(`Lock process exited ${code}`)));});
  } finally { if(locker.exitCode===null) locker.kill(); }
  await fs.unlink(flag);
  await delay(3000);
  await runPromise(cli(["thread","send",started.threadId,"second message after storage recovery"]));
  await delay(90000);
  const rows=execFileSync("sqlite3",["-json",db,"SELECT status, attempt_count, last_error FROM orchestration_v2_effect_outbox WHERE effect_type='provider-turn.start';"],{encoding:"utf8"});
  await fs.writeFile(`${T3_HOME}/startup-recovery-state.json`,rows);
  await app.screenshot("after-storage-recovery");
  await expect(screen.getByRole("button","Stop generation")).toBeHidden();
  await expect(screen.getByText("Turn failed to start",{exact:false})).toBeVisible();
  await expect(screen.getByText("first fixture turn complete",{exact:false})).toBeVisible();
});
