/**
 * Chat page tests -- MEDIUM tier.
 *
 * Starting state, NDJSON streaming contract (not SSE), assistant
 * bubble grows as chunks arrive, error path, and mobile viewport.
 */

import { test, expect, type FrigateApp } from "../fixtures/frigate-test";

/**
 * Install a window.fetch override on the page so that POSTs to
 * chat/completion resolve with a real ReadableStream that emits the
 * given chunks over time. This is the only way to validate
 * chunk-by-chunk rendering through Playwright — page.route() does not
 * support streaming responses.
 *
 * Must be called BEFORE frigateApp.goto(). The override also exposes
 * `__chatRequests` on window so tests can assert the outgoing body.
 */
async function installChatStreamOverride(
  app: FrigateApp,
  chunks: Array<Record<string, unknown>>,
  opts: { chunkDelayMs?: number; status?: number } = {},
) {
  const { chunkDelayMs = 40, status = 200 } = opts;
  await app.page.addInitScript(
    ({ chunks, chunkDelayMs, status }) => {
      (window as unknown as { __chatRequests: unknown[] }).__chatRequests = [];
      const origFetch = window.fetch;
      window.fetch = async (input, init) => {
        const url =
          typeof input === "string"
            ? input
            : input instanceof URL
              ? input.toString()
              : (input as Request).url;
        if (url.includes("chat/completion")) {
          const body =
            init?.body instanceof String || typeof init?.body === "string"
              ? JSON.parse(init!.body as string)
              : null;
          (
            window as unknown as { __chatRequests: unknown[] }
          ).__chatRequests.push({ url, body });
          if (status !== 200) {
            return new Response(JSON.stringify({ error: "boom" }), {
              status,
            });
          }
          const encoder = new TextEncoder();
          const stream = new ReadableStream({
            async start(controller) {
              for (const chunk of chunks) {
                await new Promise((r) => setTimeout(r, chunkDelayMs));
                controller.enqueue(
                  encoder.encode(JSON.stringify(chunk) + "\n"),
                );
              }
              controller.close();
            },
          });
          return new Response(stream, { status: 200 });
        }
        return origFetch.call(window, input as RequestInfo, init);
      };
    },
    { chunks, chunkDelayMs, status },
  );
}

test.describe("Chat — starting state @medium", () => {
  test("empty message list renders ChatStartingState with title and input", async ({
    frigateApp,
  }) => {
    await frigateApp.goto("/chat");
    await expect(
      frigateApp.page.getByRole("heading", { level: 1 }),
    ).toBeVisible({ timeout: 10_000 });
    await expect(frigateApp.page.getByPlaceholder(/ask/i)).toBeVisible();
    // Four quick-reply buttons from starting_requests.*
    const quickReplies = frigateApp.page.locator(
      "button:has-text('Show recent events'), button:has-text('Show camera status'), button:has-text('What happened'), button:has-text('Watch')",
    );
    await expect(quickReplies.first()).toBeVisible({ timeout: 5_000 });
  });
});

