import { fireEvent, render, screen } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { AuthProvider } from "@/auth/store";
import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";
import type { LookupResult } from "@/lib/types";

const BLOCKED: LookupResult = {
	domain: "ads.example",
	group_id: 1,
	local_records: false,
	forward_zone: null,
	blocked: true,
	reason: "blocklist_domain",
	matched: "ads.example",
	source_url: "https://lists.test/a",
	safe_search_rewrite: null,
};

let fetchMock: ReturnType<typeof createFetchMock>;
/** What `/api/lookup` answers, so a test can make it fail without rebuilding the mock. */
let lookup: (url: string) => Response;

function json(payload: unknown, status = 200, headers: Record<string, string> = {}): Response {
	return new Response(JSON.stringify(payload), {
		status,
		headers: { "content-type": "application/json", ...headers },
	});
}

function createFetchMock() {
	return vi.fn(async (input: RequestInfo | URL) => {
		const url = String(input);
		if (url === "/api/groups") {
			return json({
				groups: [
					{ id: 1, name: "default", safe_search: false },
					{ id: 2, name: "kids", safe_search: true },
				],
			});
		}
		if (url.startsWith("/api/lookup")) return lookup(url);
		return json({ error: "not stubbed" }, 404);
	});
}

beforeEach(() => {
	lookup = (url) =>
		url === "/api/lookup?domain=ads.example&group_id=1" ? json(BLOCKED) : json({ error: "not stubbed" }, 404);
	fetchMock = createFetchMock();
	vi.stubGlobal("fetch", fetchMock);
});

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

/**
 * `retry: false` for the failure tests: the shared client retries a 5xx twice
 * and a 429 after its Retry-After, so the surfaced error is what the page does
 * once the client has given up, not something a test should sit out in real
 * time.
 */
function renderPage(path = "/activity/test", { retry = true } = {}) {
	const queryClient = createQueryClient();
	if (!retry) {
		const defaults = queryClient.getDefaultOptions();
		queryClient.setDefaultOptions({ ...defaults, queries: { ...defaults.queries, retry: false } });
	}
	const router = createAppRouter(createMemoryHistory({ initialEntries: [path] }), queryClient);
	render(
		<AuthProvider>
			<QueryClientProvider client={queryClient}>
				<RouterProvider router={router} />
			</QueryClientProvider>
		</AuthProvider>,
	);
}

function lookupCalls(): string[] {
	return fetchMock.mock.calls.map(([input]) => String(input)).filter((url) => url.startsWith("/api/lookup"));
}

test("fetches nothing until submit, then renders the blocked verdict", async () => {
	renderPage();

	await screen.findByRole("heading", { name: "Current policy simulation" });
	await screen.findByLabelText("Group");
	expect(lookupCalls()).toEqual([]);

	fireEvent.change(screen.getByLabelText("Domain"), { target: { value: "ads.example" } });
	expect(lookupCalls()).toEqual([]);

	fireEvent.click(screen.getByRole("button", { name: "Simulate" }));

	await screen.findByRole("heading", { name: "Blocked" });
	expect(lookupCalls()).toEqual(["/api/lookup?domain=ads.example&group_id=1"]);

	expect(screen.getByText("blocklist_domain")).toBeTruthy();
	const link = screen.getByRole("link", { name: "https://lists.test/a" }) as HTMLAnchorElement;
	expect(link.href).toBe("https://lists.test/a");
	expect(screen.getByText("Queries for this name get a blocked response.")).toBeTruthy();
});

test("a ?domain= link asks the question on arrival instead of leaving a filled-in form", async () => {
	renderPage("/activity/test?domain=ads.example");

	// No submit here: the link is the question, so the verdict is what arrives.
	await screen.findByRole("heading", { name: "Blocked" });
	expect(lookupCalls()).toEqual(["/api/lookup?domain=ads.example&group_id=1"]);
	expect(screen.getByLabelText("Domain")).toHaveProperty("value", "ads.example");
});

test("no filter snapshot reads as a server that is starting, not as a verdict", async () => {
	lookup = () => json({ error: "no snapshot" }, 503);
	renderPage("/activity/test", { retry: false });

	fireEvent.change(await screen.findByLabelText("Domain"), { target: { value: "ads.example" } });
	fireEvent.click(screen.getByRole("button", { name: "Simulate" }));

	const alert = await screen.findByRole("alert");
	expect(alert.textContent).toContain("No filter snapshot is loaded yet");
	expect(lookupCalls()).toEqual(["/api/lookup?domain=ads.example&group_id=1"]);
	// Nothing may read as an answer while the lookup has none.
	expect(screen.queryByRole("heading", { name: "Blocked" })).toBeNull();
	expect(screen.queryByText("Simulating…")).toBeNull();
});

test("a rate limit says how long to wait, from the server's own Retry-After", async () => {
	lookup = () => json({ error: "rate limited" }, 429, { "retry-after": "12" });
	renderPage("/activity/test", { retry: false });

	fireEvent.change(await screen.findByLabelText("Domain"), { target: { value: "ads.example" } });
	fireEvent.click(screen.getByRole("button", { name: "Simulate" }));

	const alert = await screen.findByRole("alert");
	expect(alert.textContent).toBe("Rate limited. Try again in 12s.");
});

test("resubmitting the same domain and group refetches rather than showing a stale verdict", async () => {
	renderPage();

	fireEvent.change(await screen.findByLabelText("Domain"), { target: { value: "ads.example" } });
	fireEvent.click(screen.getByRole("button", { name: "Simulate" }));
	await screen.findByRole("heading", { name: "Blocked" });
	expect(lookupCalls()).toHaveLength(1);

	// The policy can change between two identical questions, so the second one
	// has to reach the server even though the query key has not moved.
	lookup = () => json({ ...BLOCKED, blocked: false, reason: "no_match", matched: "", source_url: null });
	fireEvent.click(screen.getByRole("button", { name: "Simulate" }));

	await screen.findByRole("heading", { name: "Allowed" });
	expect(lookupCalls()).toEqual([
		"/api/lookup?domain=ads.example&group_id=1",
		"/api/lookup?domain=ads.example&group_id=1",
	]);
});

test("defaults the group select to the default group (id 1)", async () => {
	renderPage();
	// A RAC Select names its trigger with the current value and then the label, so
	// the selected group's name is the only thing the trigger shows.
	const trigger = await screen.findByRole("button", { name: /Group$/ });
	expect(trigger.textContent).toContain("default");
});

test("the framing is forward-tense, so it cannot be read as an account of a past query", async () => {
	renderPage();
	await screen.findByRole("heading", { name: "Current policy simulation" });

	const intro = screen.getByRole("heading", { name: "Current policy simulation" }).nextElementSibling;
	expect(intro?.textContent).toContain("would");
	expect(intro?.textContent).toContain("right now");
	// Nothing on the page may claim to explain a query that already happened.
	expect(document.body.textContent).not.toContain("Look up");
});
