import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";

import {
  AssistantMarkdown,
  safeMarkdownUrl,
} from "@/components/chat/assistant-markdown";

afterEach(() => {
  cleanup();
  vi.restoreAllMocks();
});

describe("AssistantMarkdown", () => {
  it("allows only absolute HTTP(S) links and conservative document anchors", () => {
    const urlNode: Parameters<typeof safeMarkdownUrl>[2] = {
      type: "element",
      tagName: "a",
      properties: {},
      children: [],
    };
    const allowedUrls = [
      "https://example.com/docs?q=1",
      "http://example.com/docs",
      "#Details:part_2.value",
    ];
    const blockedUrls = [
      "/admin",
      "https://",
      "https:example.com",
      "https:/example.com",
      "https:///example.com",
      "https:\\example.com",
      "https:\\\\example.com",
      "h\nttps://example.com",
      "JaVaScRiPt:alert(1)",
      " javascript:alert(1)",
      "javascript&#58;alert(1)",
      "javascript%3Aalert(1)",
      "data:text/html,owned",
      "file:///etc/passwd",
      "blob:https://example.com/id",
      "#1invalid",
    ];

    expect(
      allowedUrls.map((url) => safeMarkdownUrl(url, "href", urlNode)),
    ).toEqual(allowedUrls);
    expect(
      blockedUrls.every(
        (url) => safeMarkdownUrl(url, "href", urlNode) == null,
      ),
    ).toBe(true);
    expect(
      safeMarkdownUrl("https://example.com/image.png", "src", urlNode) == null,
    ).toBe(true);
  });

  it("renders supported model Markdown as semantic document content", () => {
    const { container } = render(
      <AssistantMarkdown
        content={`## Итог

Обычный **важный** и *курсивный* текст с ~~удалением~~ и \`inline()\`.

- первый пункт
- [x] выполнено

> Проверяем цитату.

| Модель | Статус |
| --- | --- |
| GPT | готова |

---`}
        isStreaming={false}
      />,
    );

    expect(screen.getByRole("heading", { level: 2, name: "Итог" })).toBeVisible();
    expect(screen.getByText("важный").tagName).toBe("SPAN");
    expect(container.querySelector("em")).toHaveTextContent("курсивный");
    expect(container.querySelector("del")).toHaveTextContent("удалением");
    expect(container.querySelector('[data-streamdown="inline-code"]')).toHaveTextContent("inline()");
    expect(container.querySelector("ul")).toBeInTheDocument();
    expect(container.querySelector('input[type="checkbox"]')).toBeDisabled();
    expect(container.querySelector("blockquote")).toHaveTextContent("Проверяем цитату.");
    expect(screen.getByRole("table")).toHaveTextContent("GPT");
    expect(container.querySelector("hr")).toBeInTheDocument();
  });

  it("renders incomplete Markdown during streaming and converges when completed", () => {
    const { rerender } = render(
      <AssistantMarkdown content="Ответ с **важным" isStreaming />,
    );

    expect(screen.getByText(/Ответ с/)).toBeVisible();

    rerender(
      <AssistantMarkdown content="Ответ с **важным** выводом." isStreaming={false} />,
    );

    expect(screen.getByText("важным")).toBeVisible();
    expect(screen.getByText("важным").closest("span")).toHaveAttribute(
      "data-streamdown",
      "strong",
    );
  });

  it("renders untrusted model output without active HTML, images, or unsafe navigation", () => {
    const { container } = render(
      <AssistantMarkdown
        content={`<script>window.__owned = true</script>
<img src="https://tracker.invalid/pixel.png" onerror="window.__owned = true">
<b data-probe="raw">visible raw label</b>
<iframe src="https://tracker.invalid/frame.html"></iframe>
<style>body { display: none; }</style>

![remote](https://tracker.invalid/markdown.png)
[safe](https://example.com/docs?q=1)
[anchor](#details)
[relative](/admin)
[javascript](javascript:alert(1))
[data](data:text/html,owned)
[file](file:///etc/passwd)
[blob](blob:https://example.com/id)`}
        isStreaming={false}
      />,
    );

    expect(
      container.querySelector("script, img, iframe, object, embed, style"),
    ).toBeNull();
    expect(container.querySelector("b, [data-probe]")).toBeNull();
    expect(
      ["relative", "javascript", "data", "file", "blob"].every((label) =>
        container.textContent?.includes(label),
      ),
    ).toBe(true);

    const renderedLinks = [...container.querySelectorAll("a")];
    const safeLink = renderedLinks.find((link) => link.textContent === "safe");
    expect(safeLink).toBeDefined();
    if (!safeLink) return;
    expect(safeLink).toHaveAttribute("href", "https://example.com/docs?q=1");
    expect(safeLink).toHaveAttribute("target", "_blank");
    expect(safeLink).toHaveAttribute("rel", expect.stringContaining("noopener"));

    const anchorLink = renderedLinks.find((link) => link.textContent === "anchor");
    expect(anchorLink).toBeDefined();
    if (!anchorLink) return;
    expect(anchorLink).toHaveAttribute("href", "#details");
    expect(anchorLink).not.toHaveAttribute("target");
    expect(anchorLink).not.toHaveAttribute("rel");

    const approvedHrefs = new Set([
      "https://example.com/docs?q=1",
      "#details",
    ]);
    const navigableLinks = [...container.querySelectorAll("a[href]")];
    expect(navigableLinks.length).toBe(2);
    expect(
      navigableLinks.some(
        (link) => !approvedHrefs.has(link.getAttribute("href") ?? ""),
      ),
    ).toBe(false);
  });

  it("keeps adversarial streaming labels readable without active content or navigation", () => {
    const streamingContent = `<b data-probe="stream-raw">visible</b>
[relative](/admin)
[malformed](https:/example.com`;
    const staticContent = `${streamingContent})`;
    const { container, rerender } = render(
      <AssistantMarkdown content={streamingContent} isStreaming />,
    );

    const expectInertReadableContent = () => {
      expect(container.querySelector("b, [data-probe], img, script")).toBeNull();
      expect(
        ["visible", "relative", "malformed"].every((label) =>
          container.textContent?.includes(label),
        ),
      ).toBe(true);
      const links = [...container.querySelectorAll("a[href]")];
      expect(
        ["relative", "malformed"].map((label) =>
          links.some((link) => link.textContent === label),
        ),
      ).toEqual([false, false]);
    };

    expectInertReadableContent();

    rerender(<AssistantMarkdown content={staticContent} isStreaming={false} />);

    expectInertReadableContent();
  });

  it("rejects case, whitespace, and character-reference URL evasions", () => {
    const { container } = render(
      <AssistantMarkdown
        content={`[case](JaVaScRiPt:alert(1))
[leading](< javascript:alert(1)>)
[reference](javascript&#58;alert(1))`}
        isStreaming={false}
      />,
    );

    const unsafeLabels = new Set(["case", "leading", "reference"]);
    const navigableUnsafeLinks = [...container.querySelectorAll("a[href]")].filter(
      (link) => unsafeLabels.has(link.textContent ?? ""),
    );
    const evasionsRemainVisible = [...unsafeLabels].every((label) =>
      container.textContent?.includes(label),
    );
    expect(evasionsRemainVisible).toBe(true);
    expect(navigableUnsafeLinks).toHaveLength(0);
  });

  it("copies only inert fenced code and never offers a download", async () => {
    const user = userEvent.setup();
    const writeText = vi
      .spyOn(navigator.clipboard, "writeText")
      .mockResolvedValue(undefined);

    const { container } = render(
      <AssistantMarkdown
        content={"```typescript\nconst answer = 42;\n```"}
        isStreaming={false}
      />,
    );

    expect(container.querySelector('[data-streamdown="code-block"]')).toHaveTextContent(
      "const answer = 42;",
    );
    expect(
      container.querySelector('[data-streamdown="code-block-header"]'),
    ).toHaveTextContent("typescript");
    expect(
      container.querySelector('[data-streamdown="code-block-download-button"]'),
    ).toBeNull();

    const copyButton = container.querySelector<HTMLButtonElement>(
      '[data-streamdown="code-block-copy-button"]',
    );
    expect(copyButton).not.toBeNull();
    if (!copyButton) return;
    await user.click(copyButton);
    expect(writeText).toHaveBeenCalledWith("const answer = 42;\n");
  });

  it("keeps fenced code intact when clipboard access is rejected", async () => {
    const user = userEvent.setup();
    const writeText = vi
      .spyOn(navigator.clipboard, "writeText")
      .mockRejectedValue(new Error("clipboard unavailable"));

    const { container } = render(
      <AssistantMarkdown
        content={"```text\nretained source\n```"}
        isStreaming={false}
      />,
    );
    const codeBlock = container.querySelector('[data-streamdown="code-block"]');

    const copyButton = container.querySelector<HTMLButtonElement>(
      '[data-streamdown="code-block-copy-button"]',
    );
    expect(copyButton).not.toBeNull();
    if (!copyButton) return;
    await user.click(copyButton);

    expect(writeText).toHaveBeenCalledWith("retained source\n");
    expect(codeBlock?.textContent?.includes("retained source") ?? false).toBe(true);
    expect(container.querySelector('[role="alert"]')).toBeNull();
    expect(
      container.textContent?.includes("clipboard unavailable") ?? false,
    ).toBe(false);
  });
});
