import { expect } from "e2e";
import { readFile, writeFile } from "node:fs/promises";
import { APP_DIR } from "../e2e.config.ts";
import { test, WEB } from "./t3.ts";

// A deterministic replacement for the dev replay model. It performs real MCP HTTP calls;
// all browser automation, timeouts, tab queueing and UI rendering remain the app's own.
const agent = String.raw`
import { createInterface } from 'node:readline';
import { createServer } from 'node:http';
import { randomUUID } from 'node:crypto';
let sessionId, mcp, sid, n = 0;
const send = x => process.stdout.write(JSON.stringify({jsonrpc:'2.0', ...x})+'\n');
const update = text => send({method:'session/update',params:{sessionId,update:{sessionUpdate:'agent_message_chunk',content:{type:'text',text}}}});
async function rpc(method, params) {
 const headers = Object.fromEntries((mcp.headers || []).map(h=>[h.name,h.value]));
 Object.assign(headers, {'Content-Type':'application/json',Accept:'application/json, text/event-stream'});
 if(sid) headers['mcp-session-id']=sid;
 const res=await fetch(mcp.url,{method:'POST',headers,body:JSON.stringify({jsonrpc:'2.0',id:++n,method,params})});
 if(res.headers.get('mcp-session-id')) sid=res.headers.get('mcp-session-id');
 const raw=await res.text();
 let body; try {body=JSON.parse(raw)} catch {body=raw.split('\n').filter(l=>l.startsWith('data:')).map(l=>JSON.parse(l.slice(5))).find(x=>x.id===n)}
 if(body?.error) throw Error(JSON.stringify(body.error));
 return body?.result;
}
const call = (name,args) => rpc('tools/call',{name,arguments:args});
const value = result => result?.structuredContent?.value;
const diagnostic = result => JSON.stringify(result);
let queueStarted;
const server=createServer((req,res)=>{
 if(req.url==='/queue-start'){queueStarted?.();res.end('ok');return}
 const mode=req.url.slice(1);
 res.setHeader('Content-Type','text/html');res.end('<!doctype html><title>Click budget fixture</title><style>body{font:20px system-ui;padding:32px}button{font:inherit;padding:12px}</style><h1>Click budget fixture</h1><p id="state">Waiting for Save</p><script>window.clicks=0;window.started=performance.now();const b=document.createElement("button");b.id="save";b.disabled=true;b.textContent="Save";b.onclick=()=>{window.clicks++;document.querySelector("#state").textContent="Save clicked"};setTimeout(()=>document.body.append(b),'+(mode==='delayed'?6000:mode==='late'?1000:0)+');'+(mode==='delayed'?'':'setTimeout(()=>{b.disabled=false;document.querySelector("#state").textContent="Save enabled"},'+(mode==='late'?2500:2600)+');')+'</script>');
});
await new Promise(r=>server.listen(0,'127.0.0.1',r));
const url='http://127.0.0.1:'+server.address().port;
async function proof(){
 await rpc('initialize',{protocolVersion:'2024-11-05',capabilities:{},clientInfo:{name:'click-proof',version:'1'}});
 update('Running three real preview click cases: delayed disabled, late enable, and same-tab queue.\n\n');
 const opened=await call('preview_open',{url:url+'/delayed',open:true});
 const tabId=opened?.structuredContent?.tabId;
 update('1. Save is inserted disabled at 6 seconds. Click budget: 8000 ms.\n\n');
 let t=performance.now();
 const delayed=await call('preview_act',{action:'click',selector:'#save',timeoutMs:8000,tabId});
 const delayedMs=Math.round(performance.now()-t);
 update('Delayed disabled result ('+delayedMs+' ms): '+diagnostic(delayed)+'\n\n');
 await call('preview_navigate',{url:url+'/late',tabId});
 update('2. Save inserts at 1 second and enables at 2.5 seconds. Click budget: 2000 ms.\n\n');
 t=performance.now();
 const late=await call('preview_act',{action:'click',selector:'#save',timeoutMs:2000,tabId});
 const lateMs=Math.round(performance.now()-t);
 const lateState=await call('preview_evaluate',{expression:'({clicks:window.clicks,age:Math.round(performance.now()-window.started)})',tabId});
 update('Late enable result ('+lateMs+' ms); clicks='+value(lateState)?.clicks+'\n\n');
 await call('preview_navigate',{url:url+'/queue',tabId});
 update('3. Hold this tab with a 1400 ms evaluation; queue a 2000 ms click behind it. Save enables at 2600 ms.\n\n');
 const started=new Promise(r=>queueStarted=r);
 const blocker=call('preview_evaluate',{expression:'fetch("/queue-start").then(()=>new Promise(r=>setTimeout(()=>r({heldMs:1400}),1400)))',tabId});
 await started;
 t=performance.now();
 const queued=await call('preview_act',{action:'click',selector:'#save',timeoutMs:2000,tabId});
 const queuedMs=Math.round(performance.now()-t);
 await blocker;
 const queuedState=await call('preview_evaluate',{expression:'({clicks:window.clicks,age:Math.round(performance.now()-window.started)})',tabId});
 const results={delayedMs,delayedReason:diagnostic(delayed).includes('element is not enabled'),lateMs,lateRejected:!!late?.isError,lateClicks:value(lateState)?.clicks,queuedMs,queuedRejected:!!queued?.isError,queuedClicks:value(queuedState)?.clicks,queuedReason:diagnostic(queued).includes('element is not enabled')};
 update('### Click proof results\n\n| Case | Elapsed | Clicks | Outcome |\n|---|---:|---:|---|\n| Disabled at 6s; 8s budget | '+delayedMs+' ms | 0 | '+(results.delayedReason?'element is not enabled':'Generic broker timeout')+' |\n| Enable at 2.5s; 2s budget | '+lateMs+' ms | '+results.lateClicks+' | '+(results.lateRejected?'Rejected':'Clicked after budget')+' |\n| Queue 1.4s; 2s total budget | '+queuedMs+' ms | '+results.queuedClicks+' | '+(results.queuedRejected?'Rejected: element is not enabled':'Clicked after budget')+' |\n\nCLICK_PROOF_COMPLETE\n\n');
 update('PROOF_JSON '+JSON.stringify(results));
}
async function handle(m){const p=m.params||{};switch(m.method){
 case 'initialize':return {protocolVersion:1,agentCapabilities:{loadSession:true,mcpCapabilities:{http:true},promptCapabilities:{embeddedContext:true}},authMethods:[],agentInfo:{name:'click-proof-replay',version:'1'}};
 case 'session/new':case 'session/load':sessionId=p.sessionId||randomUUID();mcp=p.mcpServers?.find(s=>s.type==='http') || {url:process.env.T3_ACP_MCP_ENDPOINT,headers:[{name:'Authorization',value:process.env.T3_ACP_MCP_AUTHORIZATION}]};return {sessionId,models:{currentModelId:'multi-turn',availableModels:[{modelId:'multi-turn',name:'Click proof'}]}};
 case 'session/set_model':case 'authenticate':case 'session/set_mode':return {};
 case 'session/prompt':if(p.prompt?.some(x=>x.text?.includes('Run click proof'))){try{await proof()}catch(e){update('PROOF_SETUP_ERROR '+e.message)}}else update('Ready for click budget proof.');return {stopReason:'end_turn'};
 default:return {};
}}
createInterface({input:process.stdin}).on('line',line=>{let m;try{m=JSON.parse(line)}catch{return}if(m.id==null)return;handle(m).then(result=>send({id:m.id,result}),e=>send({id:m.id,error:{code:-32603,message:String(e)}}))});
`;

