import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { createQueryClient } from "@/lib/queryClient";
import type { QueriesPage, QueryRow } from "@/lib/types";
import QueryLogPage from "./QueryLogPage";

function row(id: number, domain: string, overrides: Partial<QueryRow> = {}): QueryRow {
	return {
		id,
		ts: 1_700_000_000 + id,
		domain,
		client_ip: "192.0.2.10",
		qtype: 1,
		blocked: false,
		block_reason: "",
		response_time_us: 1234,
		cache_hit: false,
		upstream: "udp://9.9.9.9:53",
		...overrides,
	};
}

const PAGES: Record<string, QueriesPage> = {
	"/api/queries": {
		queries: [
			row(20, "first.example", { qtype: 65, cache_hit: true, upstream: "" }),
			row(19, "ads.example", {
				blocked: true,
				block_reason: "blocklist:stevenblack",
				response_time_us: null,
				cache_hit: null,
			}),
		],
		next_before: 19,
	},
	"/api/queries?before=19": {
		queries: [row(5, "older.example")],
		next_before: null,
	},
	"/api/queries?domain=ads": {
		queries: [row(19, "ads.example", { blocked: true, block_reason: "blocklist:stevenblack" })],
		next_before: null,
	},
};

beforeEach(() => {
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL) => {
			const url = String(input);
			const payload = PAGES[url];
			if (payload === undefined) return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 });
			return new Response(JSON.stringify(payload), {
				status: 200,
				headers: { "content-type": "application/json" },
			});
		}),
	);
});

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

function renderPage() {
	const client = createQueryClient();
	render(
		<QueryClientProvider client={client}>
			<QueryLogPage />
		</QueryClientProvider>,
	);
	return client;
}

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

test("renders the first page with type names, blocked badge, and formatted cells", async () => {
	renderPage();
	await screen.findByText("first.example");

	expect(screen.getByText("HTTPS")).toBeTruthy();
	expect(screen.getByText("A")).toBeTruthy();
	expect(screen.getByText("Blocked")).toBeTruthy();
	expect(screen.getByText("blocklist:stevenblack")).toBeTruthy();
	expect(screen.getByText("1.2 ms")).toBeTruthy();
	expect(screen.getByText("hit")).toBeTruthy();
	expect(screen.getByText("udp://9.9.9.9:53")).toBeTruthy();
	expect(screen.getByText(/Showing 2 queries/)).toBeTruthy();
});

test("load more appends the next page and stops at the end of the log", async () => {
	renderPage();
	await screen.findByText("first.example");

	fireEvent.click(screen.getByRole("button", { name: "Load more" }));
	await screen.findByText("older.example");

	expect(screen.getByText("first.example")).toBeTruthy();
	expect(screen.getByText(/Showing 3 queries — end of log/)).toBeTruthy();
	expect(screen.queryByRole("button", { name: "Load more" })).toBeNull();
});

test("applying a filter refetches and resets the accumulated list", async () => {
	renderPage();
	await screen.findByText("first.example");

	fireEvent.click(screen.getByRole("button", { name: "Load more" }));
	await screen.findByText("older.example");

	fireEvent.change(screen.getByLabelText("Domain contains"), { target: { value: "ads" } });
	fireEvent.click(screen.getByRole("button", { name: "Apply filters" }));

	await screen.findByText(/Showing 1 query /);
	expect(screen.getByText("ads.example")).toBeTruthy();
	expect(screen.queryByText("first.example")).toBeNull();
	expect(screen.queryByText("older.example")).toBeNull();
});

test("a load-more that resolves after a filter change is discarded", async () => {
	let releaseLoadMore: () => void = () => {};
	vi.stubGlobal(
		"fetch",
		vi.fn((input: RequestInfo | URL) => {
			const url = String(input);
			if (url === "/api/queries?before=19") {
				return new Promise<Response>((resolve) => {
					releaseLoadMore = () => {
						resolve(
							new Response(JSON.stringify(PAGES["/api/queries?before=19"]), {
								status: 200,
								headers: { "content-type": "application/json" },
							}),
						);
					};
				});
			}
			const payload = PAGES[url];
			if (payload === undefined)
				return Promise.resolve(new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 }));
			return Promise.resolve(
				new Response(JSON.stringify(payload), {
					status: 200,
					headers: { "content-type": "application/json" },
				}),
			);
		}),
	);

	renderPage();
	await screen.findByText("first.example");

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

	fireEvent.change(screen.getByLabelText("Domain contains"), { target: { value: "ads" } });
	fireEvent.click(screen.getByRole("button", { name: "Apply filters" }));
	await screen.findByText(/Showing 1 query /);

	releaseLoadMore();
	await act(async () => {
		await new Promise((resolve) => setTimeout(resolve, 0));
	});

	expect(screen.queryByText("older.example")).toBeNull();
	expect(screen.getByText(/Showing 1 query /)).toBeTruthy();
	expect(screen.queryByRole("alert")).toBeNull();
});

