import { expect } from "e2e";
import { T3_HOME } from "../e2e.config.ts";
import { makeState, runPromise, test, WEB } from "./t3.ts";

test("A selected thread recovers from a transient subscription defect", WEB, async ({t3, app, screen, browser}) => {
  const project = await t3.project("Subscription recovery");
  await t3.thread({project, title: "Anchor", messages: ["Anchor conversation"]});
  await browser.addInitScript(() => {
    const fault = (window as any).__fault = {holdShell: false, armed: false, http: false, corrupted: 0, shellHeld: 0, baselineSockets: 0, subscriptions: 0, coldFailed: false, httpSeen: false, sockets: 0, log: [] as any[]};
    const Native = window.WebSocket;
    window.WebSocket = class extends Native {
      constructor(...args: ConstructorParameters<typeof WebSocket>) {
        super(...args);
        fault.sockets++;
        const requests = new Map<string, string>();
        const send = this.send.bind(this);
        this.send = (data) => {
          try { for (const m of (Array.isArray(JSON.parse(String(data))) ? JSON.parse(String(data)) : [JSON.parse(String(data))])) { requests.set(String(m.id), m.tag); if(m.tag === 'orchestration.subscribeThread') {fault.subscriptions++; fault.log.push(m);} } } catch {}
          send(data);
        };
        let redispatch = false;
        this.addEventListener('message', (event) => {
          if(redispatch) return;
          let frames: any[];
          try { const parsed = JSON.parse(event.data); frames = Array.isArray(parsed) ? parsed : [parsed]; } catch { return; }
          let changed = false;
          frames = frames.filter(m => {
            const tag = requests.get(String(m.requestId));
            if(fault.holdShell && tag === 'orchestration.subscribeShell' && m._tag === 'Chunk') {fault.shellHeld++; changed = true; return false;}
            if(fault.armed && tag === 'orchestration.subscribeThread' && m._tag === 'Chunk') {
              const walk = (v: any): boolean => {
                if(!v || typeof v !== 'object') return false;
                if(v.kind === 'snapshot' && v.projection) {v.projection = {}; return true;}
                return Object.values(v).some(walk);
              };
              if(!fault.coldFailed) { fault.coldFailed = true; Object.assign(m, {_tag: 'Exit', exit: {_tag: 'Failure', cause: [{_tag: 'Die', defect: 'Controlled cold-open failure'}]}}); delete m.values; changed = true; return true; }
              if(!fault.httpSeen) throw new Error('HTTP fallback was not attempted');
              if(walk(m)) { fault.armed = false; fault.http = false; fault.corrupted++; changed = true; }
            }
            return true;
          });
          if(changed) {
            event.stopImmediatePropagation();
            if(frames.length) {redispatch = true; this.dispatchEvent(new MessageEvent('message', {data: JSON.stringify(frames)})); redispatch = false;}
          }
        });
      }
    };
    const fetch = window.fetch.bind(window);
    window.fetch = (input, init) => {if(fault.http && String(input instanceof Request ? input.url : input).includes('/bounded')) {fault.httpSeen = true; return Promise.resolve(new Response('Temporarily unavailable', {status: 503}));} return fetch(input, init);};
  });
  await t3.open();
  await browser.locator("li[data-thread-item]").filter({hasText: "Anchor"}).first().tap();
  await screen.getByText("Anchor conversation", {exact: true}).first().waitFor();
  const anchorUrl = await browser.url();
  await browser.evaluate(() => { (window as any).__fault.holdShell = true; });
  await t3.thread({project, title: "Recovery target", messages: ["This conversation should recover automatically"]});
  const state = makeState(T3_HOME);
  const listed = await runPromise(state.cli(["thread", "list", "--project", project, "--json"]));
  const target = JSON.parse(listed.stdout).find((t: any) => t.title === "Recovery target");
  if(!target) throw new Error("Target thread was not created");
  await browser.evaluate(() => {const f = (window as any).__fault; f.baselineSockets = f.sockets; f.http = true; f.armed = true;});
  // Keep the document and its healthy socket alive while selecting the unlisted thread.
  await browser.evaluate((path) => {window.history.pushState({}, '', path); window.dispatchEvent(new PopStateEvent('popstate'));}, new URL(anchorUrl).pathname.replace(/[^/]+$/, target.threadId));
  await browser.evaluate(async () => {
    const f = (window as any).__fault;
    await new Promise<void>((resolve, reject) => {
      const end = Date.now() + 15000;
      const timer = setInterval(() => {if(f.corrupted === 1) {clearInterval(timer); resolve();} else if(Date.now() > end) {clearInterval(timer); reject(new Error('No snapshot corrupted: '+JSON.stringify(f)));}}, 50);
    });
  });
  await app.screenshot("fault-cleared");
  try {
    await expect(screen.getByText("This conversation should recover automatically", {exact: true}).first()).toBeVisible({timeout: 15000});
    await screen.getByText("Syncing messages...", {exact: true}).waitFor({state: "hidden"});
  } finally {
    await app.screenshot("automatic-recovery");
    console.log("Fault controls", await browser.evaluate(() => {
      const {log, ...f} = (window as any).__fault;
      if(f.sockets !== f.baselineSockets || document.visibilityState !== 'visible' || f.corrupted !== 1 || !f.httpSeen || !f.shellHeld) throw new Error('Invalid fault controls: '+JSON.stringify(f));
      return f;
    }));
  }
});