test("Preview clicks respect one budget including same-tab queueing and explain failure", WEB, async ({t3,app,screen,browser})=>{
 const path = `${APP_DIR}/apps/server/scripts/dev-replay-agent.ts`;
 const original=await readFile(path,'utf8');
 await writeFile(path,agent);
 try {
  const project=await t3.project('click-budget-proof');
  const thread=await t3.thread({project,title:'Preview click budget proof',messages:['Seed proof']});
  await t3.open(thread);
  await screen.getByRole('textbox').fill('Run click proof');
  await screen.getByRole('textbox').press('Enter');
  await screen.getByText('CLICK_PROOF_COMPLETE',{exact:false}).waitFor({state:'visible',timeout:90000});
  await screen.getByRole('button','Stop generation').waitFor({state:'hidden',timeout:30000});
  // The sidebar receives its final activity event independently of the final message.
  await screen.getByText(/^Working \d/).waitFor({state:'hidden',timeout:30000});
  await browser.evaluate(()=>{document.querySelectorAll('[data-testid="chat-scroll-container"]').forEach(e=>e.scrollTop=e.scrollHeight)});
  await app.screenshot('click-budget-result');
  const text=await screen.getByText('PROOF_JSON ',{exact:false}).textContent();
  const results=JSON.parse(text.slice(text.indexOf('PROOF_JSON ')+11));
  expect(results.delayedReason).toBe(true);
  expect(results.delayedMs).toBeLessThan(8800);
  expect(results.lateRejected).toBe(true);
  expect(results.lateClicks).toBe(0);
  expect(results.queuedRejected).toBe(true);
  expect(results.queuedClicks).toBe(0);
  expect(results.queuedReason).toBe(true);
 } finally {await writeFile(path,original)}
});
