import { expect } from "e2e";
import fs from "node:fs";
import {setTimeout as delay} from "node:timers/promises";
import { PNG } from "pngjs";
import { test, WEB } from "./t3.ts";
import { T3_HOME } from "../e2e.config.ts";

// Synthetic clock advances inactivity without waiting a day. Only the server is shifted.
const clock = "/tmp/settlement-proof/clock-offset";
test("Hidden children follow parent lifecycle and retain settlement guards", WEB, async ({t3, app, browser, screen}) => {
 const capture = async(label:string) => {
   await browser.evaluate(()=>{const style=document.createElement('style');style.textContent='*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}';document.head.append(style);});
   for(let attempt=0;attempt<4;attempt++) {
     const a=await app.screenshot(`${label}-stability-a-${attempt}`);
     await delay(500);
     const b=await app.screenshot(`${label}-stability-b-${attempt}`);
     if(PNG.sync.read(fs.readFileSync(a)).data.equals(PNG.sync.read(fs.readFileSync(b)).data)) return app.screenshot(label);
   }
   throw new Error(`The view did not stabilize: ${label}`);
 };
 fs.writeFileSync(clock, "0");
 const project = await t3.project("Settlement demonstration");
 await t3.thread({project,title:"Parent awaiting approval",model:"question",messages:["Wait for my approval while the child completes"]});
 await t3.thread({project,title:"Independent inactivity control",messages:["Finish the control task"]});
 await t3.open();
 await screen.getByRole('button','Parent awaiting approval, Input, Settlement demonstration').textContent();
 const origin = await browser.evaluate(()=>location.origin);
 const pairing = new URL(fs.readFileSync(`${T3_HOME}/pairing-path`, 'utf8'),origin);
 const credential = new URLSearchParams(pairing.hash.slice(1)).get('token') ?? pairing.searchParams.get('token')!;
 const auth = await fetch(`${origin}/oauth/token`,{method:'POST',body:new URLSearchParams({grant_type:'urn:ietf:params:oauth:grant-type:token-exchange',subject_token:credential,subject_token_type:'urn:t3:params:oauth:token-type:environment-bootstrap',requested_token_type:'urn:ietf:params:oauth:token-type:access_token'})}).then(r=>r.json());
 const bearer = auth.access_token;
 if(!bearer) throw new Error('Synthetic driver could not authenticate');
 const rpc = async (tag:string, payload:any):Promise<any> => browser.evaluate(async ({tag,payload,bearer}) => {
   const ticket = await fetch('/api/auth/websocket-ticket',{method:'POST',headers:{authorization:`Bearer ${bearer}`,'x-t3-orchestration-protocol':'2'}}).then(r=>r.json());
   const descriptor = await fetch('/.well-known/t3/environment').then(r=>r.json());
   const socket = new WebSocket(`${location.origin.replace(/^http/,'ws')}/ws?orchestrationProtocol=${descriptor.orchestrationProtocolVersion ?? 2}&wsTicket=${encodeURIComponent(ticket.ticket)}`);
   return await new Promise((resolve,reject)=>{
     const timer = setTimeout(()=>{socket.close();reject(new Error('RPC response timeout '+tag));},20000);
     socket.onopen=()=>socket.send(JSON.stringify({_tag:'Request',id:'1',tag,payload,headers:[]}));
     socket.onerror=()=>{clearTimeout(timer);reject(new Error('RPC connection failed'));};
     socket.onmessage=e=>{ const data=JSON.parse(e.data); for(const m of Array.isArray(data)?data:[data]) {
       if(m._tag==='Ping') socket.send(JSON.stringify({_tag:'Pong'}));
       if(m._tag==='Exit' && String(m.requestId)==='1'){clearTimeout(timer);socket.close();m.exit._tag==='Success'?resolve(m.exit.value):reject(new Error(JSON.stringify(m.exit)));}
     }};
   });
 },{tag,payload,bearer});
 const shell = await fetch(`${origin}/api/orchestration/shell`,{headers:{authorization:`Bearer ${bearer}`,'x-t3-orchestration-protocol':'2'}}).then(r=>r.json());
 fs.writeFileSync('/tmp/settlement-proof/shell-private.json',JSON.stringify(shell));
 const threads = shell.threads;
 const parent = threads.find((t:any)=>t.title==='Parent awaiting approval');
 const control = threads.find((t:any)=>t.title==='Independent inactivity control');
 const command = (type:string,threadId:string,extra:any={}) => rpc('orchestration.dispatchCommand',{type,threadId,commandId:crypto.randomUUID(),...extra});
 const projection = (threadId:string) => rpc('orchestration.getThreadProjection',{threadId});
 const p = await projection(parent.id);
 const run = p.runs.at(-1);
 await rpc('orchestration.dispatchCommand',{
   type:'delegated_task.request',commandId:crypto.randomUUID(),parentThreadId:parent.id,parentRunId:run.id,parentNodeId:run.rootNodeId,
   task:'Complete this synthetic child task',title:'Hidden idle child',modelSelection:{instanceId:'devReplay',model:'multi-turn'},runtimeMode:'full-access',interactionMode:'default',createdBy:'user',creationSource:'mcp',completionWake:'settled_only'
 });
 let childId:string='';
 await expect.poll(async()=>{
   const parentProjection = await projection(parent.id);
   childId = parentProjection.subagents[0]?.childThreadId ?? '';
   if(!childId)return false;
   const c = await projection(childId);
   return c.runs.some((r:any)=>r.status==='completed');
 }).toBe(true);
 await rpc('server.updateSettings',{patch:{sidebarAutoSettleAfterDays:1,sidebarAutoSettleOnMerge:false}});
 fs.writeFileSync(clock,String(2*86400000));
 await rpc('server.updateSettings',{patch:{sidebarAutoSettleAfterDays:2}});
 await rpc('server.updateSettings',{patch:{sidebarAutoSettleAfterDays:1}});
 await expect.poll(async()=> {const c=await projection(control.id);fs.writeFileSync('/tmp/settlement-proof/control-private.json',JSON.stringify(c));return c.thread.settledOverride;}).toBe('settled');
 const held = (await projection(childId)).thread.settledOverride !== 'settled';
 const descriptor = await browser.evaluate(async()=>fetch('/.well-known/t3/environment').then(r=>r.json()));
 await app.open(`/threads/${descriptor.environmentId}/${childId}`);
 await screen.getByText('Complete this synthetic child task',{exact:false}).first().tap();
 await capture('child-after-inactivity-parent-still-waiting');
 expect.soft(held).toBe(true);
 const settled = async (id:string) => (await projection(id)).thread.settledOverride === 'settled';
 const awaitSettled = async(id:string) => {try { await expect.poll(()=>settled(id),{timeout:4000}).toBe(true); } catch {} return settled(id);};
 const show = async (id:string, label:string, prompt='Complete this synthetic child task') => {
   await app.open(`/threads/${descriptor.environmentId}/${id}`);
   await screen.getByText(prompt,{exact:false}).first().tap();
   await capture(label);
 };
 await rpc('server.updateSettings',{patch:{sidebarAutoSettleAfterDays:null,sidebarAutoSettleOnMerge:false}});
 const all:any[] = [{case:'independent inactivity while parent awaits input',passed:held}];
 const freshParent = async(title:string) => {
   await t3.thread({project,title,model:'question',messages:['Wait for my answer while children complete']});
   const snapshot = await fetch(`${origin}/api/orchestration/shell`,{headers:{authorization:`Bearer ${bearer}`,'x-t3-orchestration-protocol':'2'}}).then(r=>r.json());
   return snapshot.threads.find((t:any)=>t.title===title);
 };
 const delegate = async(owner:any,title:string,model='multi-turn') => {
   const ownerProjection=await projection(owner.id);
   const active=ownerProjection.runs.at(-1);
   const id=crypto.randomUUID();
   await rpc('orchestration.dispatchCommand',{type:'delegated_task.request',commandId:id,parentThreadId:owner.id,parentRunId:active.id,parentNodeId:active.rootNodeId,
     task:'Complete this synthetic child task',title,modelSelection:{instanceId:'devReplay',model},runtimeMode:'full-access',interactionMode:'default',createdBy:'user',creationSource:'mcp',completionWake:'settled_only'});
   const child=`thread:delegated-task:${id}`;
   await expect.poll(async()=>{try {const c=await projection(child);return model==='question' ? c.runtimeRequests.some((r:any)=>r.status==='pending') : c.runs.some((r:any)=>r.status==='completed');}catch{return false;}}).toBe(true);
   return child;
 };
 const completeParent = async(owner:any) => {
   const p=await projection(owner.id);
   const request=p.runtimeRequests.find((r:any)=>r.status==='pending');
   await command('runtime-request.respond',owner.id,{requestId:request.id,answers:{schema_vs_ui_flexibility:'Strict schemas (Recommended)'}});
   await expect.poll(async()=> (await projection(owner.id)).runs.at(-1).status).toBe('completed');
 };
 // Each lifecycle case starts with a fresh idle child, with automatic settlement disabled.
 for (const lifecycle of ['settle','archive','delete']) {
   const owner=await freshParent(`Parent to ${lifecycle}`);
   const child=await delegate(owner,`Child follows parent ${lifecycle}`);
   await show(child,`${lifecycle}-before-parent-action`);
   if(lifecycle==='settle') await completeParent(owner);
   await command(`thread.${lifecycle}`,owner.id);
   const follows=await awaitSettled(child);
   all.push({case:`parent ${lifecycle}, auto-settlement disabled`,passed:follows});
   expect.soft(follows).toBe(true);
   await show(child,`${lifecycle}-after-parent-action`);
 }
 const guardParent=await freshParent('Parent of protected children');
 const normal=await delegate(guardParent,'Ordinary idle child');
 const pinned=await delegate(guardParent,'Pinned child');
 await command('thread.pin',pinned);
 const opted=await delegate(guardParent,'Auto-settlement disabled child');
 await command('thread.auto-settle.set',opted,{enabled:false});
 const pending=await delegate(guardParent,'Child waiting for input','question');
 const queued=await delegate(guardParent,'Child with held queued follow-up','question');
 // Interrupt the first run and hold its follow-up, leaving queue protection without a pending request.
 await command('message.dispatch',queued,{messageId:crypto.randomUUID(),text:'Queued follow-up must remain pending',attachments:[],createdBy:'user',creationSource:'web',dispatchMode:{type:'queue_after_active'}});
 const queuedRun=(await projection(queued)).runs.find((r:any)=>['running','waiting','starting'].includes(r.status));
 await command('run.interrupt',queued,{runId:queuedRun.id,holdQueue:true});
 await expect.poll(async()=> {const c=await projection(queued);return c.runtimeRequests.every((r:any)=>r.status!=='pending') && c.runs.some((r:any)=>r.status==='queued');}).toBe(true);
 await command('thread.archive',guardParent.id);
 await awaitSettled(normal);
 await show(normal,'guard-unprotected-control');
 for(const [name,id] of [['pinned',pinned],['opt-out',opted],['pending-input',pending],['held-queue',queued]]) {
   const kept=!(await settled(id));
   all.push({case:`${name} guard after parent settlement`,passed:kept});
   expect.soft(kept).toBe(true);
   await show(id,`guard-${name}`,'Complete this synthetic child task');
   if(name==='pinned' || name==='opt-out') {
     const title=name==='pinned'?'Pinned child':'Auto-settlement disabled child';
     await screen.getByRole('button',`Thread actions for ${title}`).tap();
     if(name==='opt-out') await screen.getByRole('menuitem','Auto-settle behavior').tap();
     await capture(`guard-${name}-setting-visible`);
     await browser.keyboard.press('Escape');
     await browser.keyboard.press('Escape');
   }
 }
 // The owning parent is still mid-turn, blocked on a real input request, when the child settles.
 const hookParent=await freshParent('Active parent owns shared-worktree hook');
 await command('thread.metadata.update',hookParent.id,{worktreePath:project,branch:'main'});
 fs.writeFileSync(`${project}/settle-hook.log`,'');
 await rpc('server.updateSettings',{patch:{defaultProjectScripts:[{id:'settle-proof',name:'Log settle action',command:"printf 'settle action executed\\n' >> settle-hook.log; printf 'settle action executed\\n'",icon:'play',runOnWorktreeCreate:false,runOnSettle:true}]}});
 const shared=await delegate(hookParent,'Shared-worktree child');
 await show(shared,'shared-child-before-explicit-settlement');
 await command('thread.settle',shared);
 await show(shared,'shared-child-explicitly-settled-parent-still-active');
 const terminalReport = async(label:string) => {
  await rpc('terminal.open',{threadId:hookParent.id,terminalId:'term-1',cwd:project,env:{PS1:'proof> ',PROMPT_COMMAND:''},cols:100,rows:12});
   await rpc('terminal.write',{threadId:hookParent.id,terminalId:'term-1',data:"export PS1='proof> '; unset PROMPT_COMMAND; clear; printf 'SETTLE ACTION LOG\\n'; printf 'Executions: '; wc -l < settle-hook.log; cat settle-hook.log\r"});
   await app.open(`/threads/${descriptor.environmentId}/${hookParent.id}`);
   const drawer=screen.getByRole('button','Toggle terminal drawer');
   await drawer.tap();
   await browser.evaluate(()=>{if(document.activeElement instanceof HTMLElement)document.activeElement.blur();});
   await capture(label);
   await drawer.tap();
 };
 await terminalReport('hook-after-child-settlement-parent-still-waiting');
 const childHooks=fs.readFileSync(`${project}/settle-hook.log`,'utf8').split('settle action executed').length-1;
 all.push({case:'shared-worktree child does not run hook while parent active',passed:childHooks===0,executions:childHooks});
 expect.soft(childHooks).toBe(0);
 await command('thread.stop',hookParent.id);
 await expect.poll(async()=> (await projection(hookParent.id)).runs.every((r:any)=>!['preparing','starting','running','waiting','queued'].includes(r.status))).toBe(true);
 await command('thread.settle',hookParent.id);
 await expect.poll(()=>fs.readFileSync(`${project}/settle-hook.log`,'utf8').split('settle action executed').length-1).toBe(childHooks+1);
 await terminalReport('hook-after-owning-parent-settlement');
 const parentHooks=fs.readFileSync(`${project}/settle-hook.log`,'utf8').split('settle action executed').length-1;
 all.push({case:'only owning parent executes shared hook',passed:parentHooks===1,executions:parentHooks});
 expect.soft(parentHooks).toBe(1);
 fs.writeFileSync('/tmp/settlement-proof/observed-private.json',JSON.stringify(all,null,2));
});
