import { expect } from "e2e";
import { createServer } from "node:http";
import { readdir, readFile } from "node:fs/promises";
import { test, WEB } from "./t3.ts";

// No credentials leave memory. Only these synthetic threads' own child environments are read.
async function credentials(threadId: string) {
  for (const pid of await readdir('/proc')) {
    if (!/^\d+$/.test(pid)) continue;
    try {
      const env = Object.fromEntries((await readFile(`/proc/${pid}/environ`, 'utf8')).split('\0').map(s => { const i=s.indexOf('='); return [s.slice(0,i),s.slice(i+1)]; }));
      if (env.T3CODE_THREAD_ID === threadId && env.T3_ACP_MCP_AUTHORIZATION)
        return { url: env.T3_ACP_MCP_ENDPOINT, authorization: env.T3_ACP_MCP_AUTHORIZATION };
    } catch {}
  }
  throw new Error('Synthetic replay child MCP credential was not found');
}
const delay = (ms: number) => new Promise(r => setTimeout(r, ms));

test('One timed-out preview call leaves the other thread alive', WEB, async ({t3, app}) => {
  const project = await t3.project('Shared browser proof');
  const a = await t3.thread({project,title:'Thread A — queued timeout',messages:['Wait for an answer'],model:'question'});
  const b = await t3.thread({project,title:'Thread B — independent wait',messages:['Wait for an answer'],model:'question'});
  await t3.open(b);
  const ca = await credentials(a.threadId), cb = await credentials(b.threadId);
  const lines: string[] = [];
  let start = Date.now();
  const log = (s: string) => lines.push(`+${((Date.now()-start)/1000).toFixed(1)}s  ${s}`);
  const escape = (s: string) => s.replaceAll('&','&amp;').replaceAll('<','&lt;');
  const server = createServer((req,res) => {
    if (req.url === '/events') { res.setHeader('content-type','application/json'); res.end(JSON.stringify(lines.join('\n'))); return; }
    res.setHeader('content-type','text/html; charset=utf-8');
    if (req.url === '/b') res.end('<title>Thread B</title><h1>Thread B</h1><p id="state">Waiting for 28 seconds</p><script>setTimeout(()=>document.querySelector("#state").textContent="Ready now",28000)</script>');
    else if (req.url === '/a') res.end('<title>Thread A</title><h1>Thread A</h1><p>Static page with no changing content.</p>');
    else res.end(`<title>Shared browser timeout — real MCP transcript</title><style>body{background:#111827;color:#e5e7eb;font:19px/1.6 monospace;padding:32px}h1{font:700 28px system-ui}pre{white-space:pre-wrap}small{color:#9ca3af}</style><h1>Shared browser timeout: two real replay threads</h1><small>Linux · server Chromium · fresh synthetic state · real /mcp responses<br>A: 25s missing-text wait + queued evaluate. B: Ready now after 28s.</small><pre>${escape(lines.join('\n'))}</pre><script>setInterval(async()=>{const t=await(await fetch('/events')).json();const p=document.querySelector('pre');if(p.textContent!==t)p.textContent=t},250)</script>`);
  });
  await new Promise<void>(r=>server.listen(0,'0.0.0.0',r));
  const port = (server.address() as any).port;
  let id = 0;
  const sessions = new Map<object,string>();
  for (const c of [ca,cb]) {
    const response = await fetch(c.url,{method:'POST',headers:{Authorization:c.authorization,'Content-Type':'application/json',Accept:'application/json, text/event-stream'},body:JSON.stringify({jsonrpc:'2.0',id:++id,method:'initialize',params:{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'shared-browser-proof',version:'1.0'}}})});
    await response.text();
    const session = response.headers.get('mcp-session-id');
    if (!session) throw new Error('MCP initialization did not establish a session');
    sessions.set(c,session);
  }
  async function call(c: typeof ca, name: string, args: object) {
    const response = await fetch(c.url,{method:'POST',headers:{Authorization:c.authorization,'Mcp-Session-Id':sessions.get(c)!,'Content-Type':'application/json',Accept:'application/json, text/event-stream'},body:JSON.stringify({jsonrpc:'2.0',id:++id,method:'tools/call',params:{name,arguments:args}})});
    const raw = await response.text();
    const data = raw.startsWith('data:') || raw.includes('\ndata:') ? JSON.parse(raw.split('\n').filter(l=>l.startsWith('data:')).at(-1)!.slice(5)) : JSON.parse(raw);
    if (data.error) throw new Error(JSON.stringify(data.error));
    return data.result;
  }
  const summary = (r: any) => r.isError ? r.content?.map((x:any)=>x.text??'').join(' ') : 'SUCCESS';
  try {
    const oa = await call(ca,'preview_open',{url:`http://127.0.0.1:${port}/a`});
    const ob = await call(cb,'preview_open',{url:`http://localhost:${port}/b`});
    const tab = (r:any) => r.structuredContent?.tabId ?? JSON.parse(r.content.find((x:any)=>x.type==='text').text).tabId;
    const ta = tab(oa), tb = tab(ob);
    if (!ta || !tb) throw new Error('preview_open did not return both tab IDs: '+JSON.stringify({oa,ob}));
    await app.open(`http://127.0.0.1:${port}/transcript`);
    start = Date.now();
    log('A and B opened on the shared server-browser host.');
    const bw = call(cb,'preview_wait_for',{tabId:tb,text:'Ready now',timeoutMs:40000}).then(r=>{log('B wait_for: '+summary(r));return r;});
    log('B wait_for started (40s deadline; page ready at 28s).');
    const aw = call(ca,'preview_wait_for',{tabId:ta,text:'never appears',timeoutMs:25000}).then(r=>{log('A wait_for: '+summary(r));return r;});
    log('A wait_for started (25s deadline).');
    await delay(1000);
    log('A evaluate queued behind A wait_for (15s deadline).');
    await call(ca,'preview_evaluate',{tabId:ta,expression:'document.title'}).then(r=>log('A evaluate: '+summary(r)));
    const status = await call(cb,'preview_status',{});
    log('B status immediately after A timeout: '+summary(status));
    const result = await bw;
    await aw;
    log('Observed B outcome: '+(result.isError?'ABORTED by the other thread timeout':'completed independently; shared host survived'));
    await app.open(`http://127.0.0.1:${port}/transcript`);
    await delay(1200);
    await app.screenshot('outcome-stability-check');
    await delay(500);
    await app.screenshot('shared-browser-outcome');
    await expect(result.isError ?? false).toBe(false);
  } finally { server.closeAllConnections(); await new Promise<void>(r=>server.close(()=>r())); }
});
