import { beforeEach, expect, test } from "vitest";
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { 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>
  );
}

/** 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();
});
