import { fireEvent, screen, waitFor, within } from "@testing-library/react";
import { BASE, MANAGED_FILE, NEVER, renderAt } from "./testFixtures";

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

test("a cold deep link fetches the list once and renders the client (D9)", async () => {
	const { fetchMock } = renderAt("/clients/1", BASE);

	await screen.findByRole("heading", { name: "laptop" });
	expect(screen.getByText("192.168.1.10")).toBeTruthy();
	const listCalls = fetchMock.mock.calls.filter(([input]) => String(input) === "/api/clients");
	expect(listCalls).toHaveLength(1);
});

test("identity carries the name, its provenance and both timestamps", async () => {
	renderAt("/clients/2", BASE);

	// The unnamed row is the learned one, and Clients is where the marker shows.
	const heading = await screen.findByRole("heading", { name: /kids-tablet\.lan/ });
	expect(within(heading).getByText("learned")).toBeTruthy();
	expect(screen.getByText(/This client appeared from DNS traffic/)).toBeTruthy();
	expect(screen.getByText("First seen")).toBeTruthy();
	expect(screen.getByText("Last seen")).toBeTruthy();
});

test("a hand-set row reads as edited under database authority", async () => {
	renderAt("/clients/1", BASE);

	await waitFor(() => expect(screen.getByText(/The name and group were set here/)).toBeTruthy());
});

test("the same row reads as declared under file authority", async () => {
	renderAt("/clients/1", { ...BASE, "GET /api/config/status": MANAGED_FILE });

	await screen.findByText(/Declared in \/etc\/nxdns\/config\.zon/);
});

test("an unresolved authority names the doubt instead of picking a provenance", async () => {
	renderAt("/clients/1", { ...BASE, "GET /api/config/status": NEVER });

	await screen.findByText(/Either declared in the configuration file or edited here/);
});

test("an id the list does not contain renders the missing-client state (D9)", async () => {
	renderAt("/clients/99", BASE);

	await screen.findByRole("heading", { name: "No such client" });
	expect(screen.getByText(/no client with id 99/i)).toBeTruthy();
	expect(screen.getByRole("link", { name: "← All clients" })).toBeTruthy();
});

test("policy links to the group that filters this client", async () => {
	renderAt("/clients/2", BASE);

	const link = await screen.findByRole("link", { name: "Group settings" });
	expect(link.getAttribute("href")).toBe("/configuration/protection?tab=groups&group=2");
});

test("the Activity affordance is a real link, with bounds minted at interaction time", async () => {
	const { router } = renderAt("/clients/1", BASE);
	const link = await screen.findByRole("link", { name: /last 24 hours/i });

	// A real <a href> is what makes middle-click, copy-link and open-in-new-tab
	// work; the bounds must be in that href before the click, not minted by a
	// handler the browser never runs on those gestures.
	expect(link.tagName).toBe("A");
	expect(link.getAttribute("href")).toMatch(/^\/activity\?/);

	const before = Math.floor(Date.now() / 1000);
	fireEvent.pointerDown(link);
	fireEvent.click(link);
	const after = Math.floor(Date.now() / 1000);

	await waitFor(() => expect(router.state.location.pathname).toBe("/activity"));
	const search = router.state.location.search as { mode: string; client: string; since: number; until: number };
	expect(search.mode).toBe("history");
	expect(search.client).toBe("192.168.1.10");
	expect(search.until).toBeGreaterThanOrEqual(before - 1);
	expect(search.until).toBeLessThanOrEqual(after + 1);
	expect(search.until - search.since).toBe(24 * 60 * 60);
});

test("keyboard activation re-mints the bounds, not the focus that preceded it by hours", async () => {
	const { router } = renderAt("/clients/1", BASE);
	const link = await screen.findByRole("link", { name: /last 24 hours/i });

	// Focus can be hours old by the time Enter lands, so the window it minted is
	// stale; the keypress itself is what must be bracketed.
	fireEvent.focus(link);
	const pressedAt = Date.now() + 3 * 60 * 60 * 1000;
	const clock = vi.spyOn(Date, "now").mockReturnValue(pressedAt);
	try {
		fireEvent.keyDown(link, { key: "Enter" });
		// jsdom does not synthesize the click Enter produces on a real anchor.
		fireEvent.click(link);
	} finally {
		clock.mockRestore();
	}

	await waitFor(() => expect(router.state.location.pathname).toBe("/activity"));
	const search = router.state.location.search as { client: string; since: number; until: number };
	expect(search.client).toBe("192.168.1.10");
	expect(search.until).toBe(Math.floor(pressedAt / 1000));
	expect(search.until - search.since).toBe(24 * 60 * 60);
});
