const http = require('http');
const fs = require('fs');
const path = require('path');
const {spawn} = require('child_process');

const ROOT = 'D:/Skynet/browser_reliability_research';
fs.mkdirSync(ROOT,{recursive:true});
const OUT = path.join(ROOT,'results'); fs.mkdirSync(OUT,{recursive:true});
const chrome = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
const debugPort = 9357;
const profile = path.join(ROOT,'chrome-profile');
try{fs.rmSync(profile,{recursive:true,force:true});}catch{}
const sleep = ms=>new Promise(r=>setTimeout(r,ms));

// Deterministic PRNG (Mulberry32).
function rng(seed){let a=seed>>>0; return ()=>{a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return ((t^t>>>14)>>>0)/4294967296;};}
function pick(r,a){return a[Math.floor(r()*a.length)];}

const labels=['Safe','Guarded','Verified','Strict','Stable','Protected'];
const projects=['Orion','Atlas','Nova','Helix','Aster','Vector','Kepler','Nimbus'];
const decoys=['Archive','Preview','Draft','Sandbox','Template','Mirror'];

function makeTasks(nPerLayer=24, nBaseline=24){
  const tasks=[]; let id=0;
  const layers=['access','perception','action','state','verification'];
  for(let i=0;i<nBaseline;i++){
    const r=rng(1000+i); tasks.push(makeTask(++id,'baseline',1,1000+i,r));
  }
  for(const layer of layers){
    for(let i=0;i<nPerLayer;i++){
      const severity=(i%3)+1; const seed=10000+layers.indexOf(layer)*1000+i; const r=rng(seed);
      tasks.push(makeTask(++id,layer,severity,seed,r));
    }
  }
  return tasks;
}
function makeTask(id,layer,severity,seed,r){
  const project=pick(r,projects); const target=pick(r,labels); let wrong=pick(r,labels.filter(x=>x!==target));
  return {id:`T${String(id).padStart(3,'0')}`,layer,severity,seed,project,target,wrong,
    task:`Set project ${project} mode to ${target} and save the change.`,
    accessDelay:layer==='access'?[70,180,420][severity-1]:0,
    decoyCount:layer==='perception'?severity:0,
    swallowedClicks:layer==='action'?severity:0,
    commitDelay:layer==='state'?[120,320,700][severity-1]:0,
    requiredClicks:layer==='verification'?[2,2,3][severity-1]:1};
}

const taskMap=new Map(); const stateMap=new Map();
function pageHtml(t,runId){
  const r=rng(t.seed+runId.length*17);
  const cards=[];
  const decoyNames=[];
  for(let i=0;i<t.decoyCount;i++) decoyNames.push(pick(r,decoys.filter(x=>!decoyNames.includes(x))));
  // For perception faults, decoys come first and use the same Save label.
  for(const name of decoyNames){cards.push(`<section class="card decoy" data-project="${name}"><h2>Project ${name}</h2><label>Mode <select><option>${t.wrong}</option><option>${t.target}</option></select></label><button class="save decoy-save">Save change</button><p class="status"></p></section>`)}
  cards.push(`<section class="card target" data-project="${t.project}"><h2>Project ${t.project}</h2><label>Mode <select id="mode"><option>${t.wrong}</option><option>${t.target}</option></select></label><button id="save" class="save">Save change</button><p id="status" class="status"></p><p id="summary">Current mode: ${t.wrong}</p></section>`);
  return `<!doctype html><html><head><meta charset="utf-8"><title>RStackBench ${runId}</title><style>
  body{font-family:Arial,sans-serif;margin:32px;max-width:900px}.card{border:1px solid #bbb;border-radius:12px;padding:18px;margin:14px 0}.decoy{opacity:.92}.status.success{color:green;font-weight:bold}.loading{padding:30px;border:1px dashed #999}.overlay{position:fixed;inset:0;background:rgba(255,255,255,.01);z-index:99}</style></head><body>
  <h1>Configuration Console</h1><p id="instruction">${t.task}</p><div id="root">${t.accessDelay?'<div class="loading">Loading workspaceÃ¢â‚¬Â¦</div>':cards.join('')}</div>
  <script>
  const cfg=${JSON.stringify(t)}; const runId=${JSON.stringify(runId)}; let clicks=0; let swallowed=${t.swallowedClicks};
  function bind(){
    document.querySelectorAll('.decoy-save').forEach(b=>b.onclick=()=>{b.closest('.card').querySelector('.status').textContent='Saved';b.closest('.card').querySelector('.status').className='status success';});
    const save=document.getElementById('save'); if(!save)return;
    save.onclick=async()=>{
      clicks++;
      const mode=document.getElementById('mode').value;
      if(swallowed>0){swallowed--; return;}
      if(clicks < cfg.requiredClicks){ document.getElementById('status').textContent='Saved'; document.getElementById('status').className='status success'; return; }
      // State fault: optimistic visible success before backend persistence.
      document.getElementById('status').textContent='Saved'; document.getElementById('status').className='status success';
      fetch('/commit?run='+encodeURIComponent(runId)+'&mode='+encodeURIComponent(mode)+'&delay='+cfg.commitDelay).catch(()=>{});
    };
  }
  ${t.accessDelay?`setTimeout(()=>{document.getElementById('root').innerHTML=${JSON.stringify(cards.join(''))};bind();},${t.accessDelay});`:'bind();'}
  async function refreshSummary(){try{let s=await fetch('/state?run='+encodeURIComponent(runId)).then(r=>r.json());let e=document.getElementById('summary');if(e)e.textContent='Current mode: '+s.mode;}catch{}}
  setInterval(refreshSummary,80);
  </script></body></html>`;
}

