import { cleanup, fireEvent, render, screen, waitFor, within } 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 { DIAGNOSTIC_CODES, type DiagnosticEvent } from "@/lib/types";
import { health } from "@/lib/healthFixture";
import { EVENT_COPY } from "./eventCopy";

const NOW_S = Math.floor(Date.now() / 1000);

function event(overrides: Partial<DiagnosticEvent> = {}): DiagnosticEvent {
	return {
		id: 42,
		code: "blocklist.refresh",
		component: "blocklist",
		subject: "StevenBlack",
		severity: "warning",
		first_seen: NOW_S - 7200,
		last_seen: NOW_S - 600,
		occurrences: 4,
		resolved_at: null,
		detail: "download failed: ConnectionTimedOut",
		...overrides,
	};
}

/** A 204: what `DELETE /api/diagnostics/{id}` answers on a purge. */
const NO_CONTENT = Symbol("204");

let responses: Record<string, unknown>;
let requested: string[];

beforeEach(() => {
	requested = [];
	responses = {
		"/api/version": { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 },
		// The shell reads health for the Diagnostics nav badge on every route.
		"/api/health": health(),
	};
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL, init?: { method?: string }) => {
			const url = String(input);
			const method = init?.method ?? "GET";
			const key = method === "GET" ? url : `${method} ${url}`;
			requested.push(key);
			const payload = responses[key];
			if (payload === undefined)
				return new Response(JSON.stringify({ error: "no such event" }), {
					status: 404,
					headers: { "content-type": "application/json" },
				});
			if (payload === NO_CONTENT) return new Response(null, { status: 204 });
			return new Response(JSON.stringify(payload), {
				status: 200,
				headers: { "content-type": "application/json" },
			});
		}),
	);
});

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

/**
 * `retry` is off in the failure test: the shared client backs 5xx off for
 * seconds, which the render assertions would sit through for nothing.
 */
function renderDetail(id: number, { retry = true } = {}) {
	const queryClient = createQueryClient();
	if (!retry) {
		const defaults = queryClient.getDefaultOptions();
		queryClient.setDefaultOptions({ ...defaults, queries: { ...defaults.queries, retry: false } });
	}
	const router = createAppRouter(createMemoryHistory({ initialEntries: [`/diagnostics/${id}`] }), queryClient);
	render(
		<AuthProvider>
			<QueryClientProvider client={queryClient}>
				<RouterProvider router={router} />
			</QueryClientProvider>
		</AuthProvider>,
	);
	return router;
}

test("an open episode shows its facts, its copy and the error the server sent", async () => {
	responses["/api/diagnostics/42"] = event();
	renderDetail(42);

	await screen.findByRole("heading", { name: "Blocklist source failed to update" });
	expect(screen.getByText("Warning")).toBeTruthy();
	expect(screen.getByText("StevenBlack")).toBeTruthy();
	expect(screen.getByText("Active for 2h")).toBeTruthy();
	expect(screen.getByText("Not yet — still failing")).toBeTruthy();
	expect(screen.getByText("4")).toBeTruthy();
	expect(screen.getByText("blocklist.refresh")).toBeTruthy();
	expect(screen.getByText(EVENT_COPY["blocklist.refresh"].impact)).toBeTruthy();
	expect(screen.getByText(EVENT_COPY["blocklist.refresh"].remediation)).toBeTruthy();
	expect(screen.getByText("download failed: ConnectionTimedOut")).toBeTruthy();
	expect(screen.getByRole("link", { name: "Go to Blocklist sources" }).getAttribute("href")).toBe(
		"/configuration/protection?tab=sources",
	);
});

test("a resolved episode states how long it lasted, not how long it has run", async () => {
	responses["/api/diagnostics/7"] = event({ id: 7, resolved_at: NOW_S - 3600 });
	renderDetail(7);

	await screen.findByRole("heading", { name: "Blocklist source failed to update" });
	expect(screen.getByText("Resolved after 1h")).toBeTruthy();
	expect(screen.queryByText("Not yet — still failing")).toBeNull();
});

test("an open episode offers no purge", async () => {
	responses["/api/diagnostics/42"] = event();
	renderDetail(42);

	await screen.findByRole("heading", { name: "Blocklist source failed to update" });
	expect(screen.queryByRole("button", { name: "Purge" })).toBeNull();
});