test("load more is disabled while a filter change shows placeholder data, then uses the fresh cursor", async () => {
	let releaseFiltered: () => void = () => {};
	const filteredPage: QueriesPage = {
		queries: [row(19, "ads.example", { blocked: true, block_reason: "blocklist:stevenblack" })],
		next_before: 7,
	};
	const filteredOlderPage: QueriesPage = {
		queries: [row(3, "ads.older.example")],
		next_before: null,
	};
	const fetchMock = vi.fn((input: RequestInfo | URL) => {
		const url = String(input);
		if (url === "/api/queries?domain=ads") {
			return new Promise<Response>((resolve) => {
				releaseFiltered = () => {
					resolve(
						new Response(JSON.stringify(filteredPage), {
							status: 200,
							headers: { "content-type": "application/json" },
						}),
					);
				};
			});
		}
		const payload = url === "/api/queries?domain=ads&before=7" ? filteredOlderPage : PAGES[url];
		if (payload === undefined)
			return Promise.resolve(new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 }));
		return Promise.resolve(
			new Response(JSON.stringify(payload), {
				status: 200,
				headers: { "content-type": "application/json" },
			}),
		);
	});
	vi.stubGlobal("fetch", fetchMock);

	renderPage();
	await screen.findByText("first.example");

	fireEvent.change(screen.getByLabelText("Domain contains"), { target: { value: "ads" } });
	fireEvent.click(screen.getByRole("button", { name: "Apply filters" }));

	const staleButton = screen.getByRole("button", { name: "Load more" });
	expect(staleButton).toHaveProperty("disabled", true);
	fireEvent.click(staleButton);
	expect(fetchMock.mock.calls.map((call) => String(call[0]))).not.toContain("/api/queries?domain=ads&before=19");

	releaseFiltered();
	await waitFor(() => {
		expect(screen.queryByText("first.example")).toBeNull();
	});

	const freshButton = screen.getByRole("button", { name: "Load more" });
	expect(freshButton).toHaveProperty("disabled", false);
	fireEvent.click(freshButton);
	await screen.findByText("ads.older.example");

	expect(fetchMock.mock.calls.map((call) => String(call[0]))).toContain("/api/queries?domain=ads&before=7");
	expect(screen.getByText(/Showing 2 queries — end of log/)).toBeTruthy();
});

test("a background refetch after new rows arrive leaves no gap between the loaded pages", async () => {
	// The newest-100 window moves up while the reader has a second page open.
	// Refetching only the first page would drop n20 and n19 out of the middle
	// of the table; the second page must be replayed from the fresh cursor.
	const before: Record<string, QueriesPage> = {
		"/api/queries": { queries: [row(20, "n20.example"), row(19, "n19.example")], next_before: 19 },
		"/api/queries?before=19": { queries: [row(18, "n18.example"), row(17, "n17.example")], next_before: null },
	};
	const after: Record<string, QueriesPage> = {
		"/api/queries": { queries: [row(22, "n22.example"), row(21, "n21.example")], next_before: 21 },
		"/api/queries?before=21": {
			queries: [row(20, "n20.example"), row(19, "n19.example"), row(18, "n18.example"), row(17, "n17.example")],
			next_before: null,
		},
	};
	let live = before;
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL) => {
			const payload = live[String(input)];
			if (payload === undefined) return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 });
			return json(payload);
		}),
	);

	const client = renderPage();
	await screen.findByText("n20.example");
	fireEvent.click(screen.getByRole("button", { name: "Load more" }));
	await screen.findByText("n17.example");

	live = after;
	await act(async () => {
		await client.invalidateQueries({ queryKey: ["queries"] });
	});

	await screen.findByText("n22.example");
	const shown = screen.getAllByText(/^n\d+\.example$/).map((cell) => cell.textContent);
	expect(shown).toEqual(["n22.example", "n21.example", "n20.example", "n19.example", "n18.example", "n17.example"]);
	expect(screen.getByText(/Showing 6 queries — end of log/)).toBeTruthy();
});

test("a 401 on load more routes through handleUnauthorized instead of the inline error", async () => {
	const assign = vi.fn();
	vi.stubGlobal("location", { pathname: "/queries", search: "", assign });
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL) => {
			const url = String(input);
			if (url === "/api/queries?before=19") {
				return new Response(JSON.stringify({ error: "unauthorized" }), {
					status: 401,
					headers: { "content-type": "application/json" },
				});
			}
			const payload = PAGES[url];
			if (payload === undefined) return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 });
			return new Response(JSON.stringify(payload), {
				status: 200,
				headers: { "content-type": "application/json" },
			});
		}),
	);

	renderPage();
	await screen.findByText("first.example");

	fireEvent.click(screen.getByRole("button", { name: "Load more" }));
	await waitFor(() => {
		expect(assign).toHaveBeenCalledWith(`/login?redirect=${encodeURIComponent("/queries")}`);
	});

	expect(screen.queryByRole("alert")).toBeNull();
	expect(screen.queryByText(/Failed to load more/)).toBeNull();
});
