import { beforeEach, expect, test } from "vitest";
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { delay, http, HttpResponse } from "msw";
import { Outlet, Route, Routes } from "react-router-dom";

import { server } from "../test/server";
import { renderApp } from "../test/render";
import { objectsPage } from "../test/fixtures";
import { ObjectsTable } from "./objects-table";
import { ObjectDetail } from "./object-detail";
import i18n from "../i18n";

beforeEach(async () => {
  await i18n.changeLanguage("en");
});

function tree() {
  return (
    <Routes>
      <Route path="/objects" element={<ObjectsTable />} />
      <Route path="/objects/:id" element={<ObjectDetail />} />
    </Routes>
  );
}

function nestedTree() {
  return (
    <Routes>
      <Route
        path="/objects"
        element={
          <>
            <ObjectsTable />
            <Outlet />
          </>
        }
      >
        <Route path=":id" element={<div>detail pane</div>} />
      </Route>
    </Routes>
  );
}

/** Capture the query string of every objects request so assertions can inspect URL → request flow. */
function captureRequests() {
  const calls: URLSearchParams[] = [];

  server.use(
    http.get("/api/admin/objects", ({ request }) => {
      calls.push(new URL(request.url).searchParams);

      return HttpResponse.json(objectsPage);
    }),
  );

  return calls;
}

function last(calls: URLSearchParams[]): URLSearchParams {
  return calls[calls.length - 1];
}

test("renders a row per object with number, name, visibility, location and count", async () => {
  renderApp(tree(), { route: "/objects" });

  expect(await screen.findByText("Amphora")).toBeInTheDocument();
  expect(screen.getByText("LM-0042")).toBeInTheDocument();
  // "Public" is also a filter chip and the location is shared across fixtures;
  // scope column assertions to the Amphora row.
  const row = screen.getByText("Amphora").closest("tr")!;
  expect(within(row).getByText("Public")).toBeInTheDocument();
  expect(within(row).getByText("Vault 3")).toBeInTheDocument();
});

test("clicking a sortable header updates sort/order and aria-sort", async () => {
  const calls = captureRequests();
  renderApp(tree(), { route: "/objects" });
  await screen.findByText("Amphora");

  const nameHeader = screen.getByRole("columnheader", { name: /Name/i });
  expect(nameHeader).toHaveAttribute("aria-sort", "none");

  await userEvent.click(within(nameHeader).getByRole("button"));

  await waitFor(() => expect(nameHeader).toHaveAttribute("aria-sort", "ascending"));
  await waitFor(() => {
    const cur = last(calls);
    expect(cur.get("sort")).toBe("object_name");
    expect(cur.get("order")).toBe("asc");
  });

  await userEvent.click(within(nameHeader).getByRole("button"));
  await waitFor(() => expect(nameHeader).toHaveAttribute("aria-sort", "descending"));
});

test("typing in the quick filter sets q (debounced)", async () => {
  const calls = captureRequests();
  renderApp(tree(), { route: "/objects" });
  await screen.findByText("Amphora");

  await userEvent.type(screen.getByLabelText(/filter objects/i), "amph");

  await waitFor(() => expect(last(calls).get("q")).toBe("amph"));
});

test("a visibility chip sets the visibility param", async () => {
  const calls = captureRequests();
  renderApp(tree(), { route: "/objects" });
  await screen.findByText("Amphora");

  await userEvent.click(screen.getByRole("button", { name: /^draft$/i }));

  await waitFor(() => expect(last(calls).get("visibility")).toBe("draft"));
});

test("pagination next/prev change the offset", async () => {
  const calls = captureRequests();
  // total > limit so Next is enabled.
  server.use(
    http.get("/api/admin/objects", ({ request }) => {
      calls.push(new URL(request.url).searchParams);

      return HttpResponse.json({ ...objectsPage, total: 200 });
    }),
  );
  renderApp(tree(), { route: "/objects" });
  await screen.findByText("Amphora");

  await userEvent.click(screen.getByRole("button", { name: /next/i }));
  await waitFor(() => expect(last(calls).get("offset")).toBe("50"));

  // Back to the first page: the URL drops `offset`, so the request sends offset 0.
  await userEvent.click(screen.getByRole("button", { name: /previous/i }));
  await waitFor(() => expect(last(calls).get("offset")).toBe("0"));
});

test("the page-size select sets the limit", async () => {
  const calls = captureRequests();
  renderApp(tree(), { route: "/objects" });
  await screen.findByText("Amphora");

  await userEvent.selectOptions(screen.getByLabelText(/per page/i), "100");

  await waitFor(() => expect(last(calls).get("limit")).toBe("100"));
});

test("clicking a row deep-links to /objects/:id preserving the query string", async () => {
  renderApp(tree(), { route: "/objects?sort=object_name&order=desc" });

  await userEvent.click(await screen.findByText("Amphora"));

  expect(await screen.findByRole("heading", { name: "Amphora" })).toBeInTheDocument();
});

test("the object number cell is a real link", async () => {
  renderApp(tree(), { route: "/objects" });
  expect(await screen.findByRole("link", { name: "LM-0042" })).toBeInTheDocument();
});

test("the selected row's link is marked aria-current=page", async () => {
  const first = objectsPage.items[0];
  renderApp(nestedTree(), { route: `/objects/${first.id}` });
  const link = await screen.findByRole("link", { name: first.object_number });
  expect(link).toHaveAttribute("aria-current", "page");
  const other = await screen.findByRole("link", { name: objectsPage.items[1].object_number });
  expect(other).not.toHaveAttribute("aria-current");
});

test("the table is marked aria-busy while loading", async () => {
  server.use(
    http.get("/api/admin/objects", async () => {
      await delay(50);
      return HttpResponse.json(objectsPage);
    }),
  );
  renderApp(tree(), { route: "/objects" });
  expect(screen.getByRole("table")).toHaveAttribute("aria-busy", "true");
  await screen.findByRole("link", { name: "LM-0042" });
  expect(screen.getByRole("table")).not.toHaveAttribute("aria-busy");
});

test("a failed objects fetch is announced via role=alert", async () => {
  server.use(http.get("/api/admin/objects", () => new HttpResponse(null, { status: 500 })));
  renderApp(tree(), { route: "/objects" });
  expect(await screen.findByRole("alert")).toHaveTextContent(/could not load/i);
});
