import { expect } from "e2e";
import { spawn, execFileSync } from "node:child_process";
import { mkdirSync, writeFileSync, openSync, readFileSync } from "node:fs";
import { networkInterfaces, tmpdir } from "node:os";
import { join } from "node:path";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { test, WEB } from "./t3.ts";

test("A healthy Tailscale socket survives a LAN route that answers HTTP but rejects WebSocket", WEB, async ({ t3, app, screen, browser }) => {
  const addresses = Object.values(networkInterfaces()).flat().filter(Boolean).map((a) => a!.address);
  const tail = addresses.find((a) => a.startsWith("100."))!;
  const lans = addresses.filter((a) => a.startsWith("192.168.") || a.startsWith("10.77."));
  if (!tail || !lans.length) throw new Error("This scenario needs LAN and Tailscale interfaces");
  const port = 19442;
  const home = join(tmpdir(), "factory-issue-24516-evidence-2", `remote-${Date.now()}`);
  mkdirSync(join(home, "userdata"), { recursive: true });
  writeFileSync(join(home, "userdata/settings.json"), JSON.stringify({ providerInstances: Object.fromEntries(["codex", "claudeAgent", "cursor", "grok", "muse", "pi", "opencode", "antigravity"].map((driver) => [driver, {driver, enabled: false}])) }));
  const cli = (...args: string[]) => execFileSync(process.execPath, ["apps/server/src/bin.ts", ...args, "--base-dir", home], { cwd: APP_DIR, encoding: "utf8" });
  const firewall = (action: string, ip: string) => execFileSync("sudo", ["-n", "iptables", action, "OUTPUT", "-p", "tcp", "-d", ip, "--dport", String(port), "-m", "string", "--algo", "bm", "--string", "Upgrade: websocket", "-j", "REJECT", "--reject-with", "tcp-reset"]);
  let child: ReturnType<typeof spawn> | undefined;
  let blocked = false;
  try {
    await t3.project("Connection continuity");
    await t3.open();
    await screen.getByText("Connection continuity", {exact:true}).first().waitFor({timeout:60000});
    await app.open("/settings/connections");
    const origin = new URL(await browser.url()).origin;
    const log = openSync(join(home, "server.log"), "w");
    child = spawn(process.execPath, ["apps/server/src/bin.ts", "--base-dir", home, "--host", "0.0.0.0", "--port", String(port), "--no-browser", "--dev-url", origin], { cwd: APP_DIR, env: { ...process.env, PATH: `${T3_HOME}/bin:${process.env.PATH}`, T3_SERVICE_LAUNCHER_CONTEXT: "", T3_BOOT_SERVICE_UNIT: "", T3CODE_TELEMETRY_ENABLED: "false", T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "0", T3CODE_DEV_SEED: "0", T3CODE_DEV_ALLOWED_ORIGINS: origin }, stdio: ["ignore", log, log] });
    await new Promise<void>((resolve, reject) => {
      const deadline = setTimeout(() => { clearInterval(timer); reject(new Error("Remote did not start")); }, 120000);
      const timer = setInterval(async () => {
        try { if ((await fetch(`http://127.0.0.1:${port}/.well-known/t3/environment`)).ok) { clearInterval(timer); clearTimeout(deadline); resolve(); } } catch {}
      }, 500);
    });
    await browser.evaluate(({ port, tail }) => {
      const w = window as any;
      const events: {time: number; text: string; kind: string; id?: number}[] = [];
      const sockets = new Map<number, WebSocket>();
      let sequence = 0;
      const start = Date.now();
      const panel = document.createElement("pre");
      panel.id = "connection-observer";
      panel.style.cssText = "position:fixed;right:18px;bottom:18px;z-index:99999;background:#111827;color:#e5e7eb;border:1px solid #64748b;padding:16px;border-radius:8px;font:13px/1.55 monospace;width:570px;white-space:pre-wrap;pointer-events:none";
      document.body.append(panel);
      const render = () => { const o = w.socketObserver; const original = o?.liveId ? `Original Tailscale socket #${o.liveId}: ${sockets.get(o.liveId)?.readyState === 1 ? "OPEN" : "CLOSED"}\n` : ""; panel.textContent = "NETWORK OBSERVER · real browser sockets\nLAN HTTP allowed · LAN WebSocket upgrades reset\n" + original + events.filter((e) => e.kind !== "attempt" && e.kind !== "LAN-close").slice(-10).map((e) => `${((e.time-start)/1000).toFixed(1).padStart(6)}s  ${e.text}`).join("\n"); };
      const record = (text: string, kind: string, id?: number) => { events.push({time: Date.now(), text, kind, id}); render(); window.dispatchEvent(new Event("socket-observer-event")); };
      const Native = window.WebSocket;
      window.WebSocket = class extends Native {
        constructor(url: string | URL, protocols?: string | string[]) {
          super(url, protocols);
          const parsed = new URL(String(url));
          if (parsed.port !== String(port)) return;
          const id = ++sequence;
          const route = parsed.hostname === tail ? "Tailscale" : "LAN";
          sockets.set(id, this);
          record(`${route} socket #${id}: connecting`, "attempt", id);
          this.addEventListener("open", () => record(`${route} socket #${id}: OPEN`, `${route}-open`, id));
          this.addEventListener("close", () => record(`${route} socket #${id}: CLOSED`, `${route}-close`, id));
          this.addEventListener("error", () => record(`${route} socket #${id}: upgrade failed`, "upgrade-failed", id));
        }
      };
      const nativeFetch = window.fetch;
      window.fetch = async (...args) => {
        const result = await nativeFetch(...args);
        const url = new URL(String(args[0] instanceof Request ? args[0].url : args[0]), location.href);
        if (url.port === String(port) && url.hostname !== tail && url.pathname === "/.well-known/t3/environment") record(`LAN reachability: HTTP ${result.status}`, "lan-check");
        return result;
      };
      w.socketObserver = { events, sockets, record, liveId: null };
      render();
    }, {port, tail});
    const pair = async (ip: string, adding: boolean) => {
      const issued = JSON.parse(cli("auth", "pairing", "create", "--base-url", `http://${ip}:${port}`, "--json"));
      await screen.getByRole("button", adding ? "Add route" : "Add environment", {exact: true}).tap();
      await screen.getByRole("textbox", "Host", {exact: true}).fill(issued.pairUrl);
      await screen.getByRole("button", adding ? "Add route" : "Add environment", {exact: true}).last().tap();
      await screen.getByRole("textbox", "Host", {exact: true}).waitFor({state:"hidden"});
    };
    await pair(tail, false);
    await screen.getByRole("button", "Routes", {exact:true}).tap();
    await pair(lans[0], true);
    await screen.getByText("In use", {exact:true}).waitFor();
    await app.screenshot("lan-connected");
    for (const ip of lans) firewall("-I", ip);
    blocked = true;
    execFileSync("sudo", ["-n", "ss", "-K", "dst", lans[0], "dport", "=", String(port)]);
    await browser.evaluate(() => new Promise<void>((resolve, reject) => {
      const w = window as any;
      const o = w.socketObserver;
      const check = () => {
        const event = o.events.findLast((e: any) => e.kind === "Tailscale-open" && o.sockets.get(e.id)?.readyState === 1);
        if (event) { window.removeEventListener("socket-observer-event", check); clearTimeout(timeout); o.liveId = event.id; o.mark = o.events.length; o.record(`Monitoring healthy Tailscale socket #${event.id}`, "monitor"); resolve(); }
      };
      const timeout = setTimeout(() => reject(new Error("No healthy Tailscale fallback")), 45000);
      window.addEventListener("socket-observer-event", check);
      check();
    }));
    await app.screenshot("tailnet-fallback");
    // Observe a full real 60-second better-route interval, without changing the app clock.
    await new Promise((resolve) => setTimeout(resolve, 75000));
    const result = await browser.evaluate(() => {
      const o = (window as any).socketObserver;
      const monitored = o.events.slice(o.mark);
      const closed = monitored.filter((e: any) => e.kind === "Tailscale-close" && e.id === o.liveId).length;
      const checks = monitored.filter((e: any) => e.kind === "lan-check").length;
      const failed = monitored.filter((e: any) => e.kind === "upgrade-failed").length;
      o.record(`Original Tailscale socket closed: ${closed} · LAN checks: ${checks}`, "result");
      return {closed, checks, failed, events: o.events.map((e: any) => ({text:e.text, kind:e.kind, seconds:(e.time-o.events[0].time)/1000})), originalSocket: o.liveId};
    });
    writeFileSync(join(home, "observation.json"), JSON.stringify(result, null, 2));
    const stillA = await app.screenshot("stable-a");
    await new Promise((resolve) => setTimeout(resolve, 500));
    const stillB = await app.screenshot("stable-b");
    if (!readFileSync(stillA).equals(readFileSync(stillB))) throw new Error("Final capture was not stable across 500 ms");
    await app.screenshot("after-lan-recheck");
    if (result.checks === 0 || result.failed === 0) throw new Error(`LAN fault was not exercised: ${JSON.stringify(result)}`);
    expect(result.closed).toBe(0);
  } finally {
    if (blocked) for (const ip of lans) firewall("-D", ip);
    if (child && child.exitCode === null) {
      child.kill("SIGTERM");
      await new Promise<void>((resolve) => child!.once("exit", () => resolve()));
    }
  }
});
