import { afterEach, expect, test, vi } from "vitest";
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Routes, Route } from "react-router-dom";
import { renderApp } from "../test/render";
import { ObjectsPage } from "./objects-page";
import { ObjectDetail } from "./object-detail";

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

// The shared setup stub returns `matches: false` (narrow). Override per-test to
// flip the `(min-width: 1024px)` query so we can exercise both layouts.
function setViewport(wide: boolean) {
  Object.defineProperty(window, "matchMedia", {
    value: (query: string): MediaQueryList =>
      ({
        matches: wide && query === "(min-width: 1024px)",
        media: query,
        onchange: null,
        addEventListener: () => {},
        removeEventListener: () => {},
        addListener: () => {},
        removeListener: () => {},
        dispatchEvent: () => false,
      }) as MediaQueryList,
    writable: true,
  });
}

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

test("renders the page heading and sets the document title", async () => {
  setViewport(true);
  renderApp(tree(), { route: "/objects" });

  expect(
    await screen.findByRole("heading", { level: 1, name: /objects/i }),
  ).toBeInTheDocument();
  await waitFor(() => expect(document.title).toMatch(/objects \| /i));
});

test("the table is the landing view; no detail panel until a row is opened", async () => {
  setViewport(true);
  renderApp(tree(), { route: "/objects" });

  expect(await screen.findByText("Amphora")).toBeInTheDocument();
  expect(screen.queryByRole("button", { name: /close detail/i })).not.toBeInTheDocument();
});

test("wide: clicking a row opens detail in the right pane with a close control", async () => {
  setViewport(true);
  renderApp(tree(), { route: "/objects" });
  await userEvent.click(await screen.findByText("Amphora"));

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

  const close = screen.getByRole("button", { name: /close detail/i });
  await userEvent.click(close);

  // Back to the table-only view: the detail heading is gone, the table remains.
  expect(screen.queryByRole("heading", { name: "Amphora" })).not.toBeInTheDocument();
  expect(screen.getByText("Amphora")).toBeInTheDocument();
});

test("narrow: detail renders inside a portaled drawer", async () => {
  setViewport(false);
  renderApp(tree(), { route: "/objects" });
  await userEvent.click(await screen.findByText("Amphora"));

  // The drawer popup is portaled to document.body. The narrow path lazy-loads the
  // drawer chunk (Suspense) + mounts the Base UI Drawer + fetches the object, so the
  // first query needs a wider window than the 1000ms findBy default (avoids a flake).
  const body = within(document.body);
  expect(
    await body.findByRole("heading", { name: "Amphora" }, { timeout: 5000 }),
  ).toBeInTheDocument();
  expect(body.getByRole("button", { name: /close detail/i })).toBeInTheDocument();
});

test("wide: deep-linking /objects/:id renders the table and the open detail", async () => {
  setViewport(true);
  renderApp(tree(), { route: "/objects/11111111-1111-1111-1111-111111111111" });

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