test("purging a resolved episode asks first, then returns to the list", async () => {
	responses["/api/diagnostics/7"] = event({ id: 7, resolved_at: NOW_S - 3600 });
	responses["DELETE /api/diagnostics/7"] = NO_CONTENT;
	responses["/api/diagnostics?state=active"] = { events: [], next_before: null, active: { warnings: 0, errors: 0 } };
	responses["/api/diagnostics?state=resolved"] = {
		events: [],
		next_before: null,
		active: { warnings: 0, errors: 0 },
	};
	const router = renderDetail(7);

	await screen.findByRole("heading", { name: "Blocklist source failed to update" });
	fireEvent.click(screen.getByRole("button", { name: "Purge" }));

	const dialog = await screen.findByRole("alertdialog");
	expect(dialog.textContent).toContain("Purge this resolved event? Its history is gone for good.");
	fireEvent.click(within(dialog).getByRole("button", { name: "Cancel" }));
	expect(requested).not.toContain("DELETE /api/diagnostics/7");

	fireEvent.click(screen.getByRole("button", { name: "Purge" }));
	fireEvent.click(within(await screen.findByRole("alertdialog")).getByRole("button", { name: "Purge" }));

	await waitFor(() => expect(requested).toContain("DELETE /api/diagnostics/7"));
	// The row it was showing no longer exists, so the page it navigates to is
	// the list rather than a 404 of its own.
	await waitFor(() => expect(router.state.location.pathname).toBe("/diagnostics"));
});

test("a refused purge stays on the event and shows why", async () => {
	responses["/api/diagnostics/7"] = event({ id: 7, resolved_at: NOW_S - 3600 });
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL, init?: { method?: string }) => {
			if (init?.method === "DELETE")
				return new Response(JSON.stringify({ error: "the event is still active" }), {
					status: 409,
					headers: { "content-type": "application/json" },
				});
			return new Response(JSON.stringify(responses[String(input)] ?? {}), {
				status: 200,
				headers: { "content-type": "application/json" },
			});
		}),
	);
	const router = renderDetail(7, { retry: false });

	await screen.findByRole("heading", { name: "Blocklist source failed to update" });
	fireEvent.click(screen.getByRole("button", { name: "Purge" }));
	fireEvent.click(within(await screen.findByRole("alertdialog")).getByRole("button", { name: "Purge" }));

	const alert = await screen.findByRole("alert");
	expect(alert.textContent).toContain("the event is still active");
	expect(router.state.location.pathname).toBe("/diagnostics/7");
});

test("every code renders its own title, impact and remediation", async () => {
	for (const [index, code] of DIAGNOSTIC_CODES.entries()) {
		const id = 100 + index;
		responses[`/api/diagnostics/${id}`] = event({ id, code, component: code.slice(0, code.indexOf(".")) });
		renderDetail(id);

		const copy = EVENT_COPY[code];
		await screen.findByRole("heading", { name: copy.title });
		expect(screen.getByText(copy.impact), code).toBeTruthy();
		expect(screen.getByText(copy.remediation), code).toBeTruthy();
		screen.getByText(code);
		cleanup();
	}
});

test("an event retention has removed shows the server's message, not an empty page", async () => {
	renderDetail(999);
	await screen.findByText("no such event");
	expect(screen.getByRole("link", { name: "← All diagnostics" })).toBeTruthy();
});

test("an unavailable store reports the failure instead of loading forever", async () => {
	responses["/api/diagnostics/42"] = event();
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL) =>
			String(input).startsWith("/api/diagnostics/")
				? new Response(JSON.stringify({ error: "store unavailable" }), {
						status: 503,
						headers: { "content-type": "application/json" },
					})
				: new Response(JSON.stringify(responses[String(input)] ?? {}), {
						status: 200,
						headers: { "content-type": "application/json" },
					}),
		),
	);
	renderDetail(42, { retry: false });

	const alert = await screen.findByRole("alert");
	expect(alert.textContent).toContain("The server is starting or degraded.");
	expect(screen.queryByText("Loading event…")).toBeNull();
	expect(screen.getByRole("link", { name: "← All diagnostics" })).toBeTruthy();
});
