import {
  fireEvent,
  render,
  screen,
  waitFor,
  within,
} from "@testing-library/react";
import {
  TOOLBOX_PREFERENCES_KEY,
  type ToolboxAppManifest,
} from "@add-ideas/toolbox-contract";
import { beforeEach, describe, expect, it, vi } from "vitest";

import {
  AppShell,
  ToolboxHeader,
  ToolboxPersonalizePanel,
} from "../src/index.js";

const currentApp: ToolboxAppManifest = {
  schemaVersion: 1,
  id: "de.add-ideas.pdf-tools",
  name: "PDF Workbench",
  version: "1.2.3",
  description: "Local PDF tools",
  entry: "./index.html",
  icon: "./icon.svg",
  categories: ["documents", "pdf"],
  tags: ["merge"],
  integration: {
    contextVersion: 1,
    launchModes: ["navigate", "new-tab"],
    embedding: "unsupported",
  },
  requirements: {
    secureContext: true,
    workers: true,
    indexedDb: true,
    crossOriginIsolated: false,
  },
  privacy: {
    processing: "local",
    fileUploads: false,
    telemetry: false,
  },
  source: {
    repository: "https://git.example.test/pdf-tools",
    license: "AGPL-3.0-only",
  },
};

const otherApp: ToolboxAppManifest = {
  ...currentApp,
  id: "de.add-ideas.xslt-tools",
  name: "XSLT Workbench",
  entry: "./index.html",
  source: {
    repository: "https://git.example.test/xslt-tools",
    license: "AGPL-3.0-only",
  },
};

const catalog = {
  schemaVersion: 1,
  id: "de.add-ideas.toolbox",
  name: "Toolbox",
  home: "./",
  theme: { mode: "system", brand: "add·ideas" },
  apps: [
    { manifest: "./pdf/toolbox-app.json", enabled: true },
    { manifest: "./xslt/toolbox-app.json", enabled: true },
  ],
};

const json = (value: unknown, status = 200): Response =>
  new Response(JSON.stringify(value), {
    status,
    headers: { "content-type": "application/json" },
  });

function catalogFetch() {
  return vi.fn(async (input: string | URL | Request) => {
    const url = new URL(input instanceof Request ? input.url : input);
    if (url.pathname === "/toolbox.catalog.json") return json(catalog);
    if (url.pathname === "/pdf/toolbox-app.json") return json(currentApp);
    if (url.pathname === "/xslt/toolbox-app.json") return json(otherApp);
    return json({}, 404);
  });
}

