import { act, fireEvent, render, screen, within } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { createQueryClient } from "@/lib/queryClient";
import type { Client, LiveQueryEvent } from "@/lib/types";
import { FakeEventSource } from "./fakeEventSource";
import LiveLogPage from "./LiveLogPage";

function client(ip: string, name: string, learnedName: string): Client {
	return {
		id: Number(ip.split(".").pop()),
		ip,
		name,
		learned_name: learnedName,
		group_id: 1,
		group: "default",
		hand_edited: name !== "",
		first_seen: 1_700_000_000,
		last_seen: 1_700_000_100,
	};
}

const CLIENTS: Client[] = [
	client("192.0.2.10", "Kitchen Pi", "pi.lan"),
	client("192.0.2.11", "", "laptop.lan"),
	client("192.0.2.12", "", ""),
];

beforeEach(() => {
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL) => {
			if (String(input) !== "/api/clients") {
				return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 });
			}
			return new Response(JSON.stringify({ clients: CLIENTS }), {
				status: 200,
				headers: { "content-type": "application/json" },
			});
		}),
	);
});

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

function frame(ts: number, domain: string, overrides: Partial<LiveQueryEvent> = {}): { data: string } {
	const payload: LiveQueryEvent = {
		ts,
		domain,
		client_ip: "192.0.2.10",
		qtype: 1,
		blocked: false,
		block_reason: "",
		response_time_us: 500,
		cache_hit: true,
		upstream: "",
		...overrides,
	};
	return { data: JSON.stringify(payload) };
}

function renderPage() {
	const sources: FakeEventSource[] = [];
	const createEventSource = (url: string) => {
		const es = new FakeEventSource(url);
		sources.push(es);
		return es;
	};
	render(
		<QueryClientProvider client={createQueryClient()}>
			<LiveLogPage createEventSource={createEventSource} />
		</QueryClientProvider>,
	);
	return sources;
}

test("streams rows, flags blocked ones, and freezes the display", () => {
	const sources = renderPage();
	expect(screen.getByText("Connecting…")).toBeTruthy();

	act(() => sources[0]!.emit("open"));
	expect(screen.getByRole("status", { name: "Live" })).toBeTruthy();
	expect(screen.getByText("Waiting for queries…")).toBeTruthy();

	act(() => {
		sources[0]!.emit("query", frame(1000, "ok.example"));
		sources[0]!.emit(
			"query",
			frame(1001, "ads.example", { blocked: true, block_reason: "blocklist:stevenblack", qtype: 28 }),
		);
	});

	expect(screen.getByText("ok.example")).toBeTruthy();
	expect(screen.getByText("Blocked")).toBeTruthy();
	expect(screen.getByText("blocklist:stevenblack")).toBeTruthy();
	expect(screen.getByText("AAAA")).toBeTruthy();
	// StyleX compiles to opaque class names, so the check is structural: a blocked
	// row carries every class a plain row does, plus the ones the flag adds.
	const blockedRow = screen.getByText("ads.example").closest("tr");
	const plainRow = screen.getByText("ok.example").closest("tr");
	const blockedClasses = new Set(blockedRow?.className.split(" "));
	const plainClasses = plainRow?.className.split(" ") ?? [];
	expect(plainClasses.every((name) => blockedClasses.has(name))).toBe(true);
	expect(blockedClasses.size).toBeGreaterThan(plainClasses.length);

	const freeze = screen.getByRole("button", { name: "Freeze" });
	fireEvent.click(freeze);
	expect(freeze.getAttribute("aria-pressed")).toBe("true");

	act(() => sources[0]!.emit("query", frame(1002, "later.example")));
	expect(screen.queryByText("later.example")).toBeNull();
	expect(screen.getByText(/3 in buffer/)).toBeTruthy();

	fireEvent.click(screen.getByRole("button", { name: "Resume" }));
	expect(screen.getByText("later.example")).toBeTruthy();
});

test("resolves each row's client to its display name, keeping the IP as the tooltip", async () => {
	const sources = renderPage();
	act(() => sources[0]!.emit("open"));
	act(() => {
		sources[0]!.emit("query", frame(1000, "named.example", { client_ip: "192.0.2.10" }));
		sources[0]!.emit("query", frame(1001, "learned.example", { client_ip: "192.0.2.11" }));
		sources[0]!.emit("query", frame(1002, "nameless.example", { client_ip: "192.0.2.12" }));
		sources[0]!.emit("query", frame(1003, "stranger.example", { client_ip: "192.0.2.99" }));
	});

	// A hand-typed name wins outright; the learned name never surfaces for it.
	const named = await screen.findByText("Kitchen Pi");
	expect(named.getAttribute("title")).toBe("192.0.2.10");
	expect(screen.queryByText("pi.lan")).toBeNull();

	// A learned name reads muted and nothing more here: the "learned" tag would
	// repeat on every row of the table, so the Clients page carries it instead.
	const learned = screen.getByText("laptop.lan");
	expect(learned.getAttribute("title")).toBe("192.0.2.11");
	expect(within(learned.closest("tr")!).queryByText("learned")).toBeNull();

	// A known client with neither name, and a client the loaded list has never
	// seen, both fall back to the bare address with no tooltip standing in.
	const nameless = screen.getByText("192.0.2.12");
	expect(nameless.getAttribute("title")).toBeNull();
	const stranger = screen.getByText("192.0.2.99");
	expect(stranger.getAttribute("title")).toBeNull();
	expect(screen.getByText("stranger.example").closest("tr")?.textContent).toContain("192.0.2.99");
});

test("rows stream in as bare IPs while the client list is still loading", async () => {
	let releaseClients: () => void = () => {};
	vi.stubGlobal(
		"fetch",
		vi.fn(
			(input: RequestInfo | URL) =>
				new Promise<Response>((resolve) => {
					if (String(input) !== "/api/clients") {
						resolve(new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 }));
						return;
					}
					releaseClients = () =>
						resolve(
							new Response(JSON.stringify({ clients: CLIENTS }), {
								status: 200,
								headers: { "content-type": "application/json" },
							}),
						);
				}),
		),
	);

	const sources = renderPage();
	act(() => sources[0]!.emit("open"));
	act(() => sources[0]!.emit("query", frame(1000, "named.example", { client_ip: "192.0.2.10" })));

	expect(screen.getByText("192.0.2.10")).toBeTruthy();
	expect(screen.queryByText("Kitchen Pi")).toBeNull();

	releaseClients();
	expect(await screen.findByText("Kitchen Pi")).toBeTruthy();
});

test("repeated connection failures show the viewer-cap state with a retry button", () => {
	const sources = renderPage();
	act(() => {
		sources[0]!.emit("error");
		sources[0]!.emit("error");
		sources[0]!.emit("error");
	});
	expect(screen.getByRole("alert").textContent).toContain("too many live viewers");

	fireEvent.click(screen.getByRole("button", { name: "Retry" }));
	expect(sources).toHaveLength(2);
	expect(screen.getByText("Connecting…")).toBeTruthy();
});
