import { expect, test } from "vitest";
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createMemoryRouter, Route, RouterProvider, Routes } from "react-router-dom";

import { server } from "../test/server";
import { renderApp } from "../test/render";
import { amphora } from "../test/fixtures";
import { SearchPage } from "./search-page";
import { SelectSearchPrompt } from "./select-search-prompt";
import { ObjectDetail } from "../objects/object-detail";
import "../i18n";

function tree() {
  return (
    <Routes>
      <Route path="/search" element={<SearchPage />}>
        <Route index element={<SelectSearchPrompt />} />
        <Route path=":id" element={<ObjectDetail />} />
      </Route>
    </Routes>
  );
}

// The search rows are <NavLink>s. Under the shared `renderApp` harness the test
// subtree lives in a descendant <Routes> under a catch-all `*` data route, where
// the data router does not intercept the link click (it falls through to a real
// browser navigation that jsdom rejects). Mounting the search routes as real
// data-router routes lets RouterProvider intercept the NavLink, which is the
// data-router equivalent of the old MemoryRouter behavior.
function renderSearchRouter(route = "/search") {
  const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  const router = createMemoryRouter(
    [
      {
        path: "/search",
        element: <SearchPage />,
        children: [
          { index: true, element: <SelectSearchPrompt /> },
          { path: ":id", element: <ObjectDetail /> },
        ],
      },
    ],
    { initialEntries: [route] },
  );

  return render(
    <QueryClientProvider client={qc}>
      <RouterProvider router={router} />
    </QueryClientProvider>,
  );
}

test("typing searches and renders highlighted rich rows", async () => {
  renderApp(tree(), { route: "/search" });
  await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");

  expect(await screen.findByText("Bronze figurine")).toBeInTheDocument();
  const mark = await screen.findByText("bronze");
  expect(mark.tagName).toBe("MARK");
  // The estimated count lives in a status region so updates are announced.
  expect(screen.getByRole("status")).toHaveTextContent(/~\s*25 results/i);
  expect(screen.getByText(/1962-04-03/)).toBeInTheDocument();
});

test("Load more appends the next page", async () => {
  renderApp(tree(), { route: "/search" });
  await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
  await screen.findByText("Bronze figurine");

  expect(screen.queryByText("Object 21")).toBeNull();
  await userEvent.click(screen.getByRole("button", { name: /load more/i }));
  expect(await screen.findByText("Object 21")).toBeInTheDocument();
});

test("the visibility filter adds the param to the request", async () => {
  let lastVisibility: string | null = "unset";
  server.use(
    http.get("/api/admin/search", ({ request }) => {
      lastVisibility = new URL(request.url).searchParams.get("visibility");
      return HttpResponse.json({ hits: [], estimated_total: 0 });
    }),
  );
  renderApp(tree(), { route: "/search" });
  await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
  await userEvent.click(screen.getByRole("button", { name: /^draft$/i }));

  await waitFor(() => expect(lastVisibility).toBe("draft"));
});

test("empty query shows the prompt; zero results shows empty", async () => {
  renderApp(tree(), { route: "/search" });
  expect(screen.getByText(/type to search/i)).toBeInTheDocument();

  server.use(
    http.get("/api/admin/search", () => HttpResponse.json({ hits: [], estimated_total: 0 })),
  );
  await userEvent.type(screen.getByLabelText(/search the collection/i), "zzz");
  expect(await screen.findByText(/no results/i)).toBeInTheDocument();
});

test("clicking a result shows the object in the detail pane", async () => {
  renderSearchRouter();
  await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
  await userEvent.click(await screen.findByText("Bronze figurine"));

  expect(await screen.findByText(amphora.object_name)).toBeInTheDocument();
});

test("a 503 shows the search-unavailable message", async () => {
  server.use(http.get("/api/admin/search", () => new HttpResponse(null, { status: 503 })));
  renderApp(tree(), { route: "/search" });
  await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
  expect(await screen.findByText(/not available on this server/i)).toBeInTheDocument();
});

test("a 500 shows the generic search error", async () => {
  server.use(http.get("/api/admin/search", () => new HttpResponse(null, { status: 500 })));
  renderApp(tree(), { route: "/search" });
  await userEvent.type(screen.getByLabelText(/search the collection/i), "bronze");
  expect(await screen.findByText(/^search is unavailable$/i)).toBeInTheDocument();
});

test("hydrates query and visibility from the initial URL", async () => {
  renderApp(tree(), { route: "/search?q=bronze" });

  expect(screen.getByLabelText(/search the collection/i)).toHaveValue("bronze");
  expect(await screen.findByText("Bronze figurine")).toBeInTheDocument();

  const { container } = renderApp(tree(), { route: "/search?q=bronze&visibility=internal" });

  expect(
    within(container).getByRole("button", { name: /^internal$/i }),
  ).toHaveAttribute("aria-pressed", "true");
});