test.describe("Chat — streaming @medium", () => {
  test("submission POSTs to chat/completion with stream: true", async ({
    frigateApp,
  }) => {
    await installChatStreamOverride(frigateApp, [
      { type: "content", delta: "Hel" },
      { type: "content", delta: "lo" },
      {
        type: "messages",
        messages: [
          { role: "system", content: "sys" },
          { role: "user", content: "hello chat" },
          { role: "assistant", content: "Hello" },
        ],
      },
      { type: "done" },
    ]);
    await frigateApp.goto("/chat");
    const input = frigateApp.page.getByPlaceholder(/ask/i);
    await expect(input).toBeVisible({ timeout: 10_000 });
    await input.fill("hello chat");
    await input.press("Enter");

    await expect
      .poll(
        async () =>
          frigateApp.page.evaluate(
            () =>
              (window as unknown as { __chatRequests: unknown[] })
                .__chatRequests?.length ?? 0,
          ),
        { timeout: 5_000 },
      )
      .toBeGreaterThan(0);

    const request = await frigateApp.page.evaluate(
      () =>
        (
          window as unknown as {
            __chatRequests: Array<{
              url: string;
              body: { stream: boolean; messages: Array<{ content: string }> };
            }>;
          }
        ).__chatRequests[0],
    );
    expect(request.body.stream).toBe(true);
    expect(
      request.body.messages[request.body.messages.length - 1].content,
    ).toBe("hello chat");
  });

  test("NDJSON content chunks accumulate in the assistant bubble", async ({
    frigateApp,
  }) => {
    await installChatStreamOverride(
      frigateApp,
      [
        { type: "content", delta: "Hel" },
        { type: "content", delta: "lo, " },
        { type: "content", delta: "world!" },
        {
          type: "messages",
          messages: [
            { role: "system", content: "sys" },
            { role: "user", content: "greet me" },
            { role: "assistant", content: "Hello, world!" },
          ],
        },
        { type: "done" },
      ],
      { chunkDelayMs: 50 },
    );
    await frigateApp.goto("/chat");
    const input = frigateApp.page.getByPlaceholder(/ask/i);
    await expect(input).toBeVisible({ timeout: 10_000 });
    await input.fill("greet me");
    await input.press("Enter");

    await expect(frigateApp.page.getByText(/Hello, world!/i)).toBeVisible({
      timeout: 10_000,
    });
  });

  test("tool calls in the chain render a ToolCallsGroup", async ({
    frigateApp,
  }) => {
    const toolTurn = [
      { role: "system", content: "sys" },
      { role: "user", content: "find people" },
      {
        role: "assistant",
        content: null,
        tool_calls: [
          {
            id: "call_1",
            type: "function",
            function: {
              name: "search_objects",
              arguments: '{"label":"person"}',
            },
          },
        ],
      },
      { role: "tool", tool_call_id: "call_1", content: "[]" },
    ];
    await installChatStreamOverride(frigateApp, [
      { type: "messages", messages: toolTurn },
      { type: "content", delta: "Searching for people." },
      {
        type: "messages",
        messages: [
          ...toolTurn,
          { role: "assistant", content: "Searching for people." },
        ],
      },
      { type: "done" },
    ]);
    await frigateApp.goto("/chat");
    const input = frigateApp.page.getByPlaceholder(/ask/i);
    await expect(input).toBeVisible({ timeout: 10_000 });
    await input.fill("find people");
    await input.press("Enter");

    // ToolCallsGroup normalizes "search_objects" → "Search Objects" via
    // normalizeName(). Match the rendered display label instead.
    await expect(frigateApp.page.getByText(/search objects/i)).toBeVisible({
      timeout: 10_000,
    });
    await expect(
      frigateApp.page.getByText(/searching for people/i),
    ).toBeVisible({ timeout: 5_000 });
  });
});

test.describe("Chat — stop @medium", () => {
  test("Stop button aborts an in-flight stream and freezes the partial message", async ({
    frigateApp,
  }) => {
    // A long chunk sequence with big delays gives us time to hit Stop.
    await installChatStreamOverride(
      frigateApp,
      [
        { type: "content", delta: "First chunk. " },
        { type: "content", delta: "Second chunk. " },
        { type: "content", delta: "Third chunk. " },
      ],
      { chunkDelayMs: 300 },
    );
    await frigateApp.goto("/chat");
    const input = frigateApp.page.getByPlaceholder(/ask/i);
    await expect(input).toBeVisible({ timeout: 10_000 });
    await input.fill("slow response please");
    await input.press("Enter");

    // Wait for the first chunk to render
    await expect(frigateApp.page.getByText(/First chunk\./)).toBeVisible({
      timeout: 10_000,
    });

    // The Stop button is a destructive rounded button shown while isLoading.
    // It contains only an FaStop SVG icon (no visible text). Find it by the
    // destructive variant class or fall back to aria-label.
    const stopBtn = frigateApp.page
      .locator("button.bg-destructive, button[class*='destructive']")
      .first();
    await stopBtn.click({ timeout: 3_000 }).catch(async () => {
      await frigateApp.page
        .getByRole("button", { name: /stop|cancel/i })
        .first()
        .click();
    });

    // Third chunk should never appear.
    await expect(frigateApp.page.getByText(/Third chunk\./)).toHaveCount(0);
  });
});

