import { expect } from "e2e";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { test, WEB, runPromise } from "./t3.ts";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { run } from "./shell.ts";

test(
  "Terminal excerpts preserve a hidden question draft",
  WEB,
  async ({ t3, app, screen, browser }) => {
    const capture = async (label: string) => {
      await browser.evaluate(() => {
        (document.activeElement as HTMLElement)?.blur();
        if (!document.querySelector("#evidence-style")) {
          const style = document.createElement("style");
          style.id = "evidence-style";
          style.textContent = "*{caret-color:transparent!important}";
          document.head.append(style);
        }
      });
      let previous = await app.screenshot(label + "-settle");
      for (let i = 0; i < 6; i++) {
        await new Promise((resolve) => setTimeout(resolve, 500));
        const current = await app.screenshot(label + (i === 0 ? "" : "-" + i));
        if (readFileSync(previous).equals(readFileSync(current))) return;
        previous = current;
      }
      throw Error("Capture did not settle: " + label);
    };
    const project = await t3.project("terminal-demo");
    await t3.thread({
      project,
      title: "Terminal question proof",
      messages: ["Prepare the workspace"],
    });
    await t3.open();
    await screen
      .getByTestId("sidebar-row-card")
      .first()
      .waitFor({ state: "visible", timeout: 120000 });
    await screen.getByTestId("sidebar-row-card").first().tap();
    await screen.getByRole("textbox").fill("draft-before");
    // Public recordings omit runtime metadata and the shell's initial filesystem prompt.
    await browser.evaluate(() => {
      const style = document.createElement("style");
      style.textContent =
        "[data-chat-composer-footer-controls]{visibility:hidden!important} [data-thread-terminal-drawer] canvas{visibility:hidden!important}";
      style.id = "privacy-style";
      document.head.append(style);
    });
    await screen.getByRole("button", "Toggle terminal drawer").tap();
    const terminal = screen.getByRole("textbox", "Terminal input");
    await terminal.press("Control+C");
    await terminal.fill("PS1=''; clear; printf 'hello\\n'");
    await terminal.press("Enter");
    await new Promise((resolve) => setTimeout(resolve, 500));
    await browser.evaluate(() => {
      document.querySelector("#privacy-style")!.textContent =
        "[data-chat-composer-footer-controls]{visibility:hidden!important}";
    });
    await capture("prepared-draft");
    // Configure a synthetic native replay: the real Codex adapter handles its async question.
    const threadId = await browser.evaluate(() =>
      decodeURIComponent(location.pathname.split("/").at(-1)!),
    );
    await browser.evaluate(
      async ({ threadId, binary }) => {
        const { ticket } = await (
          await fetch("/api/auth/websocket-ticket", { method: "POST" })
        ).json();
        const ws = new WebSocket(
          `${location.origin.replace(/^http/, "ws")}/ws?orchestrationProtocol=2&wsTicket=${encodeURIComponent(ticket)}`,
        );
        await new Promise<void>((resolve, reject) => {
          ws.onopen = () => resolve();
          ws.onerror = () => reject(Error("socket unavailable"));
        });
        let id = 0;
        const rpc = (tag: string, payload: unknown) =>
          new Promise((resolve, reject) => {
            const key = String(++id);
            const listener = (event: MessageEvent) => {
              const msg = JSON.parse(event.data);
              if (String(msg.requestId) !== key) return;
              if (msg._tag === "Exit") {
                ws.removeEventListener("message", listener);
                msg.exit._tag === "Success"
                  ? resolve(msg.exit.value)
                  : reject(Error(JSON.stringify(msg.exit)));
              }
            };
            ws.addEventListener("message", listener);
            ws.send(JSON.stringify({ _tag: "Request", id: key, tag, payload, headers: [] }));
          });
        await rpc("server.updateSettings", {
          patch: {},
          providerInstanceMutation: {
            operation: "upsert",
            instanceId: "codex",
            instance: { driver: "codex", enabled: true, config: { binaryPath: binary } },
          },
        });
        await rpc("orchestration.dispatchCommand", {
          type: "thread.model-selection.set",
          commandId: crypto.randomUUID(),
          threadId,
          modelSelection: { instanceId: "codex", model: "scenario" },
        });
        ws.close();
      },
      { threadId, binary: fileURLToPath(new URL("./codex-question-replay.cjs", import.meta.url)) },
    );
    await runPromise(
      run(
        process.execPath,
        [
          "apps/server/src/bin.ts",
          "thread",
          "send",
          threadId,
          "Ask which approach to use",
          "--base-dir",
          T3_HOME,
        ],
        { cwd: APP_DIR },
      ),
    );
    await screen
      .getByRole("button", "Dismiss question without answering")
      .waitFor({ state: "visible", timeout: 20000 });
    const add = async () => {
      const rect = await browser.evaluate(() => {
        const e = document.querySelector("[data-thread-terminal-drawer] canvas")!;
        const r = e.getBoundingClientRect();
        return { x: r.x, y: r.y, w: r.width, h: r.height };
      });
      await browser.mouse.move(rect.x + 1, rect.y + 9);
      await browser.mouse.down();
      await browser.mouse.move(rect.x + 45, rect.y + 9);
      await browser.mouse.up();
      await browser.locator("[data-thread-terminal-drawer] canvas").secondaryTap();
      await new Promise((resolve) => setTimeout(resolve, 700));
      await screen.getByText("Add to chat", { exact: true }).first().tap();
    };
    await add();
    await capture("question-visible");
    const notice = await screen
      .getByText("Terminal output can't be added to a question answer.", { exact: true })
      .count();
    const answer = await screen.getByTestId("composer-editor").textContent();
    await screen.getByRole("button", "Dismiss question without answering").tap();
    await capture("draft-after-dismissal");
    const chipCount = await browser.locator('[aria-label^="Terminal excerpt,"]').count();
    const draft = await screen.getByTestId("composer-editor").textContent();
    await add();
    await capture("normal-insertion");
    expect(chipCount).toBe(0);
    expect(draft).toBe("draft-before");
    expect(answer).toBe("");
    expect(notice).toBe(1);
    await expect(browser.locator('[aria-label^="Terminal excerpt,"]').first()).toBeVisible();
  },
);
