import { expect } from "e2e";
import * as fs from "node:fs";
import * as path from "node:path";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";
import { test, WEB, makeState, runPromise } from "./t3.ts";

// These synthetic replay recordings produce the same protocol events on each commit.
const frame = (
  kind: string,
  method: string,
  params?: unknown,
  result?: unknown,
  afterMs?: number,
) => ({
  type: kind === "out" ? "expect_outbound" : "emit_inbound",
  ...(afterMs ? { afterMs } : {}),
  frame: {
    kind: kind === "out" ? "request" : kind,
    method,
    ...(params ? { params } : {}),
    ...(result ? { result } : {}),
  },
});
const update = (text: string, afterMs = 30) =>
  frame(
    "notification",
    "session/update",
    {
      sessionId: "session",
      update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } },
    },
    undefined,
    afterMs,
  );
const prompt = frame("out", "session/prompt", {});
const end = frame("response", "session/prompt", undefined, { stopReason: "end_turn" });
const fixtures = path.join(APP_DIR, "apps/server/src/orchestration-v2/testkit/fixtures");
const write = (name: string, entries: unknown[]) =>
  fs.writeFileSync(
    path.join(fixtures, name, "registry_transcript.ndjson"),
    entries.map((x) => JSON.stringify(x)).join("\n"),
  );

test(
  "An idle parent is notified when its delegated child needs command approval",
  WEB,
  async ({ t3, app, screen, browser }) => {
    write("multi_turn", [
      prompt,
      update("I will delegate the timestamp task and finish without polling. "),
      ...Array.from({ length: 4 }, () => update(" ", 2000)),
      update("The child is running. My turn is complete."),
      end,
      prompt,
      update(
        "The delegated child is waiting for your approval. Approve or decline date > stamp.txt in the app.",
      ),
      end,
    ]);
    write("tool_call_read_only_on_request", [
      prompt,
      ...Array.from({ length: 20 }, () => update(" ", 2000)),
      frame("notification", "session/update", {
        sessionId: "session",
        update: {
          sessionUpdate: "tool_call",
          toolCallId: "stamp",
          title: "date > stamp.txt",
          kind: "execute",
          status: "pending",
          rawInput: { command: "date > stamp.txt" },
        },
      }),
      frame("request", "session/request_permission", {
        sessionId: "session",
        toolCall: {
          toolCallId: "stamp",
          title: "date > stamp.txt",
          kind: "execute",
          status: "pending",
          content: [{ type: "content", content: { type: "text", text: "Run date > stamp.txt" } }],
        },
        options: [
          { optionId: "allow", name: "Allow once", kind: "allow_once" },
          { optionId: "reject", name: "Decline", kind: "reject_once" },
        ],
      }),
      end,
    ]);
    const agentFile = path.join(APP_DIR, "apps/server/scripts/dev-replay-agent.ts");
    let source = fs.readFileSync(agentFile, "utf8");
    source = source.replace(
      "const sessions = new Map<",
      "const mcpConfigs = new Map();\n  const sessions = new Map<",
    );
    source = source.replace(
      "sessions.set(sessionId, { cwd,",
      "mcpConfigs.set(sessionId, params.mcpServers);\n        sessions.set(sessionId, { cwd,",
    );
    source = source.replace(
      "const turns = loadTurns(session.model);",
      `
        if(session.model === "multi-turn" && session.promptCount === 0) {
          const config = mcpConfigs.get(sessionId)?.find((s) => s.name === "t3-code");
          const env = Object.fromEntries((config?.env ?? []).map((e) => [e.name,e.value]));
          let mcpSession;
          let rpcId = 0;
          const call = async (method, params) => {
            const response = await fetch(env.T3_ACP_MCP_ENDPOINT, {method:"POST",headers:{"content-type":"application/json",accept:"application/json, text/event-stream",authorization:env.T3_ACP_MCP_AUTHORIZATION,...(mcpSession ? {"mcp-session-id":mcpSession} : {})},body:JSON.stringify({jsonrpc:"2.0",id:++rpcId,method,params})});
            mcpSession = response.headers.get("mcp-session-id") ?? mcpSession;
            const text = await response.text();
            const payload = text.startsWith("data:") ? JSON.parse(text.split("\\n").find(l=>l.startsWith("data:")).slice(5)) : JSON.parse(text);
            if(payload.error || payload.result?.isError) throw new Error(JSON.stringify(payload));
            return payload.result;
          };
          await call("initialize",{protocolVersion:"2025-06-18",capabilities:{},clientInfo:{name:"scenario-replay",version:"1"}});
          await call("tools/call",{name:"orchestrator_capabilities",arguments:{}});
          await call("tools/call",{name:"delegate_task",arguments:{task:"Run date > stamp.txt",title:"Timestamp child",mode:"async",runtimeMode:"approval-required",target:{providerInstanceId:"devReplay",model:"approval"}}});
        }
        const turns = loadTurns(session.model);`,
    );
    fs.writeFileSync(agentFile, source);
    await t3.open();
    await screen.getByRole("button", "Start", { exact: true }).tap();
    await screen
      .getByRole("button", "Add project")
      .first()
      .waitFor({ state: "visible", timeout: 60000 });
    const project = await t3.project("approval-demo");
    const { cli } = makeState(T3_HOME);
    const started = await runPromise(
      cli([
        "thread",
        "start",
        project,
        "Delegate the timestamp write, then finish without polling.",
        "--title",
        "Timestamp parent",
        "--provider",
        "devReplay",
        "--model",
        "multi-turn",
        "--json",
      ]),
    );
    const { threadId: parentId } = JSON.parse(started.stdout);
    const environmentId = fs
      .readFileSync(path.join(T3_HOME, "userdata/environment-id"), "utf8")
      .trim();
    await app.open(`/${environmentId}/${encodeURIComponent(parentId)}`);
    await screen
      .getByText("Subagent needs approval", { exact: false })
      .first()
      .waitFor({ state: "visible", timeout: 60000 });
    await screen
      .getByText("My turn is complete.", { exact: false })
      .first()
      .waitFor({ state: "visible", timeout: 60000 });
    const stableCapture = async (label: string) => {
      await browser.evaluate(() => document.getAnimations().forEach((a) => a.pause()));
      for (let i = 0; i < 12; i++) {
        const first = await app.screenshot(`${label}-stability-${i}`);
        await new Promise((resolve) => setTimeout(resolve, 500));
        const second = await app.screenshot(label);
        if (fs.readFileSync(first).equals(fs.readFileSync(second))) return;
      }
      throw new Error("The capture did not settle");
    };
    await stableCapture("parent-after-child-approval");
    await expect(
      screen
        .getByText("The delegated child is waiting for your approval.", { exact: false })
        .first(),
    ).toBeVisible({ timeout: 15000 });
    await stableCapture("parent-notification");
  },
);
