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

test(
  "Parent lifecycle cascades through nested subagents and updates Archive without reload",
  WEB,
  async ({ t3, app, screen, browser }) => {
    const project = await t3.project("Lifecycle demonstration");
    const parent = await t3.thread({
      project,
      title: "Lifecycle parent",
      model: "question",
      messages: ["Delegate research and a nested research task."],
    });
    await t3.open(parent);
    await screen
      .getByRole("button", "Thread actions for Lifecycle parent")
      .waitFor({ timeout: 120000 });
    const environmentId = await browser.evaluate(() => location.pathname.split("/")[1]);
    const parentPath = `/${environmentId}/${parent.threadId}`;
    const rpc = async (tag: string, payload: unknown): Promise<any> =>
      browser.evaluate(
        async ({ tag, payload }) => {
          const ws = new WebSocket(
            `${location.origin.replace(/^http/, "ws")}/ws?orchestrationProtocol=2`,
          );
          return await new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
              ws.close();
              reject(new Error("RPC timeout: " + tag));
            }, 30000);
            ws.onopen = () =>
              ws.send(JSON.stringify({ _tag: "Request", id: "1", tag, payload, headers: [] }));
            ws.onerror = () => reject(new Error("WebSocket failed"));
            ws.onmessage = (e) => {
              for (const line of String(e.data).split("\n").filter(Boolean)) {
                const msg = JSON.parse(line);
                if (msg._tag === "Ping") ws.send(JSON.stringify({ _tag: "Pong" }));
                if (msg._tag === "Exit" && String(msg.requestId) === "1") {
                  clearTimeout(timer);
                  ws.close();
                  msg.exit._tag === "Success"
                    ? resolve(msg.exit.value)
                    : reject(new Error(JSON.stringify(msg.exit)));
                }
              }
            };
          });
        },
        { tag, payload } as any,
      );
    const command = (payload: any) =>
      rpc("orchestration.dispatchCommand", { commandId: crypto.randomUUID(), ...payload });
    const read = (threadId: string) => rpc("orchestration.getThreadProjection", { threadId });
    const wait = async (id: string, predicate: (p: any) => boolean) => {
      for (let i = 0; i < 100; i++) {
        const p = await read(id);
        if (predicate(p)) return p;
        await new Promise((r) => setTimeout(r, 200));
      }
      throw new Error("Thread never reached fixture milestone");
    };
    const delegate = async (id: string, title: string, model = "question") => {
      const p = await wait(
        id,
        (p) =>
          p.runs.some((r: any) => ["running", "waiting"].includes(r.status)) &&
          p.runs.at(-1)?.rootNodeId,
      );
      const run = p.runs.at(-1);
      const commandId = crypto.randomUUID();
      await command({
        type: "thread.create",
        threadId: `thread:delegated-task:${commandId}`,
        projectId: p.thread.projectId,
        title,
        modelSelection: { instanceId: "devReplay", model },
        runtimeMode: "full-access",
        interactionMode: "default",
        branch: null,
        worktreePath: null,
        createdBy: "agent",
        creationSource: "mcp",
      });
      await command({
        type: "delegated_task.request",
        commandId,
        parentThreadId: id,
        parentRunId: run.id,
        parentNodeId: run.rootNodeId,
        task: title,
        title,
        modelSelection: { instanceId: "devReplay", model },
        runtimeMode: "full-access",
        interactionMode: "default",
        completionWake: "settled_only",
        createdBy: "agent",
        creationSource: "mcp",
      });
      return (await read(id)).subagents.find((s: any) => s.prompt === title)
        .childThreadId as string;
    };
    const child = await delegate(parent.threadId, "Research child");
    const nested = await delegate(child, "Nested research", "multi-turn");
    const independent = await delegate(parent.threadId, "Independent archive", "multi-turn");
    await wait(nested, (p) => p.runs.at(-1)?.status === "completed");
    await wait(independent, (p) => p.runs.at(-1)?.status === "completed");
    // Complete the replay questions after the children have finished.
    for (const id of [child, parent.threadId]) {
      const p = await wait(id, (p) => p.runtimeRequests?.some((r: any) => r.status === "pending"));
      const request = p.runtimeRequests.find((r: any) => r.status === "pending");
      await command({
        type: "runtime-request.respond",
        threadId: id,
        requestId: request.id,
        answers: { schema_vs_ui_flexibility: "Strict schemas (Recommended)" },
      });
      await wait(id, (p) =>
        p.runs.every(
          (r: any) => !["running", "waiting", "starting", "preparing", "queued"].includes(r.status),
        ),
      );
    }
    const fork = crypto.randomUUID();
    await command({
      type: "thread.fork",
      sourceThreadId: parent.threadId,
      targetThreadId: fork,
      title: "Independent fork",
      sourcePoint: { type: "latest_stable" },
      createdBy: "user",
      creationSource: "web",
    });
    await command({ type: "thread.archive", threadId: independent });
    await app.open(parentPath);
    await screen.getByRole("button", "Thread actions for Lifecycle parent").tap();
    await screen.getByRole("menuitem", "Archive thread").tap();
    if (await screen.getByRole("button", "Confirm", { exact: true }).count())
      await screen.getByRole("button", "Confirm", { exact: true }).tap();
    await wait(parent.threadId, (p) => p.thread.archivedAt !== null);
    await new Promise((r) => setTimeout(r, 1500));
    await app.open("/settings/archived");
    await screen.getByRole("button", "Unarchive Lifecycle parent").waitFor({ state: "visible" });
    await app.screenshot("parent-archived");
    await new Promise((resolve) => setTimeout(resolve, 2000));
    const archived = await rpc("orchestration.getArchivedShellSnapshot", {});
    const archiveIds = archived.threads.map((t: any) => t.id);
    await screen.getByRole("button", "Unarchive Lifecycle parent").tap();
    await screen.getByRole("button", "Unarchive Lifecycle parent").waitFor({ state: "hidden" });
    await wait(child, (p) => p.thread.archivedAt === null);
    await wait(nested, (p) => p.thread.archivedAt === null);
    await app.screenshot("restored-without-reload");
    await new Promise((resolve) => setTimeout(resolve, 4000));
    const staleRows = await browser.evaluate(() =>
      [...document.querySelectorAll("button")]
        .map((x) => x.getAttribute("aria-label"))
        .filter((x) => x === "Unarchive Research child" || x === "Unarchive Nested research"),
    );
    const independentStillArchived = (await read(independent)).thread.archivedAt !== null;
    await app.open(parentPath);
    await screen.getByRole("button", "Thread actions for Lifecycle parent").tap();
    await screen.getByRole("menuitem", "Delete").tap();
    if (await screen.getByRole("button", "Confirm", { exact: true }).count())
      await screen.getByRole("button", "Confirm", { exact: true }).tap();
    await wait(parent.threadId, (p) => p.thread.deletedAt !== null);
    await new Promise((r) => setTimeout(r, 1500));
    await app.open("/settings/archived");
    await screen.getByRole("dialog", "Settings").waitFor();
    await screen
      .getByText("Loading archived threads", { exact: true })
      .waitFor({ state: "hidden" });
    await new Promise((r) => setTimeout(r, 1000));
    await app.screenshot("deleted-including-independent-archive");
    await new Promise((resolve) => setTimeout(resolve, 2000));
    const deleted = [];
    for (const id of [child, nested, independent])
      deleted.push((await read(id)).thread.deletedAt !== null);
    const forkSurvived = (await read(fork)).thread.deletedAt === null;
    await app.open(`/${environmentId}/${nested}`);
    await screen.getByRole("navigation", "Threads").waitFor();
    await new Promise((r) => setTimeout(r, 2000));
    await app.screenshot("deleted-nested-thread");
    if (process.env.T3_PROOF_OUTPUT)
      writeFileSync(
        `${process.env.T3_PROOF_OUTPUT}/observations-${Date.now()}.json`,
        JSON.stringify(
          {
            parent: parent.threadId,
            child,
            nested,
            independent,
            fork,
            archiveIds,
            staleRows,
            independentStillArchived,
            deleted,
            forkSurvived,
          },
          null,
          2,
        ),
      );
    await expect({
      archiveChildren: [child, nested].map((id) => archiveIds.includes(id)),
      staleRows,
      independentStillArchived,
      deleted,
      forkSurvived,
    }).toEqual({
      archiveChildren: [true, true],
      staleRows: [],
      independentStillArchived: true,
      deleted: [true, true, true],
      forkSurvived: true,
    });
  },
);
