import { render, screen, 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 type { Authority, Settings, SettingsEnvelope } from "@/lib/types";

// One file for the whole file-mode sweep: the settings envelope is the only
// discovery mechanism, so every page test needs the same stubbed envelope.

const CONFIG_PATH = "/etc/nxdns/config.zon";

const DATABASE: Authority = { mode: "database", path: null, reconciled_at: null };
const MANAGED_FILE: Authority = { mode: "managed_file", path: CONFIG_PATH, reconciled_at: 1754899200 };

function baseSettings(): Settings {
	return {
		upstream: { attempt_timeout_ms: 2500, read_timeout_ms: 3000, total_timeout_ms: 5000 },
		dns: { bind_ipv4: "0.0.0.0", bind_ipv6: "::", port: 53, rate_limit: 100, rate_window_seconds: 60 },
		blocking: { response: "zero", ttl: 300 },
		cache: { size: 10000, negative_ttl_max: 300 },
		web: {
			enabled: true,
			bind: "127.0.0.1",
			port: 8080,
			session_ttl_hours: 24,
			api_rate_limit_per_min: 60,
			api_localhost_exempt: true,
			sse_max_connections_per_ip: 2,
			trusted_proxies: "",
			auth_enabled: true,
		},
		doh_server: { enabled: false, bind: "0.0.0.0", port: 443, cert_path: "", key_path: "" },
		dot_server: { enabled: false, bind: "0.0.0.0", port: 853, cert_path: "", key_path: "" },
		edns: { ecs_mode: "strip" },
		logging: {
			level: "info",
			retention_days: 30,
			query_log_buffer_max: 10000,
			hide_domains: false,
			hide_client_ips: false,
			output: "stderr",
			file_path: "",
			max_size_mb: 50,
			max_files: 3,
		},
		disk: { min_free_mb: 100, warn_free_mb: 500 },
		blocklist_update: { enabled: true, interval_hours: 24 },
	};
}

function envelope(authority: Authority): SettingsEnvelope {
	return { settings: baseSettings(), restart_required: [], authority };
}

const GROUPS = {
	groups: [
		{ id: 1, name: "default", safe_search: false },
		{ id: 2, name: "kids", safe_search: true },
	],
};

const BLOCKLISTS = {
	blocklists: [
		{
			id: 1,
			url: "https://example.com/ads.txt",
			name: "Ads",
			enabled: true,
			is_suggested: false,
			last_updated: null,
			domain_count: 100,
			wildcard_count: 0,
			skipped_regex_count: 0,
			checksum: null,
		},
	],
};

const RULES = {
	rules: [
		{
			id: 1,
			group_id: 1,
			group: "default",
			pattern: "ads.example.com",
			kind: "exact",
			action: "block",
			created_at: 1700000000,
		},
	],
};

const CLIENTS = {
	clients: [
		{
			id: 1,
			ip: "192.168.1.10",
			name: "laptop",
			group_id: 1,
			group: "default",
			hand_edited: true,
			first_seen: 1700000000,
			last_seen: 1700003600,
		},
		{
			id: 2,
			ip: "192.168.1.11",
			name: "",
			group_id: 2,
			group: "kids",
			hand_edited: false,
			first_seen: 1700000000,
			last_seen: 1700007200,
		},
	],
};

const PREFIXES = {
	client_prefixes: [{ id: 1, prefix: "192.168.1.0/24", group_id: 2, group: "kids", priority: 100 }],
};

const VERSION = { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 };

const BASE: Record<string, unknown> = {
	"GET /api/version": VERSION,
	"GET /api/groups": GROUPS,
	"GET /api/blocklists": BLOCKLISTS,
	"GET /api/rules": RULES,
	"GET /api/clients": CLIENTS,
	"GET /api/client-prefixes": PREFIXES,
};

function stubFetch(map: Record<string, unknown>) {
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
			const key = `${init?.method ?? "GET"} ${String(input)}`;
			const payload = map[key];
			if (payload === undefined) {
				return new Response(JSON.stringify({ error: `not stubbed: ${key}` }), { status: 404 });
			}
			return new Response(JSON.stringify(payload), {
				status: 200,
				headers: { "content-type": "application/json" },
			});
		}),
	);
}

