import { expect } from 'e2e';
import { test, WEB, makeState, runPromise } from './t3.ts';
import { APP_DIR, T3_HOME } from '../e2e.config.ts';
import { spawn, execFileSync } from 'node:child_process';
import fs from 'node:fs';
import { createRequire } from 'node:module';
const scratch=process.env.T3_PROOF_SCRATCH ?? '/tmp/remote-follower-proof';
const require=createRequire(`${APP_DIR}/apps/server/package.json`);
const {WebSocket}=require('ws');
const pause=(ms:number)=>new Promise(r=>setTimeout(r,ms));
const cli=(home:string,args:string[])=>execFileSync(process.execPath,['apps/server/src/bin.ts',...args,'--base-dir',home],{cwd:APP_DIR,env:process.env,encoding:'utf8'}).trim();
async function until<T>(f:()=>T|Promise<T>, description:string):Promise<T>{for(let n=0;n<600;n++){try {const v=await f();if(v)return v;}catch{}await pause(100);}throw new Error('STEP_TIMEOUT: '+description);}
async function rpc(origin:string,token:string,tag:string,payload:unknown){
 const ws=new WebSocket(origin.replace('http','ws')+'/ws?orchestrationProtocol=2',{headers:{Authorization:'Bearer '+token}});
 return await new Promise<any>((resolve,reject)=>{ws.on('error',reject);ws.on('open',()=>ws.send(JSON.stringify({_tag:'Request',id:'1',tag,payload,headers:[]})));ws.on('message',(raw:any)=>{for(const m of String(raw).trim().split('\n').map(JSON.parse)){if(m._tag==='Exit'&&m.requestId==='1'){ws.close();if(m.exit._tag==='Success')resolve(m.exit.value);else reject(new Error(JSON.stringify(m)));}}});setTimeout(()=>{ws.close();reject(new Error('RPC timeout '+tag));},20000);});
}
async function mcp(credential:any,name:string,args:unknown){
 const headers:any={Authorization:credential.authorization,'Content-Type':'application/json',Accept:'application/json, text/event-stream'};
 if(!credential.session){
 const initialized=await fetch(credential.endpoint,{method:'POST',headers,body:JSON.stringify({jsonrpc:'2.0',id:0,method:'initialize',params:{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'remote-follower-proof',version:'1'}}})});
 credential.session=initialized.headers.get('mcp-session-id');
 if(!credential.session)throw new Error('MCP initialization failed: '+await initialized.text());
 headers['MCP-Session-Id']=credential.session;
 await fetch(credential.endpoint,{method:'POST',headers,body:JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})});
 }
 headers['MCP-Session-Id']=credential.session;
 const response=await fetch(credential.endpoint,{method:'POST',headers,body:JSON.stringify({jsonrpc:'2.0',id:1,method:'tools/call',params:{name,arguments:args}})});
 const raw=await response.text();const data=raw.startsWith('event:')||raw.startsWith('data:')?JSON.parse(raw.split('\n').find(x=>x.startsWith('data:'))!.slice(5)):JSON.parse(raw);
 if(data.error||data.result?.isError)throw new Error(JSON.stringify(data));return data.result?.structuredContent;
}

