/// <reference lib="dom" />
import { expect } from "e2e";
import { readFile, writeFile, chmod } from "node:fs/promises";
import { join } from "node:path";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { makeState, runPromise, test, WEB } from "./t3.ts";

test("Settling disarms usage-limit recovery and explains the refusal", WEB, async ({t3, app, browser, screen}) => {
  await browser.addInitScript(() => {
    const pending = new Map<string, (value: unknown) => void>();
    let socket: WebSocket;
    const watched = new WeakSet<WebSocket>();
    const send = WebSocket.prototype.send;
    WebSocket.prototype.send = function(data) {
      if (new URL(this.url).pathname !== "/ws") return send.call(this,data);
      socket = this;
      if (!watched.has(this)) {
        watched.add(this);
        this.addEventListener("message", event => {
          if (typeof event.data !== "string") return;
          for (const msg of [JSON.parse(event.data)].flat()) {
            if (msg._tag === "Exit" && pending.has(String(msg.requestId))) {
              pending.get(String(msg.requestId))!(msg.exit);
              pending.delete(String(msg.requestId));
            }
          }
        });
      }
      return send.call(this, data);
    };
    Object.assign(window, {proofRpc: (tag: string, payload: unknown) => new Promise(resolve => {
      const id = String(900000000 + Math.floor(Math.random() * 100000000));
      pending.set(id, resolve);
      socket.send(JSON.stringify({_tag:"Request", id, tag, payload, headers:[]}));
    })});
  });
  const rpc = (tag: string, payload: unknown) => browser.evaluate(async ({tag,payload}) =>
    (window as any).proofRpc(tag,payload), {tag,payload}) as Promise<any>;
  const dispatch = (command: Record<string,unknown>) => rpc("orchestration.dispatchCommand", {commandId:crypto.randomUUID(), ...command});
  const project = await t3.project("Recovery demo");
  const thread = await t3.thread({project,title:"Usage limited work",messages:["first"]});
  await t3.open(thread);
  await screen.getByRole("button","Thread actions for Usage limited work").tap();
  await browser.keyboard.press("Escape");
  const wire = JSON.parse(await readFile(join(APP_DIR,"apps/server/src/provider/testFixtures/codexMultiAgentWire.json"),"utf8"));
  const binary = join(T3_HOME,"bin","usage-peer");
  const reset = Math.floor(Date.now()/1000)+150;
  await writeFile(binary, `#!/usr/bin/env node
import * as readline from 'node:readline';
import {appendFileSync} from 'node:fs';
const wire=${JSON.stringify(wire.responses)};
const reset=${reset};
const requestLog=${JSON.stringify(join(T3_HOME,"proof-turns.ndjson"))};
let threadId='usage-fixture', count=0;
const send=x=>process.stdout.write(JSON.stringify(x)+'\\n');
if(process.argv.includes('--version')) { console.log('codex-cli 0.156.1'); process.exit(0); }
if(process.argv.includes('--help')) { console.log('app-server'); process.exit(0); }
readline.createInterface({input:process.stdin}).on('line',line=>{
 const m=JSON.parse(line), {id,method}=m;
 const reply=result=>send({id,result});
 const note=(method,params)=>send({method,params});
 if(method==='initialize') return reply({userAgent:'proof-peer/1.0',codexHome:'/fixtures',platformFamily:'unix',platformOs:'linux'});
 if(method==='account/read') return reply({account:{type:'apiKey'},requiresOpenaiAuth:false});
 if(method==='model/list'||method==='skills/list') return reply({data:[]});
 if(method==='account/rateLimits/read') return reply({rateLimits:{limitId:'codex',primary:{usedPercent:100,resetsAt:reset,windowDurationMins:300}}});
 if(method==='thread/start'||method==='thread/resume') { const r=structuredClone(wire.threadStart); threadId=m.params?.threadId||crypto.randomUUID(); r.thread.id=threadId; r.thread.sessionId=threadId; return reply(r); }
 if(method==='turn/start') {
   appendFileSync(requestLog, JSON.stringify({method,at:new Date().toISOString(),input:m.params?.input})+'\\n');
   const turn={...wire.turnStart.turn,id:crypto.randomUUID(),status:'inProgress',error:null};
   reply({turn}); note('turn/started',{threadId,turn});
   note('account/rateLimits/updated',{rateLimits:{limitId:'codex',primary:{usedPercent:100,resetsAt:reset,windowDurationMins:300}}});
   note('turn/completed',{threadId,turn:{...turn,status:'failed',error:{message:'Usage limit reached. Try again at reset.',codexErrorInfo:'usageLimitExceeded'}}}); return;
 }
 if(id!==undefined) reply({});
});
`);
  await chmod(binary,0o755);
  const setup=await rpc("server.updateSettings", {patch:{},providerInstanceMutation:{operation:"create",instanceId:"usageFixture",instance:{driver:"codex",displayName:"Usage fixture",enabled:true,config:{binaryPath:binary}}}});
  if(setup._tag!=="Success") throw new Error(JSON.stringify(setup));
  const switched=await dispatch({type:"provider.switch",threadId:thread.threadId,modelSelection:{instanceId:"usageFixture",model:"gpt-5.6-luna"}});
  if(switched._tag!=="Success") throw new Error(JSON.stringify(switched));
  const state=makeState(T3_HOME);
  await runPromise(state.cli(["thread","send",thread.threadId,"Continue the work","--wait","--json"],[0,1,2]));
  await screen.getByRole("button","Resume at reset").tap();
  await screen.getByRole("button","Snooze until reset").tap();
  await app.screenshot("armed-recovery");
  await screen.getByRole("button","Thread actions for Usage limited work").tap();
  await screen.getByRole("menuitem","Settle thread",{exact:true}).tap();
  await app.screenshot("settled-recovery");
  const projection=await rpc("orchestration.getThreadProjection",{threadId:thread.threadId});
  await writeFile(join(T3_HOME,"proof-projection.json"),JSON.stringify(projection,null,2));
  const text=JSON.stringify(projection);
  const runId=(text.match(/"runId":"([^"]+)"/)||[])[1];
  const resetAt=new Date(reset*1000).toISOString();
  const refusal=await dispatch({type:"thread.metadata.update",threadId:thread.threadId,limitRecovery:{runId,resetAt,autoResume:true}});
  await writeFile(join(T3_HOME,"proof-refusal.json"),JSON.stringify(refusal,null,2));
  await screen.getByRole("button","Un-settle",{exact:true}).tap();
  await app.screenshot("after-unsettle");
  const afterUnsettle = await browser.evaluate(() => ({
    canArm: [...document.querySelectorAll("button")].some(b => b.textContent === "Resume at reset"),
    stillArmed: [...document.querySelectorAll("button")].some(b => b.textContent === "Cancel auto-resume"),
  }));
  while (Date.now() < reset*1000+5000) {
    await browser.evaluate(ms => new Promise(resolve => setTimeout(resolve, ms)), Math.min(25000,reset*1000+5000-Date.now()));
  }
  await app.screenshot("after-reset");
  const turnRequests = (await readFile(join(T3_HOME,"proof-turns.ndjson"),"utf8")).trim().split("\n").map(line => JSON.parse(line));
  await writeFile(join(T3_HOME,"proof-observed.json"),JSON.stringify({afterUnsettle,refusal,turnRequests},null,2));
  expect(afterUnsettle.canArm).toBe(true);
  expect(afterUnsettle.stillArmed).toBe(false);
  expect(turnRequests.length).toBe(1);
  expect(JSON.stringify(refusal)).toContain("The thread is settled. Un-settle it to resume at the reset.");
});