describe("AppShell", () => {
  beforeEach(() => localStorage.clear());

  it("renders identity, privacy, version, actions, and content standalone", async () => {
    const onHelp = vi.fn();
    const fetch = vi.fn();
    render(
      <AppShell
        app={currentApp}
        helpAction={{ onClick: onHelp }}
        appActions={<a href="/help">Help</a>}
        contextOptions={{
          location: "https://tools.example.test/pdf/",
          fetch,
        }}
      >
        <p>Application content</p>
      </AppShell>,
    );

    expect(
      screen.getByRole("heading", { level: 1, name: "PDF Workbench" }),
    ).toBeInTheDocument();
    expect(screen.getByLabelText("Version 1.2.3")).toBeInTheDocument();
    expect(screen.getByText(/Local processing/u)).toBeInTheDocument();
    expect(screen.getByRole("link", { name: "Help" })).toHaveAttribute(
      "href",
      "/help",
    );
    expect(screen.getByText("Application content")).toBeInTheDocument();
    await waitFor(() => expect(fetch).not.toHaveBeenCalled());
    expect(
      document.querySelector("[data-toolbox-context='standalone']"),
    ).toBeInTheDocument();
    const controls = screen.getByLabelText("Toolbox controls");
    expect(
      Array.from(controls.children, (element) =>
        element.getAttribute("aria-label"),
      ),
    ).toEqual([
      "Help",
      "Source for PDF Workbench on Gitea",
      "Apps",
      "Personalize",
    ]);
    expect(
      Array.from(
        document.querySelector(".toolbox-shell__bar")?.children ?? [],
        (element) => element.className,
      ),
    ).toEqual([
      "toolbox-shell__brand",
      "toolbox-shell__identity",
      "toolbox-shell__controls",
    ]);
    fireEvent.click(screen.getByRole("button", { name: "Apps" }));
    expect(
      screen.getByRole("navigation", { name: "Toolbox applications" }),
    ).toHaveTextContent("Open this app from Toolbox");
    expect(
      screen.getByRole("link", { name: /Source for PDF Workbench/u }),
    ).toHaveAttribute("href", "https://git.example.test/pdf-tools");
    fireEvent.click(screen.getByRole("button", { name: "Help" }));
    expect(onHelp).toHaveBeenCalledOnce();
    expect(
      screen.queryByRole("navigation", { name: "Toolbox applications" }),
    ).not.toBeInTheDocument();
    expect(
      screen.getByRole("button", { name: "Personalize" }),
    ).toHaveTextContent("Personalize");
    expect(screen.getByRole("button", { name: "Apps" })).toHaveTextContent(
      "Apps",
    );
    expect(document.querySelector(".toolbox-shell__icon")).toBeNull();
    expect(screen.getByText("add·ideas Toolbox").closest("a")).toBeNull();
    expect(document.querySelector(".toolbox-shell__footer")).toContainElement(
      screen.getByLabelText("Version 1.2.3"),
    );
  });

  it("loads same-origin context and creates full-page contextual switch links", async () => {
    const fetch = catalogFetch();
    render(
      <AppShell
        app={currentApp}
        manifestUrl="https://tools.example.test/pdf/toolbox-app.json"
        contextOptions={{
          location:
            "https://tools.example.test/pdf/?toolbox=/toolbox.catalog.json",
          fetch,
        }}
      >
        PDF
      </AppShell>,
    );

    await waitFor(() =>
      expect(
        document.querySelector("[data-toolbox-context='connected']"),
      ).toBeInTheDocument(),
    );
    fireEvent.click(screen.getByRole("button", { name: "Apps" }));
    const navigation = screen.getByRole("navigation", {
      name: "Toolbox applications",
    });
    expect(navigation).toBeInTheDocument();
    const current = screen.getByRole("link", { name: "PDF Workbench" });
    expect(current).toHaveAttribute("aria-current", "page");
    const other = screen.getByRole("link", { name: "XSLT Workbench" });
    const destination = new URL(other.getAttribute("href") as string);
    expect(destination.pathname).toBe("/xslt/index.html");
    expect(destination.searchParams.get("toolbox")).toBe(
      "https://tools.example.test/toolbox.catalog.json",
    );
    expect(
      screen.getByRole("link", { name: "add·ideas Toolbox" }),
    ).toHaveAttribute("href", "https://tools.example.test/");
    expect(
      screen
        .getByRole("link", { name: "add·ideas Toolbox" })
        .querySelector("img"),
    ).toHaveAttribute("src", "https://tools.example.test/favicon.svg");
    expect(
      document.querySelector("[data-toolbox-context='connected']"),
    ).toBeInTheDocument();
  });

  it("rejects cross-origin context and remains standalone", async () => {
    const fetch = vi.fn();
    const onContextError = vi.fn();
    render(
      <AppShell
        app={currentApp}
        contextOptions={{
          location:
            "https://tools.example.test/pdf/?toolbox=https://evil.example/catalog.json",
          fetch,
        }}
        onContextError={onContextError}
      >
        PDF
      </AppShell>,
    );

    await waitFor(() => expect(onContextError).toHaveBeenCalledOnce());
    expect(onContextError.mock.calls[0]?.[0]).toMatchObject({
      code: "cross-origin",
    });
    expect(fetch).not.toHaveBeenCalled();
    fireEvent.click(screen.getByRole("button", { name: "Apps" }));
    expect(
      screen.getByRole("navigation", { name: "Toolbox applications" }),
    ).toHaveTextContent("Open this app from Toolbox");
  });

  it.each([
    ["invalid catalog", async () => json({ schemaVersion: 2 })],
    ["fetch failure", async () => json({}, 503)],
  ])("survives %s in standalone mode", async (_label, fetchImplementation) => {
    const onContextError = vi.fn();
    render(
      <AppShell
        app={currentApp}
        contextOptions={{
          location:
            "https://tools.example.test/pdf/?toolbox=/toolbox.catalog.json",
          fetch: fetchImplementation,
        }}
        onContextError={onContextError}
      >
        Still usable
      </AppShell>,
    );

    await waitFor(() => expect(onContextError).toHaveBeenCalledOnce());
    expect(screen.getByText("Still usable")).toBeInTheDocument();
    expect(
      document.querySelector("[data-toolbox-context='standalone']"),
    ).toBeInTheDocument();
  });

  it("uses and updates the shared light, dark, and system preference", async () => {
    localStorage.setItem(
      TOOLBOX_PREFERENCES_KEY,
      JSON.stringify({
        version: 1,
        pinned: [],
        order: [],
        hidden: [],
        theme: "dark",
      }),
    );
    render(
      <AppShell
        app={currentApp}
        contextOptions={{ location: "https://tools.example.test/pdf/" }}
      >
        PDF
      </AppShell>,
    );

    const shell = document.querySelector(".toolbox-shell");
    expect(shell).toHaveAttribute("data-toolbox-theme", "dark");
    fireEvent.click(screen.getByRole("button", { name: "Personalize" }));
    expect(
      screen.getByRole("heading", { name: "Personalize your toolbox" }),
    ).toBeInTheDocument();
    expect(screen.getByText("On this device")).toBeInTheDocument();
    expect(
      screen.getByRole("group", { name: "Appearance" }),
    ).toBeInTheDocument();
    expect(screen.getByRole("button", { name: "Dark" })).toHaveAttribute(
      "aria-pressed",
      "true",
    );
    expect(screen.queryByText("Pinned")).not.toBeInTheDocument();
    expect(
      screen.queryByRole("button", { name: "Export JSON" }),
    ).not.toBeInTheDocument();
    fireEvent.click(screen.getByRole("button", { name: "Light" }));
    expect(shell).toHaveAttribute("data-toolbox-theme", "light");
    expect(
      JSON.parse(localStorage.getItem(TOOLBOX_PREFERENCES_KEY) ?? "{}"),
    ).toMatchObject({ theme: "light" });
  });

  it("shares the portal-style personalization frame and extension slot", () => {
    const onThemeChange = vi.fn();
    render(
      <ToolboxPersonalizePanel
        theme="system"
        onThemeChange={onThemeChange}
        storageAvailable={false}
      >
        <p>Portal preference actions</p>
      </ToolboxPersonalizePanel>,
    );

    expect(
      screen.getByRole("heading", { name: "Personalize your toolbox" }),
    ).toBeInTheDocument();
    expect(screen.getByRole("status")).toHaveTextContent(
      "Browser storage is unavailable",
    );
    expect(screen.getByText("Portal preference actions")).toBeInTheDocument();
    fireEvent.click(screen.getByRole("button", { name: "Dark" }));
    expect(onThemeChange).toHaveBeenCalledWith("dark");
  });

  it("resolves manifest-relative header links without rendering an app icon", async () => {
    render(
      <AppShell
        app={{
          ...currentApp,
          actions: [
            {
              id: "source",
              label: "Source",
              url: "./source/",
            },
          ],
        }}
        manifestUrl="./toolbox-app.json"
        contextOptions={{
          location: "https://tools.example.test/nested/pdf/index.html",
        }}
      >
        PDF
      </AppShell>,
    );

    expect(
      screen.getByRole("link", { name: "Source for PDF Workbench on Gitea" }),
    ).toHaveAttribute("href", "https://tools.example.test/nested/pdf/source/");
    expect(document.querySelector(".toolbox-shell__icon")).toBeNull();
  });

  it("uses one accessible controlled popover for Personalize and Apps", () => {
    const onHelp = vi.fn();
    render(
      <ToolboxHeader
        title="PDF Workbench"
        subtitle="Local PDF tools"
        theme="system"
        personalizeContent={<a href="#preferences">Full preferences</a>}
        helpAction={{ onClick: onHelp }}
        sourceHref="https://git.example.test/pdf-tools"
        apps={[
          {
            id: "xslt",
            name: "XSLT Workbench",
            href: "#xslt",
          },
        ]}
      />,
    );

    const personalize = screen.getByRole("button", { name: "Personalize" });
    const apps = screen.getByRole("button", { name: "Apps" });
    expect(
      screen.getByRole("group", { name: "Toolbox controls" }),
    ).toBeInTheDocument();
    expect(personalize).toHaveAttribute("aria-label", "Personalize");
    expect(apps).toHaveAttribute("aria-label", "Apps");
    expect(apps).not.toHaveAttribute("aria-haspopup");

    fireEvent.click(personalize);
    expect(
      screen.getByRole("dialog", { name: "Personalize Toolbox" }),
    ).toHaveTextContent("Full preferences");
    expect(personalize).toHaveAttribute("aria-expanded", "true");
    expect(
      screen.getByRole("link", { name: "Full preferences" }),
    ).toHaveFocus();

    fireEvent.click(apps);
    expect(
      screen.queryByRole("dialog", { name: "Personalize Toolbox" }),
    ).not.toBeInTheDocument();
    const navigation = screen.getByRole("navigation", {
      name: "Toolbox applications",
    });
    expect(navigation).toBeInTheDocument();
    expect(
      within(navigation).getByRole("link", { name: "XSLT Workbench" }),
    ).toHaveFocus();

    fireEvent.keyDown(document, { key: "Escape" });
    expect(
      screen.queryByRole("navigation", { name: "Toolbox applications" }),
    ).not.toBeInTheDocument();
    expect(apps).toHaveFocus();

    fireEvent.click(personalize);
    fireEvent.pointerDown(document.body);
    expect(
      screen.queryByRole("dialog", { name: "Personalize Toolbox" }),
    ).not.toBeInTheDocument();

    fireEvent.click(apps);
    fireEvent.click(
      within(
        screen.getByRole("navigation", { name: "Toolbox applications" }),
      ).getByRole("link", { name: "XSLT Workbench" }),
    );
    expect(
      screen.queryByRole("navigation", { name: "Toolbox applications" }),
    ).not.toBeInTheDocument();
  });

  it("keeps the legacy onPersonalize callback when no popover content exists", () => {
    const onPersonalize = vi.fn();
    const onThemeChange = vi.fn();
    render(
      <ToolboxHeader
        title="PDF Workbench"
        theme="system"
        onPersonalize={onPersonalize}
        onThemeChange={onThemeChange}
      />,
    );

    fireEvent.click(screen.getByRole("button", { name: "Personalize" }));
    expect(onPersonalize).toHaveBeenCalledOnce();
    expect(
      screen.queryByRole("dialog", { name: "Personalize Toolbox" }),
    ).not.toBeInTheDocument();
  });

  it("closes Personalize if its content becomes unavailable", () => {
    const { rerender } = render(
      <ToolboxHeader
        title="PDF Workbench"
        theme="system"
        onThemeChange={vi.fn()}
      />,
    );

    fireEvent.click(screen.getByRole("button", { name: "Personalize" }));
    expect(
      screen.getByRole("dialog", { name: "Personalize Toolbox" }),
    ).toBeInTheDocument();

    rerender(<ToolboxHeader title="PDF Workbench" theme="system" />);
    expect(
      screen.queryByRole("dialog", { name: "Personalize Toolbox" }),
    ).not.toBeInTheDocument();
    expect(screen.getByRole("button", { name: "Personalize" })).toBeDisabled();
  });
});
