import { expect } from "e2e";
import { readFileSync, writeFileSync } from "node:fs";
import { test, WEB } from "./t3.ts";
import { APP_DIR } from "../e2e.config.ts";

// Observers wrap the original functions without changing their inputs or results.
const observer = `
const proof = () => globalThis.__timelineProof ??= { events: [], failures: [], stage: 'ready' };
const textOf = e => e?.kind === 'work' ? e.entry.detail : e?.kind === 'proposed-plan' ? e.proposedPlan.planMarkdown : e?.message?.text;
const holds = (r, id) => r.id === id || r.groupedEntries?.some(e => e.id === id);
const equal = (a,b) => JSON.stringify(a) === JSON.stringify(b);
`;
function instrument() {
  const entryPath = APP_DIR + '/apps/web/src/session-logic.ts';
  let entries = readFileSync(entryPath, 'utf8');
  entries = entries.replace('export function deriveTimelineEntriesFromVisibleTurnItemsWithState(', 'function originalEntries(');
  entries += observer + `
export function deriveTimelineEntriesFromVisibleTurnItemsWithState(input, previous = null) {
 const beforeTexts=previous?.entries.map(e=>({entry:e,text:textOf(e)}))??[];
 const result = originalEntries(input, previous);
 const p = proof();
 (p.raw??=[]).push({previous:!!previous,plans:input.plans?.length, previousPlans:previous?.input.plans?.length, reasoning:input.visibleTurnItems.filter(x=>x.item.type==='reasoning').map(x=>({id:x.item.id,streaming:x.item.streaming,length:x.item.text?.length, old:previous?.input.visibleTurnItems.find(y=>y.item.id===x.item.id)?.item.text?.length,oldStreaming:previous?.input.visibleTurnItems.find(y=>y.item.id===x.item.id)?.item.streaming})),changes:previous?input.visibleTurnItems.filter((x,i)=>previous.input.visibleTurnItems[i]!==x).map(x=>x.item.type):[]});
 const changes = previous ? input.visibleTurnItems.filter((x,i) => previous.input.visibleTurnItems[i]?.item.id === x.item.id && previous.input.visibleTurnItems[i] !== x) : [];
 const textChanges = changes.filter(x => { const old = previous.input.visibleTurnItems.find(y=>y.item.id===x.item.id)?.item; return old && old.streaming && x.item.streaming && (old.text !== x.item.text || old.markdown !== x.item.markdown); });
 const renewedPlans = !!input.plans?.some((x,i)=>previous?.input.plans?.[i] && x!==previous.input.plans[i] && x.id===previous.input.plans[i].id && x.kind===previous.input.plans[i].kind && x.status===previous.input.plans[i].status);
 if (textChanges.length || renewedPlans) {
  const ids = new Set(textChanges.map(x=>x.item.id));
  const unrelated = previous.entries.filter(e=>!ids.has(e.id));
  const kept = unrelated.filter(e=>result.entries.includes(e)).length;
  const freshPlans = renewedPlans;
  const changed = result.entries.filter(e=>ids.has(e.id));
  const correctText = changed.every(e=>{const x=textChanges.find(x=>x.item.id===e.id);return textOf(e)===(x.item.text ?? x.item.markdown);});
  const changedReplaced=changed.every(e=>!previous.entries.includes(e));
  const previousTextPreserved=beforeTexts.every(x=>textOf(x.entry)===x.text);
  p.events.push({layer:'entries',changedReplaced,previousTextPreserved, types:textChanges.map(x=>x.item.type), unrelated:unrelated.length, retained:kept, freshPlans, correctText});
 }
 return result;
}
`;
  writeFileSync(entryPath,entries);
  const rowPath = APP_DIR + '/apps/web/src/components/chat/MessagesTimeline.logic.ts';
  let rows=readFileSync(rowPath,'utf8');
  rows=rows.replace('function replaceStreamingMessageRows(', 'function originalReplace(');
  rows=rows.replace('export function deriveMessagesTimelineRowsWithState(', 'function originalRows(');
  rows += observer + `
function replaceStreamingMessageRows(input,previous) {
 const result = originalReplace(input,previous);
 proof().lastFast = result !== null;
 return result;
}
export function deriveMessagesTimelineRowsWithState(input, previous = null) {
 const result = originalRows(input, previous);
 const p=proof();
 if (previous) {
  const changes=input.timelineEntries.filter((e,i)=>previous.input.timelineEntries[i]?.id===e.id && previous.input.timelineEntries[i]!==e);
  const textChanges=changes.filter(e=>textOf(e)!==textOf(previous.input.timelineEntries.find(x=>x.id===e.id)));
  const ids=new Set(textChanges.map(e=>e.id));
  const unrelated=previous.rows.filter(r=>![...ids].some(id=>holds(r,id)));
  const retained=unrelated.filter(r=>result.rows.includes(r)).length;
  let reason=null;
  if(input.toolCallVisibility!==previous.input.toolCallVisibility) reason='visibility';
  else if(!equal([...input.expandedWorkGroupIds??[]],[...previous.input.expandedWorkGroupIds??[]])) reason='group expansion';
  else if(input.isWorking!==previous.input.isWorking || input.latestRun?.status!==previous.input.latestRun?.status) reason='completion/run status';
  else if(input.timelineEntries.length!==previous.input.timelineEntries.length) reason='grouping/item count';
  else if(changes.some(e=>{ const old=previous.input.timelineEntries.find(x=>x.id===e.id); const a=e.entry?.projectedItem?.item??e.projectedItem?.item; const b=old?.entry?.projectedItem?.item??old?.projectedItem?.item; return a&&b&&(a.status!==b.status||a.streaming!==b.streaming||a.title!==b.title);})) reason='metadata/status';
  if(!reason && textChanges.some(e=>e.kind==='work' && !previous.rows.some(r=>r.kind==='work-live' && holds(r,e.id)))) reason='settled thought group';
  if(textChanges.length || reason) {
    const full=deriveMessagesTimelineRows(input);
    const matches=equal(result.rows,full);
    if(!matches)p.failures.push({reason,types:textChanges.map(e=>e.kind)});
    p.events.push({layer:'rows', types:textChanges.map(e=>e.kind==='work'?'reasoning':e.kind==='proposed-plan'?'proposed_plan':e.kind), fast:p.lastFast, unrelated:unrelated.length,retained,reason,matches,visibility:input.toolCallVisibility, thoughts:result.rows.filter(r=>r.kind==='work-live').map(r=>({status:r.thought?.toolLifecycleStatus??r.entry?.toolLifecycleStatus, expanded:r.expanded, count:r.groupedEntries.length}))});
  }
 }
 p.lastInput=input;p.lastResult=result;
 return result;
}
`;
  writeFileSync(rowPath,rows);

}