const server=http.createServer(async(req,res)=>{
  const u=new URL(req.url,'http://127.0.0.1');
  if(u.pathname==='/task'){
    const run=u.searchParams.get('run'); const tid=u.searchParams.get('tid'); const t=taskMap.get(tid);
    res.writeHead(t?200:404,{'content-type':'text/html; charset=utf-8','cache-control':'no-store'}); return res.end(t?pageHtml(t,run):'missing');
  }
  if(u.pathname==='/commit'){
    const run=u.searchParams.get('run'), mode=u.searchParams.get('mode'), delay=Number(u.searchParams.get('delay')||0);
    const s=stateMap.get(run); if(s){setTimeout(()=>{s.mode=mode;s.committedAt=Date.now();},delay);} res.writeHead(204); return res.end();
  }
  if(u.pathname==='/state'){
    const run=u.searchParams.get('run'); const s=stateMap.get(run)||{mode:'UNKNOWN'}; res.writeHead(200,{'content-type':'application/json','cache-control':'no-store'}); return res.end(JSON.stringify(s));
  }
  res.writeHead(404);res.end('not found');
});

async function connect(wsUrl){const ws=new WebSocket(wsUrl);await new Promise((ok,rej)=>{ws.onopen=ok;ws.onerror=rej});let seq=0;const p=new Map();ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.id&&p.has(m.id)){const x=p.get(m.id);p.delete(m.id);m.error?x.rej(new Error(m.error.message)):x.res(m.result);}};return{ws,send:(method,params={})=>new Promise((res,rej)=>{const id=++seq;p.set(id,{res,rej});ws.send(JSON.stringify({id,method,params}));})};}
async function waitChrome(){for(let i=0;i<100;i++){try{const a=await fetch(`http://127.0.0.1:${debugPort}/json`);if(a.ok){const j=await a.json();const p=j.find(x=>x.type==='page');if(p)return p;}}catch{}await sleep(100);}throw new Error('Chrome CDP unavailable');}
async function evalv(send,expression){const r=await send('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw new Error(r.exceptionDetails.text||'eval exception');return r.result.value;}
async function nav(send,url){await send('Page.navigate',{url});for(let i=0;i<80;i++){const s=await evalv(send,'document.readyState');if(s==='complete'||s==='interactive')break;await sleep(25);}await sleep(20);}
async function click(send,expr){return evalv(send,`(()=>{const e=${expr};if(!e)return {ok:false,reason:'not_found'};const r=e.getBoundingClientRect();if(!r.width||!r.height)return {ok:false,reason:'not_visible'};e.click();return {ok:true,text:(e.innerText||'').trim()};})()`)}
async function selectTarget(send,target){return evalv(send,`(()=>{const s=document.getElementById('mode');if(!s)return false;for(const o of s.options){if(o.textContent===${JSON.stringify(target)}){s.value=o.value;s.dispatchEvent(new Event('change',{bubbles:true}));return true}}return false})()`)}
async function hasSuccess(send){return evalv(send,`!![...document.querySelectorAll('.status.success')].find(e=>/saved/i.test(e.textContent||''))`)}
async function summary(send){return evalv(send,`(document.getElementById('summary')||{}).textContent||''`)}
async function targetReady(send){return evalv(send,`!!document.querySelector('.card.target #save')`)}
async function waitUntil(fn,timeout=1200,step=40){const end=Date.now()+timeout;while(Date.now()<end){if(await fn())return true;await sleep(step);}return false;}

async function runPolicy(send,baseUrl,t,policy,runId){
  const start=Date.now();let actions=0,retries=0,apparent=false,verified=false,notes=[];
  await nav(send,`${baseUrl}/task?tid=${encodeURIComponent(t.id)}&run=${encodeURIComponent(runId)}`);
  if(policy==='selector-only'){
    // Open-loop baseline: first visible Save button, no wait/retry, trusts any success banner.
    const sel=await selectTarget(send,t.target); if(sel)actions++;
    const cr=await click(send,`[...document.querySelectorAll('button')].find(b=>/save change/i.test(b.innerText||''))`); actions++;
    await sleep(35); apparent=await hasSuccess(send); notes.push(cr.reason||'clicked');
  } else {
    // Semantic recovery: waits for the requested project card and retries when no visible success occurs.
    const ready=await waitUntil(()=>targetReady(send),1400,35); if(!ready)notes.push('target_timeout');
    if(ready){const sel=await selectTarget(send,t.target); if(sel)actions++;
      for(let k=0;k<4;k++){
        const cr=await click(send,`document.querySelector('.card.target #save')`);actions++; if(k>0)retries++;
        const ok=await waitUntil(()=>hasSuccess(send),180,30); if(ok){apparent=true;break;} await sleep(45);
      }
    }
    if(policy==='verification-aware' && apparent){
      // Verify a second user-visible surface backed by server state; retry rather than trusting the banner.
      let ok=await waitUntil(async()=>{const s=await summary(send);return s.includes(t.target);},950,65);
      if(!ok){
        notes.push('verification_mismatch');
        for(let k=0;k<3&&!ok;k++){
          const cr=await click(send,`document.querySelector('.card.target #save')`);actions++;retries++;
          ok=await waitUntil(async()=>{const s=await summary(send);return s.includes(t.target);},950,65);
        }
      }
      verified=ok;
    }
  }
  // Fixed short scoring horizon. Pending delayed commits beyond this point do not count as completed task state.
  await sleep(policy==='verification-aware'?30:55);
  const gt=stateMap.get(runId); const vts=!!gt&&gt.mode===t.target;
  if(policy!=='verification-aware') verified=vts;
  return {policy,taskId:t.id,layer:t.layer,severity:t.severity,seed:t.seed,apparentSuccess:apparent,verifiedTaskSuccess:vts,falseCompletion:apparent&&!vts,actions,retries,latencyMs:Date.now()-start,notes};
}

function wilson(k,n,z=1.959963984540054){if(!n)return[0,0];const p=k/n,zz=z*z,d=1+zz/n,c=(p+zz/(2*n))/d,h=z*Math.sqrt((p*(1-p)+zz/(4*n))/n)/d;return[Math.max(0,c-h),Math.min(1,c+h)];}
function exactMcNemar(a,b){ // a: A success/B fail, b: A fail/B success; exact two-sided binomial
  const n=a+b;if(!n)return 1;const m=Math.min(a,b);let s=0;for(let k=0;k<=m;k++){let comb=1;for(let j=1;j<=k;j++)comb*= (n-j+1)/j;s+=comb*Math.pow(0.5,n);}return Math.min(1,2*s);
}
function aggregate(rows){
  const groups={};for(const r of rows){for(const key of [`policy:${r.policy}`,`policy:${r.policy}|layer:${r.layer}`]){(groups[key]??=[]).push(r)}}
  const out={};for(const [k,g] of Object.entries(groups)){const n=g.length,s=g.filter(x=>x.verifiedTaskSuccess).length,a=g.filter(x=>x.apparentSuccess).length,f=g.filter(x=>x.falseCompletion).length;out[k]={n,verified_success:s,verified_rate:s/n,verified_rate_wilson95:wilson(s,n),apparent_success:a,apparent_rate:a/n,false_completions:f,false_completion_rate_given_apparent:a?f/a:0,mean_actions:g.reduce((q,x)=>q+x.actions,0)/n,mean_retries:g.reduce((q,x)=>q+x.retries,0)/n,mean_latency_ms:g.reduce((q,x)=>q+x.latencyMs,0)/n};}
  const policies=['selector-only','semantic-recovery','verification-aware'];const paired=[];
  for(let i=0;i<policies.length;i++)for(let j=i+1;j<policies.length;j++)for(const layer of ['all','baseline','access','perception','action','state','verification']){
    const A=new Map(rows.filter(r=>r.policy===policies[i]&&(layer==='all'||r.layer===layer)).map(r=>[r.taskId,r.verifiedTaskSuccess]));
    const B=new Map(rows.filter(r=>r.policy===policies[j]&&(layer==='all'||r.layer===layer)).map(r=>[r.taskId,r.verifiedTaskSuccess]));let a=0,b=0,n=0;for(const [id,av]of A){if(!B.has(id))continue;n++;const bv=B.get(id);if(av&&!bv)a++;if(!av&&bv)b++;}paired.push({policyA:policies[i],policyB:policies[j],layer,n,A_success_B_fail:a,A_fail_B_success:b,mcnemar_exact_p:exactMcNemar(a,b)});
  }
  return {groups:out,paired_tests:paired};
}

async function main(){
  const tasks=makeTasks(24,24); for(const t of tasks)taskMap.set(t.id,t);
  fs.writeFileSync(path.join(OUT,'tasks.json'),JSON.stringify(tasks,null,2));
  await new Promise(ok=>server.listen(0,'127.0.0.1',ok)); const serverPort=server.address().port; const baseUrl=`http://127.0.0.1:${serverPort}`;
  const cp=spawn(chrome,[`--remote-debugging-port=${debugPort}`,`--user-data-dir=${profile}`,'--headless=new','--disable-gpu','--no-first-run','--disable-background-networking','about:blank'],{stdio:'ignore',windowsHide:true});
  const target=await waitChrome(); const c=await connect(target.webSocketDebuggerUrl); const send=c.send; await send('Page.enable'); await send('Runtime.enable');
  const policies=['selector-only','semantic-recovery','verification-aware']; const rows=[];
  for(const policy of policies){
    for(let i=0;i<tasks.length;i++){
      const t=tasks[i]; const runId=`${policy}-${t.id}-${Date.now()}-${i}`; stateMap.set(runId,{mode:t.wrong,committedAt:null});
      const row=await runPolicy(send,baseUrl,t,policy,runId); rows.push(row); if((i+1)%12===0)console.log(`${policy} ${i+1}/${tasks.length} VTS=${rows.filter(x=>x.policy===policy&&x.verifiedTaskSuccess).length}`);
      stateMap.delete(runId);
    }
  }
  const summary=aggregate(rows); const meta={schema:'rstackbench/1',createdAt:new Date().toISOString(),chromeVersion:await evalv(send,'navigator.userAgent'),taskCount:tasks.length,runCount:rows.length,policies,design:{baselineTasks:24,perFaultLayerTasks:24,layers:['access','perception','action','state','verification'],severities:[1,2,3],metric:'verified task success determined from server-side persisted state; apparent success determined from visible success UI'}};
  fs.writeFileSync(path.join(OUT,'runs.jsonl'),rows.map(x=>JSON.stringify(x)).join('\n')+'\n'); fs.writeFileSync(path.join(OUT,'summary.json'),JSON.stringify({meta,summary},null,2));
  let md=`# RStackBench experimental summary\n\nTasks: ${tasks.length}; runs: ${rows.length}; Chromium renderer.\n\n| Policy | N | Verified success | Apparent success | False completions | Mean actions | Mean latency ms |\n|---|---:|---:|---:|---:|---:|---:|\n`;
  for(const p of policies){const g=summary.groups[`policy:${p}`];md+=`| ${p} | ${g.n} | ${(100*g.verified_rate).toFixed(1)}% | ${(100*g.apparent_rate).toFixed(1)}% | ${g.false_completions} | ${g.mean_actions.toFixed(2)} | ${g.mean_latency_ms.toFixed(0)} |\n`;}
  md+='\n## Layer-specific verified success\n\n| Policy | Baseline | Access | Perception | Action | State | Verification |\n|---|---:|---:|---:|---:|---:|---:|\n';
  for(const p of policies){md+=`| ${p} | ${['baseline','access','perception','action','state','verification'].map(l=>`${(100*summary.groups[`policy:${p}|layer:${l}`].verified_rate).toFixed(1)}%`).join(' | ')} |\n`;}
  md+='\n## Paired exact McNemar tests\n\n```json\n'+JSON.stringify(summary.paired_tests,null,2)+'\n```\n';
  fs.writeFileSync(path.join(OUT,'summary.md'),md);
  c.ws.close(); try{cp.kill();}catch{}; server.close(); console.log(`DONE tasks=${tasks.length} runs=${rows.length} out=${OUT}`);
}
main().catch(e=>{console.error(e);try{server.close()}catch{};process.exit(1)});