test.describe("Chat — error @medium", () => {
  test("non-OK response renders an error banner", async ({ frigateApp }) => {
    await installChatStreamOverride(frigateApp, [], { status: 500 });
    await frigateApp.goto("/chat");
    const input = frigateApp.page.getByPlaceholder(/ask/i);
    await expect(input).toBeVisible({ timeout: 10_000 });
    await input.fill("trigger error");
    await input.press("Enter");
    // The error banner is a role="alert" paragraph; target by role so we
    // don't collide with the user-message bubble that contains "trigger
    // error" (which would match /error/ in strict mode).
    await expect(
      frigateApp.page.getByRole("alert").filter({
        hasText: /boom|something went wrong/i,
      }),
    ).toBeVisible({ timeout: 5_000 });
  });
});

test.describe("Chat — attachment chip @medium", () => {
  test("attaching an event renders a ChatAttachmentChip", async ({
    frigateApp,
  }) => {
    // The chat starts with an empty message list (ChatStartingState).
    // After sending a message, ChatEntry with the paperclip button appears.
    // We use the stream override so the first message completes quickly.
    await installChatStreamOverride(frigateApp, [
      { type: "content", delta: "Done." },
      {
        type: "messages",
        messages: [
          { role: "system", content: "sys" },
          { role: "user", content: "hello" },
          { role: "assistant", content: "Done." },
        ],
      },
      { type: "done" },
    ]);
    await frigateApp.goto("/chat");

    // Send a first message to transition out of ChatStartingState so the
    // full ChatEntry (with the paperclip) is visible.
    const input = frigateApp.page.getByPlaceholder(/ask/i);
    await expect(input).toBeVisible({ timeout: 10_000 });
    await input.fill("hello");
    await input.press("Enter");
    // Wait for the assistant response to complete so isLoading becomes false
    // and the paperclip button is re-enabled.
    await expect(frigateApp.page.getByText(/Done\./i)).toBeVisible({
      timeout: 10_000,
    });

    // The paperclip button has aria-label from t("attachment_picker_placeholder")
    // = "Attach an event".
    const paperclip = frigateApp.page
      .getByRole("button", { name: /attach an event/i })
      .first();
    await expect(paperclip).toBeVisible({ timeout: 5_000 });
    await paperclip.click();

    // The popover shows a paste input with placeholder "Or paste event ID".
    const idInput = frigateApp.page
      .locator('input[placeholder*="event" i], input[aria-label*="attach" i]')
      .first();
    await expect(idInput).toBeVisible({ timeout: 3_000 });
    await idInput.fill("test-event-1");
    await frigateApp.page
      .getByRole("button", { name: /^attach$/i })
      .first()
      .click();

    // The ChatAttachmentChip renders in the composer area. It shows an
    // activity indicator while loading event data (event_ids API not mocked),
    // so assert on the chip container being present in the composer.
    await expect(
      frigateApp.page.locator(
        "[class*='inline-flex'][class*='rounded-lg'][class*='border']",
      ),
    ).toBeVisible({ timeout: 5_000 });
  });
});

test.describe("Chat — mobile @medium @mobile", () => {
  test.skip(({ frigateApp }) => !frigateApp.isMobile, "Mobile-only");

  test("chat input is focusable at mobile viewport", async ({ frigateApp }) => {
    await frigateApp.goto("/chat");
    const input = frigateApp.page.getByPlaceholder(/ask/i);
    await expect(input).toBeVisible({ timeout: 10_000 });
    await input.focus();
    await expect(input).toBeFocused();
  });
});