async function renderAt(route: string, heading: string, authority: Authority) {
	stubFetch({ ...BASE, "GET /api/settings": envelope(authority) });
	const queryClient = createQueryClient();
	const router = createAppRouter(createMemoryHistory({ initialEntries: [route] }), queryClient);
	render(
		<AuthProvider>
			<QueryClientProvider client={queryClient}>
				<RouterProvider router={router} />
			</QueryClientProvider>
		</AuthProvider>,
	);
	await screen.findByRole("heading", { name: heading });
}

function button(name: string): HTMLButtonElement {
	return screen.getByRole("button", { name }) as HTMLButtonElement;
}

function clientRow(ip: string): HTMLElement {
	const row = screen.getByText(ip).closest("tr");
	if (row === null) throw new Error(`no client row for ${ip}`);
	return row;
}

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

test("the banner names the managed file in file mode", async () => {
	await renderAt("/rules", "Rules", MANAGED_FILE);

	const banner = await screen.findByText(/configuration is managed by/i);
	expect(banner.textContent).toContain(CONFIG_PATH);
	expect(banner.textContent).toMatch(/restart/i);
	expect(banner.closest('[role="status"]')).toBeTruthy();
});

test("the banner is absent in database mode", async () => {
	await renderAt("/rules", "Rules", DATABASE);

	await screen.findByRole("button", { name: "Create rule" });
	expect(screen.queryByText(/configuration is managed by/i)).toBeNull();
});

function kidsRow(): HTMLElement {
	const row = screen.getByText("kids").closest("li");
	if (row === null) throw new Error("no row for group kids");
	return row;
}

test("file mode disables the Groups create and delete controls", async () => {
	await renderAt("/groups", "Groups", MANAGED_FILE);
	await screen.findByText(/configuration is managed by/i);

	expect(button("Create").disabled).toBe(true);
	expect((within(kidsRow()).getByRole("button", { name: "Delete" }) as HTMLButtonElement).disabled).toBe(true);
	expect((within(kidsRow()).getByRole("checkbox", { name: "Safe search" }) as HTMLInputElement).disabled).toBe(true);
});

test("database mode leaves the Groups create and delete controls enabled", async () => {
	await renderAt("/groups", "Groups", DATABASE);
	await screen.findByRole("button", { name: "Create" });

	expect(button("Create").disabled).toBe(false);
	expect((within(kidsRow()).getByRole("button", { name: "Delete" }) as HTMLButtonElement).disabled).toBe(false);
	expect((within(kidsRow()).getByRole("checkbox", { name: "Safe search" }) as HTMLInputElement).disabled).toBe(false);
});

test("file mode disables the Rules create and delete controls", async () => {
	await renderAt("/rules", "Rules", MANAGED_FILE);
	await screen.findByText(/configuration is managed by/i);

	expect(button("Create rule").disabled).toBe(true);
	expect(button("Delete").disabled).toBe(true);
});

test("database mode leaves the Rules create and delete controls enabled", async () => {
	await renderAt("/rules", "Rules", DATABASE);
	await screen.findByRole("button", { name: "Create rule" });

	expect(button("Create rule").disabled).toBe(false);
	expect(button("Delete").disabled).toBe(false);
});

test("file mode keeps delete live for an observed client and blocks it for a declared one", async () => {
	await renderAt("/clients", "Clients", MANAGED_FILE);
	await screen.findByText(/configuration is managed by/i);

	const declared = clientRow("192.168.1.10");
	const observed = clientRow("192.168.1.11");
	expect((within(declared).getByRole("button", { name: "Delete" }) as HTMLButtonElement).disabled).toBe(true);
	expect((within(observed).getByRole("button", { name: "Delete" }) as HTMLButtonElement).disabled).toBe(false);
	expect((within(observed).getByRole("button", { name: "Edit" }) as HTMLButtonElement).disabled).toBe(true);
});

test("file mode leaves the blocklist refresh button enabled", async () => {
	await renderAt("/blocklists", "Blocklists", MANAGED_FILE);
	await screen.findByText(/configuration is managed by/i);

	expect(button("Update now").disabled).toBe(false);
	expect(button("Add source").disabled).toBe(true);
	expect((screen.getByRole("checkbox", { name: "Ads enabled" }) as HTMLInputElement).disabled).toBe(true);
});