test('remote follower defects settle the task and wake the parent',WEB,async({t3,app,screen,browser})=>{
 const defectCount=Number(process.env.T3_PROOF_DEFECT_COUNT??3);
 fs.writeFileSync(scratch+'/faults.json','{}');
 fs.writeFileSync(scratch+'/release-all','false');
 const remoteHome=scratch+'/remote-'+Date.now();
 const output=fs.openSync(scratch+'/remote-server-'+Date.now()+'.log','w');
 const remote=spawn(process.execPath,[new URL('./start-proof-app.ts',import.meta.url).pathname,'web',remoteHome,'127.0.0.1','15673'],{cwd:APP_DIR,env:{...process.env,T3CODE_DEV_SEED:'0'},stdio:['ignore',output,output],detached:false});
 fs.writeFileSync(scratch+'/remote-pid',String(remote.pid));
 try {
 await until(()=>fs.existsSync(remoteHome+'/pairing-path'),'remote server startup');
 const remoteOrigin=JSON.parse(fs.readFileSync(remoteHome+'/userdata/server-runtime.json','utf8')).origin;
 const remoteState=makeState(remoteHome);
 const remoteProject=await remoteState.state.project('proof-remote');
 const remoteInfo=await (await fetch(remoteOrigin+'/.well-known/t3/environment')).json();
 const token=cli(T3_HOME,['auth','session','issue','--token-only']);
 const pair=JSON.parse(cli(remoteHome,['auth','pairing','create','--json']));
 fs.writeFileSync(scratch+'/pair-shape.json',JSON.stringify(Object.keys(pair)));
 const runtime=JSON.parse(fs.readFileSync(T3_HOME+'/userdata/server-runtime.json','utf8'));
 await rpc(runtime.origin,token,'server.updateSettings',{patch:{linkedEnvironmentsEnabled:true}});
 await rpc(remoteOrigin,cli(remoteHome,['auth','session','issue','--token-only']),'server.updateSettings',{patch:{linkedEnvironmentsEnabled:true}});
 await rpc(runtime.origin,token,'peerLinks.link',{url:remoteOrigin,pairingCode:pair.credential,access:'full-access',expectedLabel:'Remote proof'});
 const listed=await (await fetch(remoteOrigin+'/api/projects',{headers:{Authorization:'Bearer '+cli(remoteHome,['auth','session','issue','--token-only']),'x-t3-orchestration-protocol':'2'}})).json();
 fs.writeFileSync(scratch+'/snapshot-shape.json',JSON.stringify(listed));
 const projectId=listed.projects.find((p:any)=>p.workspaceRoot===remoteProject)?.id;
 const project=await t3.project('proof-parent');
 const started=JSON.parse(cli(T3_HOME,['thread','start',project,'Delegate the remote task and wait for its result.','--title','Remote follower proof','--provider','devReplay','--model','multi-turn','--json']));
 const parent=started.threadId;
 await t3.open({threadId:parent});
 await screen.getByRole('textbox').waitFor({state:'visible',timeout:120000});
 const credential=await until(()=>{const p=scratch+'/credential-'+parent+'.json';return fs.existsSync(p)&&JSON.parse(fs.readFileSync(p,'utf8'));},'provider credential');
 fs.writeFileSync(scratch+'/faults.json',JSON.stringify({[parent]:{remaining:defectCount}}));
 await mcp(credential,'orchestrator_capabilities',{});
 const task=await mcp(credential,'delegate_task',{task:'Complete the replayed remote task.',title:'Remote delegated task',target:{environmentId:remoteInfo.environmentId,projectId,providerInstanceId:'devReplay',model:'multi-turn'},mode:'async',clientRequestId:'proof-remote'});
 fs.writeFileSync(scratch+'/delegated.json',JSON.stringify({parent,task}));
 fs.writeFileSync(scratch+'/release-all','true');
 // The unchanged production backoff is 2s then 4s; give both sides the same observation window.
 await pause(20000);
 const read=async(origin:string,bearer:string,threadId:string)=>{
 const res=await fetch(origin+'/api/orchestration/threads/'+encodeURIComponent(threadId),{headers:{Authorization:'Bearer '+bearer,'x-t3-orchestration-protocol':'2'}});
 if(!res.ok)throw new Error('Reading real thread snapshot failed: '+res.status);
 return (await res.json()).projection;
 };
 const parentRead=await read(runtime.origin,token,parent);
 const remoteRead=await read(remoteOrigin,cli(remoteHome,['auth','session','issue','--token-only']),task.remoteChild.threadId);
 const status=parentRead.subagents.find((a:any)=>a.id===task.taskId);
 const sha=execFileSync('git',['rev-parse','HEAD'],{cwd:APP_DIR,encoding:'utf8'}).trim();
 fs.writeFileSync(scratch+'/observed-'+sha+'-'+defectCount+'.json',JSON.stringify({sha,task:{status:status.status,summary:status.result},remote:{threadId:remoteRead.thread.id,runs:remoteRead.runs.map((r:any)=>({status:r.status}))},parent:{runs:parentRead.runs.map((r:any)=>({status:r.status})),messages:parentRead.messages.filter((m:any)=>m.role==='assistant').map((m:any)=>m.text??m.content)},injectedDefects:fs.readFileSync(scratch+'/attempts.ndjson','utf8').trim().split('\n').map(JSON.parse).filter((a:any)=>a.threadId===parent).length},null,2));
 const work=screen.getByRole('button',/^Worked for/).first();
 if(await work.isVisible())await work.tap();
 await browser.evaluate(()=>{document.getAnimations().filter(a=>a.effect?.getTiming().iterations===Infinity).forEach(a=>a.pause());return null;});
 const artifactRoot=new URL('../.e2e/',import.meta.url).pathname;
 const captureBytes=(capture:string)=>{
 if(fs.existsSync(capture))return fs.readFileSync(capture);
 const files=fs.readdirSync(artifactRoot,{recursive:true}).map(String).filter(p=>p.endsWith('/'+capture)).map(p=>artifactRoot+p).sort((a,b)=>fs.statSync(b).mtimeMs-fs.statSync(a).mtimeMs);
 if(!files[0])throw new Error('Capture not found: '+capture);
 return fs.readFileSync(files[0]);
 };
 let stable=false;
 for(let n=0;n<8;n++){
 const first=await app.screenshot('stability-first');
 await pause(500);
 const second=await app.screenshot('remote-follower-result');
 if(captureBytes(first).equals(captureBytes(second))){stable=true;break;}
 }
 if(!stable)throw new Error('Screenshots never stabilized');
 await browser.evaluate(()=>{document.getAnimations().filter(a=>a.playState==='paused').forEach(a=>a.play());return null;});
 if(defectCount===3) await expect(screen.getByText('failed repeatedly', {exact:false}).last()).toBeVisible();
 else await expect(screen.getByText('first fixture turn complete',{exact:false}).first()).toBeVisible();
 await expect(screen.getByText('Parent resumed after delegated task completion.',{exact:false})).toBeVisible();
 } finally {
 remote.kill('SIGTERM');
 await new Promise<void>(resolve=>{remote.once('exit',()=>resolve());setTimeout(resolve,10000);});
 fs.closeSync(output);
 }
});
