import { expect } from 'e2e';
import { execFileSync } from 'node:child_process';
import { writeFileSync, unlinkSync, readFileSync, existsSync } from 'node:fs';
import { createServer } from 'node:http';
import { test, IOS } from './t3-mobile.ts';
import { APP_DIR, MOBILE_T3_HOME, MOBILE } from '../e2e.config.ts';

const scratch = process.env.T3_PROOF_SCRATCH!;
const side = process.env.T3_PROOF_SIDE!;
const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
const cli = (...args: string[]) => execFileSync(process.execPath, ['apps/server/src/bin.ts', ...args, '--base-dir', MOBILE_T3_HOME], {cwd:APP_DIR, encoding:'utf8'}).trim();

test('A stalled first preview capture reaches startup error and reconnects', IOS, async ({t3, app, screen}) => {
 const page = createServer((_req,res)=>{res.setHeader('Content-Type','text/html');res.end('<html><head><title>Capture recovery</title><meta name="viewport" content="width=device-width"></head><body style="background:#eef4f8;font:24px system-ui;padding:32px;color:#17344b"><h1>Capture recovery</h1><p>The preview is live.</p><button style="font:24px system-ui;padding:20px" onclick="this.textContent=\'Interaction received\'">Test interaction</button></body></html>');});
 await new Promise<void>(r=>page.listen(14852,'0.0.0.0',r));
 let ws: WebSocket | undefined;
 let probe: WebSocket | undefined;
 const observed: Record<string,unknown> = {side};
 const frames: Array<{label:string,path:string,time:number}> = [];
 const snap = async (label:string) => {
  const path=await app.screenshot(label);
  frames.push({label,path,time:Date.now()});
  return path;
 };
 const recordWait = async (ms:number,label:string) => {
  const until=Date.now()+ms;
  let index=0;
  do {
   const started=Date.now();
   await snap(label+'-'+String(++index).padStart(3,'0'));
   await wait(Math.max(0,Math.min(until-Date.now(),1000-(Date.now()-started))));
  } while(Date.now()<until);
 };
 const stableShot = async (label:string) => {
  for(let i=0;i<5;i++) {
   const a=await snap(label+'-settle-a');
   await wait(500);
   const b=await snap(label+'-settle-b');
   if(readFileSync(a).equals(readFileSync(b))) { await snap(label); return; }
  }
  observed[label+'Dynamic']=true;
  await snap(label);
 };
 try {
  const project = await t3.project('Preview recovery');
  await t3.thread({project,title:'Stalled capture',messages:['Prepare preview']});
  const listed = JSON.parse(cli('thread','list','--project',project,'--json'));
  writeFileSync(scratch+'/threads-shape.json', JSON.stringify(listed));
  const threads = Array.isArray(listed) ? listed : listed.threads;
  const threadId = threads.find((t:any)=>t.title==='Stalled capture').threadId;
  const token = cli('auth','session','issue','--token-only');
  // The RPC accepts the scoped bearer as a protocol header through a short-lived ticket.
  const ticketResponse = await fetch(`http://127.0.0.1:${MOBILE.serverPort}/api/auth/websocket-ticket`, {method:'POST',headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json'},body:'{}'});
  const ticket = await ticketResponse.json() as any;
  writeFileSync(scratch+'/ticket-shape.json',JSON.stringify({status:ticketResponse.status,keys:Object.keys(ticket)}));
  ws = new WebSocket(`ws://127.0.0.1:${MOBILE.serverPort}/ws?orchestrationProtocol=2&wsTicket=${encodeURIComponent(ticket.ticket ?? ticket.wsTicket)}`);
  await new Promise<void>((resolve,reject)=>{ws!.onopen=()=>resolve();ws!.onerror=reject;});
  let id=0;
  const rpc = (tag:string,payload:unknown) => new Promise<any>((resolve,reject)=>{
   const rid=String(++id);
   const timer=setTimeout(()=>reject(new Error('RPC timed out: '+tag)),20000);
   const listener=(event:MessageEvent)=>{for(const v of String(event.data).split('\n').filter(Boolean)){const m=JSON.parse(v);if(m.requestId===rid&&m._tag==='Exit'){clearTimeout(timer);ws!.removeEventListener('message',listener);if(m.exit._tag==='Success')resolve(m.exit.value);else reject(new Error(JSON.stringify(m.exit)));}}};
   ws!.addEventListener('message',listener);
   ws!.send(JSON.stringify({_tag:'Request',id:rid,tag,payload,headers:[]}));
  });
  await t3.open();
  await screen.getByText('Stalled capture',{exact:false}).first().tap();
  await wait(3000);
  await snap('ready-before-fault');
  writeFileSync(scratch+'/stall','1');
  await rpc('preview.open',{threadId,url:'http://127.0.0.1:14852',reveal:true,runtime:'server'});
  const waitStart = Date.now();
  while(!existsSync(scratch+'/cdp.jsonl') || !readFileSync(scratch+'/cdp.jsonl','utf8').split('\n').filter(Boolean).map(s=>JSON.parse(s)).some(e=>e.side===side && e.event==='viewer-jpeg-unanswered' && e.time>=waitStart-1000)) {
   if(Date.now()-waitStart>60000) { await snap('setup-no-cdp'); writeFileSync(scratch+'/'+side+'-opening-tabs.json',JSON.stringify(await rpc('preview.list',{threadId}),null,2)); throw new Error('CDP fixture did not receive the first JPEG'); }
   await wait(100);
  }
  const media = await (await fetch(`http://127.0.0.1:${MOBILE.serverPort}/api/auth/websocket-ticket/media`, {method:'POST',headers:{Authorization:`Bearer ${token}`}})).json() as any;
  const tabs = await rpc('preview.list',{threadId});
  const tabId=(Array.isArray(tabs)?tabs:tabs.sessions)[0].tabId;
  const probeStarted=Date.now();
  probe=new WebSocket(`ws://127.0.0.1:${MOBILE.serverPort}/api/preview-stream/ws?threadId=${threadId}&tabId=${tabId}&maxWidth=777&maxHeight=600&quality=70&interactive=false&wsTicket=${encodeURIComponent(media.ticket)}`);
  probe.binaryType='arraybuffer';
  probe.onopen=()=>{observed.probeSocketOpenedMs=Date.now()-probeStarted;};
  probe.onmessage=(e)=>{if(typeof e.data!=='string') {observed.probeFrameMs=Date.now()-probeStarted;probe!.send(JSON.stringify({type:'ack'}));probe!.close();}};
  probe.onerror=()=>{observed.probeError=true;};
  await recordWait(18000,'compact-wait');
  observed.probeOpened=probe.readyState!==WebSocket.CONNECTING;
  await stableShot('compact-after-startup-deadline');
  await screen.getByRole('button', 'Open browser, Capture recovery').tap();
  await recordWait(18000,'full-wait');
  await stableShot('full-after-startup-deadline');
  const reconnect=screen.getByRole('button','Reconnect');
  observed.reconnectVisible=await reconnect.isVisible();
  unlinkSync(scratch+'/stall');
  if(observed.reconnectVisible) {
   await reconnect.tap();
   await recordWait(5000,'reconnecting');
   await stableShot('reconnected');
   await screen.tapAt({x:160,y:490});
   await recordWait(2000,'interaction');
   await stableShot('interaction-received');
  } else {
   await recordWait(5000,'fault-removed');
   await stableShot('fault-removed-no-reconnect');
  }
  expect(observed.reconnectVisible).toBe(true);
  expect(observed.probeFrameMs !== undefined).toBe(true);
 } finally {
  writeFileSync(scratch+'/'+side+'-frames.json',JSON.stringify(frames,null,2));
  writeFileSync(scratch+'/'+side+'-observed.json',JSON.stringify(observed,null,2));
  probe?.close();
  ws?.close();
  try {unlinkSync(scratch+'/stall');} catch {}
  page.closeAllConnections();page.close();
 }
});