test('Thinking and plan streams retain unrelated entries and rows', WEB, async ({t3, app, screen, browser}) => {
 instrument();
 const stability:Record<string,boolean>={};
 const capture=async(label:string)=>{await browser.evaluate(()=>(window as any).__paintProof?.());if(label!=='completed-reference-counts'){await app.screenshot(label);return;}for(let i=0;i<6;i++){const a=await app.screenshot(label+'-stability');await browser.evaluate(()=>new Promise(r=>setTimeout(r,500)));const b=await app.screenshot(label);if(readFileSync(a).equals(readFileSync(b))){stability[label]=true;return;}}stability[label]=false;};
 const project=await t3.project('Timeline demonstration', {'README.md':'Synthetic timeline history.\n'});
 await t3.thread({project,title:'Streaming work demonstration',messages:['Historical turn one','Historical turn two','Historical turn three'],model:'showcase'});
 await t3.open();
 await screen.getByRole('button','Streaming work demonstration, Done, Timeline demonstration').tap();
 await screen.getByText('History ready.',{exact:false}).last().waitFor({state:'visible'});
 for(let i=0;i<5;i++){const load=screen.getByRole('button','Load earlier turns');if(await load.count()===0)break;const n=await browser.evaluate(()=>(window as any).__timelineProof?.lastInput?.timelineEntries.length??0);await load.tap();await browser.evaluate((before:number)=>new Promise(resolve=>{const t=setInterval(()=>{if(((window as any).__timelineProof?.lastInput?.timelineEntries.length??0)!==before){clearInterval(t);resolve(true);}},50);setTimeout(()=>{clearInterval(t);resolve(false);},1500);}),n);}
 const history=screen.getByRole('button','Worked for',{exact:false}).first();if(await history.count())await history.tap();
 await screen.getByRole('textbox').fill('Stream thinking and a detailed plan');
 await screen.getByRole('button','Submit message').tap();
 await screen.getByRole('button','Stop generation').waitFor({state:'visible'});
 await browser.evaluate(()=>{
  const style=document.createElement('style');style.textContent='body {width:calc(100% - 320px)!important;height:100vh!important;position:relative!important;transform:translateZ(0)!important} #proof-panel{position:fixed;right:0;top:0;bottom:0;width:310px;background:#101827;color:#e5edf9;padding:12px;font:11px/1.3 monospace;overflow:auto;z-index:99999;border-left:1px solid #64748b;white-space:pre-wrap}';document.head.append(style);
  const panel=document.createElement('aside');panel.id='proof-panel';document.documentElement.append(panel);
  (window as any).__paintProof=()=>{
   const p=(window as any).__timelineProof; const events=p?.events??[];
   const lines=['TIMELINE STREAM OBSERVER','Real server • Chromium','Synthetic history: 300+ items','Read-only reference counters',''];
   for(const type of ['reasoning','proposed_plan'])for(const layer of ['entries','rows']){
    const es=events.filter((e:any)=>e.layer===layer&&e.types.includes(type)&&(layer!=='rows'||!e.reason));
    lines.push(type+' / '+layer, 'updates: '+es.length, layer==='rows'?'fast patches: '+es.filter((e:any)=>e.fast).length:'fresh plan artifacts: '+es.filter((e:any)=>e.freshPlans).length,'retain all unrelated: '+es.filter((e:any)=>e.unrelated>0&&e.retained===e.unrelated).length+'/'+es.length,'latest retained: '+(es.at(-1)?.retained??0)+'/'+(es.at(-1)?.unrelated??0),'');
   }
   const artifacts=events.filter((e:any)=>e.layer==='entries'&&e.freshPlans&&e.types.length===0);
   lines.push('PLAN ARTIFACT ONLY', 'updates: '+artifacts.length,'all entries retained: '+artifacts.filter((e:any)=>e.retained===e.unrelated).length+'/'+artifacts.length,'');
   lines.push('FULL REBUILD OBSERVATIONS');
   for(const reason of ['visibility','group expansion','completion/run status','grouping/item count','metadata/status','settled thought group']) {const es=events.filter((e:any)=>e.layer==='rows'&&e.reason===reason);lines.push(reason+': '+es.filter((e:any)=>!e.fast).length);}
   lines.push('','Live thought status: '+(events.filter((e:any)=>e.layer==='rows').at(-1)?.thoughts??[]).map((x:any)=>x.status+' / '+(x.expanded?'expanded':'collapsed')).join(', '),'','Rows match full derivation:',String(events.filter((e:any)=>e.layer==='rows').every((e:any)=>e.matches)),'Entry text / replacement / prior text:',String(events.filter((e:any)=>e.layer==='entries').every((e:any)=>e.correctText&&e.changedReplaced&&e.previousTextPreserved)),'','No CPU gain inferred.');panel.textContent=lines.join('\n');
  };
  const timer=setInterval((window as any).__paintProof,200);(window as any).__proofTimer=timer;
 });
 try {
 // The waits drive the same complete stream on both sides; assertions follow all captures.
 await browser.evaluate(()=>new Promise<void>(resolve=>{const t=setInterval(()=>{if((window as any).__timelineProof?.events.filter((e:any)=>e.layer==='rows'&&e.types.includes('reasoning')).length>=1){clearInterval(t);resolve();}},100);}));
 await capture('thinking-stream');
 const thoughtButton=browser.locator('button[aria-expanded]').filter({hasText:'Thinking'}).first();if(await thoughtButton.count())await thoughtButton.tap();
 await capture('expanded-thinking');
 writeFileSync('/tmp/timeline-proof/thinking-debug.json',JSON.stringify(await browser.evaluate(()=>JSON.parse(JSON.stringify({raw:(window as any).__timelineProof.raw,events:(window as any).__timelineProof.events,buttons:[...document.querySelectorAll('button')].map(b=>({label:b.getAttribute('aria-label'),text:b.textContent}))}))),null,2));
 await browser.evaluate(()=>new Promise<void>(resolve=>{const t=setInterval(()=>{if((window as any).__timelineProof?.lastInput?.timelineEntries.some((e:any)=>e.entry?.detail?.includes('Checking grouped thought 4.'))){clearInterval(t);resolve();}},100);}));
 await capture('grouped-thinking');
 await browser.evaluate(async()=>{ const settings=await import('/src/hooks/useSettings.ts');await settings.updateClientSettings({toolCallVisibility:'hideAll'}); });
 await capture('grouped-thinking-stream');
 await browser.evaluate(()=>new Promise<void>(resolve=>{const t=setInterval(()=>{if((window as any).__timelineProof?.events.some((e:any)=>e.layer==='rows'&&e.types.includes('reasoning')&&!e.reason&&e.visibility==='hideAll')){clearInterval(t);resolve();}},100);}));
 await capture('hidden-tool-thinking-update');
 await browser.evaluate(()=>new Promise<void>(resolve=>{const t=setInterval(()=>{if((window as any).__timelineProof?.events.some((e:any)=>e.reason==='settled thought group')){clearInterval(t);resolve();}},100);}));
 await capture('settled-thought-rebuild');
 await browser.evaluate(()=>new Promise<void>(resolve=>{const t=setInterval(()=>{if((window as any).__timelineProof?.events.filter((e:any)=>e.layer==='entries'&&e.types.includes('proposed_plan')).length>=10){clearInterval(t);resolve();}},100);}));
 await capture('plan-stream');
 await screen.getByText('Proof replay finished.',{exact:false}).first().waitFor({state:'visible'});
 await screen.getByRole('button','Stop generation').waitFor({state:'hidden'});
 await browser.evaluate(()=>{clearInterval((window as any).__proofTimer);(window as any).__paintProof();});
 await capture('completed-reference-counts');
 const metrics=await browser.evaluate(()=>{const p=(window as any).__timelineProof;return JSON.parse(JSON.stringify({events:p.events,failures:p.failures,raw:p.raw}));});
 writeFileSync('/tmp/timeline-proof/metrics-'+APP_DIR.split('-').at(-1)+'.json',JSON.stringify({...metrics,stability},null,2));
 // Both layers are independently checked, including recreated plan artifacts.
 for(const type of ['reasoning','proposed_plan']) {
  const entries=metrics.events.filter((e:any)=>e.layer==='entries'&&e.types.includes(type));
  const rows=metrics.events.filter((e:any)=>e.layer==='rows'&&e.types.includes(type)&&!e.reason);
  expect(entries.length).toBeGreaterThan(1);
  expect(rows.length).toBeGreaterThan(1);
  expect(entries.every((e:any)=>e.correctText&&e.changedReplaced&&e.previousTextPreserved)).toBe(true);
  expect(rows.every((e:any)=>e.fast && e.retained===e.unrelated)).toBe(true);
  expect(entries.every((e:any)=>e.retained===e.unrelated)).toBe(true);
  
 }
 const artifactUpdates=metrics.events.filter((e:any)=>e.layer==='entries'&&e.freshPlans&&e.types.length===0);
 expect(artifactUpdates.length).toBeGreaterThan(0);
 expect(artifactUpdates.every((e:any)=>e.retained===e.unrelated)).toBe(true);
 for(const reason of ['visibility','group expansion','completion/run status','grouping/item count','metadata/status','settled thought group']) {const cases=metrics.events.filter((e:any)=>e.layer==='rows'&&e.reason===reason);expect(cases.length).toBeGreaterThan(0);expect(cases.every((e:any)=>!e.fast&&e.matches)).toBe(true);}
const hidden=metrics.events.filter((e:any)=>e.layer==='rows'&&e.types.includes('reasoning')&&!e.reason&&e.visibility==='hideAll');expect(hidden.length).toBeGreaterThan(0);expect(hidden.every((e:any)=>e.thoughts.some((t:any)=>t.status==='inProgress')&&e.matches)).toBe(true);
 expect(metrics.failures).toEqual([]);
 } finally {
 const debug=await browser.evaluate(()=>JSON.parse(JSON.stringify({events:(window as any).__timelineProof?.events,raw:(window as any).__timelineProof?.raw,input:(window as any).__timelineProof?.lastInput})));writeFileSync('/tmp/timeline-proof/debug-'+APP_DIR.split('-').at(-1)+'.json',JSON.stringify(debug,null,2));
 }
});
