/**
 * Reload mid-stream: while a Task streams a long tool output (or a long assistant text),
 * refreshing the page must bring the in-progress message straight back — the already
 * streamed prefix visible promptly (before the stream finishes), the content still
 * growing live, and the final state identical to a run that was never reloaded.
 *
 * Mechanics under test: GET /messages returns `live` ({cursor, fragments}) while the
 * Task runs; the frontend seeds the synthetic `partial_* start` fragments on top of
 * history and drops the buffered partials the snapshot already covers.
 *
 * The LLM is mock-llm.mjs: "slow stream test" makes it call exec_command with a command
 * that prints one line every 200ms for ~8s (real tool execution, really streamed);
 * "slow text test" streams a 40-chunk text one delta every 200ms.
 */
import { test, expect } from "@playwright/test";
import { provisionAndLogin } from "./auth.mjs";

const BASE = process.env.BASE_URL;
const MOCK = process.env.MOCK_URL;
const U = "reloaduser";
const P = "password123";

/** Create a session for the user's auto-provisioned project (models PUT is idempotent). */
async function createSession(page, approvalMode) {
  const projects = await (await page.request.get(`${BASE}/api/projects`)).json();
  const projectId = projects.projects[0].projectId;
  const put = await page.request.put(`${BASE}/api/projects/${projectId}/models`, {
    data: {
      defaultModel: { provider: "custom", modelId: "claude-4-8" },
      models: [
        {
          provider: "custom",
          modelId: "claude-4-8",
          apiKey: "sk-mock",
          baseUrl: MOCK,
          contextWindow: 200000,
        },
      ],
    },
  });
  expect(put.ok(), "put models").toBeTruthy();
  const res = await page.request.post(
    `${BASE}/api/projects/${projectId}/agents/default_agent/sessions`,
    { data: { provider: "custom", modelId: "claude-4-8", approvalMode } },
  );
  expect(res.ok(), `create session: ${await res.text()}`).toBeTruthy();
  return (await res.json()).session.sessionId;
}

/**
 * Keep the running work group open past the end of the turn (it auto-collapses at turn
 * end unless the user toggled it — same trick as chat.spec), then expand the
 * exec_command tool card and return the output <pre> locator.
 */
async function openToolOutput(page) {
  const group = page
    .locator("button[aria-expanded]")
    .filter({ hasText: /运行中|运行完毕/ })
    .first();
  await expect(group).toBeVisible();
  if ((await group.textContent())?.includes("运行中")) {
    await group.click(); // toggle → marks the group user-toggled
    await group.click(); // toggle back → deliberately kept open, survives turn end
  } else if ((await group.getAttribute("aria-expanded")) !== "true") {
    await group.click(); // finished and collapsed: open it to reach the card
  }
  const toolCard = page
    .locator("button[aria-expanded]")
    .filter({ hasText: "exec_command" })
    .first();
  await expect(toolCard).toBeVisible();
  if ((await toolCard.getAttribute("aria-expanded")) !== "true") await toolCard.click();
  await expect(toolCard).toHaveAttribute("aria-expanded", "true");
  // Two <pre>s live in the expanded card (arguments, then output); the output one is the
  // only one containing a literal "line 1".
  return page.locator("pre", { hasText: /line 1\b/ }).first();
}

/** Send the "slow stream test" prompt, approve exec_command, and return the output <pre>. */
async function startSlowToolRun(page, sessionId) {
  await page.goto(`${BASE}/chat/${sessionId}`);
  const ta = page.getByPlaceholder(/输入消息/);
  await ta.waitFor();
  await ta.fill("slow stream test");
  await page.getByRole("button", { name: "发送" }).click();
  await expect(page.getByText("exec_command").first()).toBeVisible();
  await page.getByRole("button", { name: "允许" }).click();
  const outputPre = await openToolOutput(page);
  await expect(outputPre).toContainText("line 3");
  return outputPre;
}

test("in-progress tool output survives a reload: prefix back promptly, still growing, final state matches a never-reloaded run", async ({
  page,
}) => {
  await provisionAndLogin(page.request, U, P);

  // --- Control run (never reloaded): capture the final tool output to compare against ---
  const controlId = await createSession(page, "always-ask");
  await startSlowToolRun(page, controlId);
  await expect(page.getByText("Command finished; the result looks as expected.")).toBeVisible({
    timeout: 30_000,
  });
  const controlPre = await openToolOutput(page);
  const controlOutput = await controlPre.textContent();
  expect(controlOutput).toContain("line 40");

  // --- The run under test: reload while the output is streaming ---
  const sessionId = await createSession(page, "always-ask");
  await startSlowToolRun(page, sessionId);
  await page.reload();

  // (a) The already-streamed prefix is back promptly — well before the ~8s command ends.
  const outputPre = await openToolOutput(page);
  await expect(outputPre).toContainText("line 2", { timeout: 4000 });
  // Not finished yet: what we see is genuinely the in-progress stream, not the final state.
  await expect(outputPre).not.toContainText("line 40");

  // (b) The output keeps growing live on the same connection.
  await expect(outputPre).toContainText("line 35", { timeout: 15_000 });

  // (c) After completion the final state matches the never-reloaded run exactly
  //     (every line exactly once — no lost prefix, no duplicated overlap).
  await expect(page.getByText("Command finished; the result looks as expected.")).toBeVisible({
    timeout: 30_000,
  });
  const reloadedOutput = await (await openToolOutput(page)).textContent();
  expect(reloadedOutput).toBe(controlOutput);
});

test("in-progress assistant TEXT survives a reload and keeps streaming", async ({ page }) => {
  await provisionAndLogin(page.request, U, P);
  const sessionId = await createSession(page, "allow-all");

  await page.goto(`${BASE}/chat/${sessionId}`);
  const ta = page.getByPlaceholder(/输入消息/);
  await ta.waitFor();
  await ta.fill("slow text test");
  await page.getByRole("button", { name: "发送" }).click();

  const reply = page.locator(".md-body", { hasText: /chunk-1\s/ }).first();
  await expect(reply).toContainText("chunk-3 ");
  await page.reload();

  // (a) The streamed prefix is back promptly, while the ~8s stream is still going.
  const reloaded = page.locator(".md-body", { hasText: /chunk-1\s/ }).first();
  await expect(reloaded).toContainText("chunk-2 ", { timeout: 4000 });
  await expect(reloaded).not.toContainText("chunk-40");

  // (b) It keeps streaming live, up to the full reply.
  await expect(reloaded).toContainText("chunk-40", { timeout: 15_000 });

  // (c) No duplicated prefix: the buffered partials the snapshot covered were dropped,
  //     so every chunk appears exactly once.
  const text = await reloaded.textContent();
  expect(text.match(/chunk-5 /g)).toHaveLength(1);
  expect(text.match(/chunk-1 /g)).toHaveLength(1);
});
