import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { AuthProvider } from "@/auth/store";
import { dismissRestartBanner } from "@/features/settings/restartBanner";
import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";

const UPSTREAMS = {
	upstreams: [
		{ id: 1, url: "udp://1.1.1.1:53", priority: 100, enabled: true, tls_name: "" },
		{ id: 2, url: "tls://9.9.9.9:853", priority: 200, enabled: false, tls_name: "dns.quad9.net" },
	],
};

const RESPONSES: Record<string, unknown> = {
	"/api/upstreams": UPSTREAMS,
	"/api/version": { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 },
};

interface Call {
	url: string;
	method: string;
	body: unknown;
}

let calls: Call[];
let writeResponse: (() => Response) | null;

beforeEach(() => {
	calls = [];
	writeResponse = null;
	dismissRestartBanner();
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
			const url = String(input);
			const method = init?.method ?? "GET";
			if (method !== "GET") {
				calls.push({
					url,
					method,
					body: typeof init?.body === "string" ? JSON.parse(init.body) : undefined,
				});
				if (writeResponse !== null) return writeResponse();
				if (method === "DELETE") return new Response(null, { status: 204 });
				return new Response(
					JSON.stringify({
						id: 3,
						url: "udp://8.8.8.8:53",
						priority: 100,
						enabled: true,
						tls_name: "",
						restart_required: true,
					}),
					{ status: 200, headers: { "content-type": "application/json" } },
				);
			}
			const payload = RESPONSES[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();
	vi.restoreAllMocks();
});

async function renderUpstreamsRoute() {
	const queryClient = createQueryClient();
	const router = createAppRouter(createMemoryHistory({ initialEntries: ["/upstreams"] }), queryClient);
	render(
		<AuthProvider>
			<QueryClientProvider client={queryClient}>
				<RouterProvider router={router} />
			</QueryClientProvider>
		</AuthProvider>,
	);
	await screen.findByRole("heading", { name: "Upstreams" });
}

test("renders the upstream table and the add form", async () => {
	await renderUpstreamsRoute();

	expect(screen.getByText("udp://1.1.1.1:53")).toBeTruthy();
	expect(screen.getByText("tls://9.9.9.9:853")).toBeTruthy();
	expect(screen.getByText("100")).toBeTruthy();
	expect(screen.getByText("200")).toBeTruthy();
	expect(screen.getByText("dns.quad9.net")).toBeTruthy();

	const enabledToggle = screen.getByLabelText("udp://1.1.1.1:53 enabled") as HTMLInputElement;
	expect(enabledToggle.checked).toBe(true);
	const disabledToggle = screen.getByLabelText("tls://9.9.9.9:853 enabled") as HTMLInputElement;
	expect(disabledToggle.checked).toBe(false);

	expect(screen.getByRole("heading", { name: "Add upstream" })).toBeTruthy();
	expect(screen.getByText(/reflects the running pool/)).toBeTruthy();
});

test("adding an upstream posts every field and raises the restart banner", async () => {
	await renderUpstreamsRoute();

	fireEvent.change(screen.getByLabelText("URL"), { target: { value: "udp://8.8.8.8:53" } });
	fireEvent.change(screen.getByLabelText("Priority"), { target: { value: "150" } });
	fireEvent.click(screen.getByRole("button", { name: "Add upstream" }));

	await waitFor(() => expect(calls).toHaveLength(1));
	expect(calls[0]).toEqual({
		url: "/api/upstreams",
		method: "POST",
		body: { url: "udp://8.8.8.8:53", priority: 150, enabled: true, tls_name: "" },
	});

	const banner = await screen.findByRole("status");
	expect(banner.textContent).toContain("Changes saved. Restart nxdns to apply.");
});

test("toggling enabled resends the whole row", async () => {
	await renderUpstreamsRoute();

	fireEvent.click(screen.getByLabelText("tls://9.9.9.9:853 enabled"));

	await waitFor(() => expect(calls).toHaveLength(1));
	expect(calls[0]).toEqual({
		url: "/api/upstreams/2",
		method: "PUT",
		body: { url: "tls://9.9.9.9:853", priority: 200, enabled: true, tls_name: "dns.quad9.net" },
	});
	await screen.findByRole("status");
});

test("delete asks for confirmation and skips the request when refused", async () => {
	await renderUpstreamsRoute();

	vi.spyOn(window, "confirm").mockReturnValue(false);
	fireEvent.click(screen.getAllByRole("button", { name: "Delete" })[0]!);
	expect(calls).toHaveLength(0);

	vi.spyOn(window, "confirm").mockReturnValue(true);
	fireEvent.click(screen.getAllByRole("button", { name: "Delete" })[0]!);
	await waitFor(() => expect(calls).toHaveLength(1));
	expect(calls[0]!.method).toBe("DELETE");
	expect(calls[0]!.url).toBe("/api/upstreams/1");
	await screen.findByRole("status");
});

test("a 409 on create renders the conflict text inline", async () => {
	await renderUpstreamsRoute();

	writeResponse = () =>
		new Response(JSON.stringify({ error: "an upstream with that url already exists" }), {
			status: 409,
			headers: { "content-type": "application/json" },
		});

	fireEvent.change(screen.getByLabelText("URL"), { target: { value: "udp://1.1.1.1:53" } });
	fireEvent.click(screen.getByRole("button", { name: "Add upstream" }));

	const alert = await screen.findByRole("alert");
	expect(alert.textContent).toBe("an upstream with that url already exists");
	expect(screen.queryByRole("status")).toBeNull();
});

test("a 409 on toggle renders the last-enabled conflict above the form", async () => {
	await renderUpstreamsRoute();

	writeResponse = () =>
		new Response(JSON.stringify({ error: "the last enabled upstream cannot be disabled" }), {
			status: 409,
			headers: { "content-type": "application/json" },
		});

	fireEvent.click(screen.getByLabelText("udp://1.1.1.1:53 enabled"));

	const alert = await screen.findByRole("alert");
	expect(alert.textContent).toBe("the last enabled upstream cannot be disabled");
	expect(screen.queryByRole("status")).toBeNull();
});

test("a 409 on delete renders the last-enabled conflict", async () => {
	await renderUpstreamsRoute();

	vi.spyOn(window, "confirm").mockReturnValue(true);
	writeResponse = () =>
		new Response(JSON.stringify({ error: "the last enabled upstream cannot be removed" }), {
			status: 409,
			headers: { "content-type": "application/json" },
		});

	fireEvent.click(screen.getAllByRole("button", { name: "Delete" })[0]!);

	const alert = await screen.findByRole("alert");
	expect(alert.textContent).toBe("the last enabled upstream cannot be removed");
	expect(screen.queryByRole("status")).toBeNull();
});

test("editing a row seeds the form and PUTs the replaced row", async () => {
	await renderUpstreamsRoute();

	fireEvent.click(screen.getAllByRole("button", { name: "Edit" })[1]!);
	await screen.findByRole("heading", { name: "Edit tls://9.9.9.9:853" });

	expect((screen.getByLabelText("URL") as HTMLInputElement).value).toBe("tls://9.9.9.9:853");
	expect((screen.getByLabelText("Priority") as HTMLInputElement).value).toBe("200");
	expect((screen.getByLabelText("TLS name") as HTMLInputElement).value).toBe("dns.quad9.net");

	fireEvent.change(screen.getByLabelText("Priority"), { target: { value: "10" } });
	fireEvent.click(screen.getByRole("button", { name: "Save changes" }));

	await waitFor(() => expect(calls).toHaveLength(1));
	expect(calls[0]).toEqual({
		url: "/api/upstreams/2",
		method: "PUT",
		body: { url: "tls://9.9.9.9:853", priority: 10, enabled: false, tls_name: "dns.quad9.net" },
	});
	await screen.findByRole("heading", { name: "Add upstream" });
});
