import { screen, waitFor, within } from "@testing-library/react";
import { CONFIG_PATH, DATABASE, MANAGED_FILE, contentArea, renderPage, renderRoute, stubApi } from "./testFixtures";

/**
 * Authority is three-state, and the difference between the states is the whole
 * point: an unresolved status is never treated as database mode, and file mode
 * is a different rendering rather than the same forms with their controls
 * turned off.
 */

const NEVER = new Promise<Response>(() => {});

/**
 * Every button the file-mode configuration content is allowed to contain: the
 * runtime actions, and the Retry an error surface offers. Anything else is a
 * mutation control that should not have been rendered at all.
 */
const RUNTIME_BUTTONS = ["Update now", "Reload certificates", "Retry"];

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

function mutationControls(): Element[] {
	return [
		...contentArea().querySelectorAll(
			'input, textarea, select, [role="combobox"], [role="checkbox"], [role="switch"], [contenteditable]',
		),
	];
}

function buttonLabels(): string[] {
	return [...contentArea().querySelectorAll("button")].map((button) => (button.textContent ?? "").trim());
}

test("a pending status renders neither form nor definition list on Protection", async () => {
	stubApi(DATABASE, { responses: { "GET /api/config/status": NEVER } });
	await renderPage("/configuration/protection", "Protection");

	expect(await screen.findByText(/which configuration source this server obeys/i)).toBeTruthy();
	expect(mutationControls()).toHaveLength(0);
	expect(screen.queryByRole("button", { name: "Create" })).toBeNull();
});

test("a pending status still leaves the runtime actions live (R3-4)", async () => {
	stubApi(DATABASE, { responses: { "GET /api/config/status": NEVER } });
	await renderPage("/configuration/system", "System");

	const reload = (await screen.findByRole("button", { name: "Reload certificates" })) as HTMLButtonElement;
	expect(reload.disabled).toBe(false);
	// The settings form is not drawn behind it.
	expect(screen.queryByRole("button", { name: "Save" })).toBeNull();
});

test("a pending status renders neither form nor definition list on Resolution", async () => {
	stubApi(DATABASE, { responses: { "GET /api/config/status": NEVER } });
	await renderPage("/configuration/resolution", "Resolution");

	expect(await screen.findByText(/which configuration source this server obeys/i)).toBeTruthy();
	expect(mutationControls()).toHaveLength(0);
	expect(screen.queryByRole("button", { name: "Add upstream" })).toBeNull();
});

test("a failed status renders the error and a Retry, never editable forms", async () => {
	stubApi(DATABASE, {
		responses: { "GET /api/config/status": new Response(JSON.stringify({ error: "gone" }), { status: 404 }) },
	});
	await renderPage("/configuration/protection", "Protection");

	await screen.findByText(/cannot say whether a file or the database owns/i);
	expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
	expect(mutationControls()).toHaveLength(0);
});

test("a failed status renders the error and a Retry on Resolution too", async () => {
	stubApi(DATABASE, {
		responses: { "GET /api/config/status": new Response(JSON.stringify({ error: "gone" }), { status: 404 }) },
	});
	await renderPage("/configuration/resolution", "Resolution");

	await screen.findByText(/cannot say whether a file or the database owns/i);
	expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
	expect(mutationControls()).toHaveLength(0);
	expect(screen.queryByRole("button", { name: "Add upstream" })).toBeNull();
});

test("a failed status keeps Update now enabled on the Sources tab", async () => {
	stubApi(DATABASE, {
		responses: { "GET /api/config/status": new Response(JSON.stringify({ error: "gone" }), { status: 404 }) },
	});
	await renderPage("/configuration/protection?tab=sources", "Protection");

	await screen.findByText(/cannot say whether a file or the database owns/i);
	const update = screen.getByRole("button", { name: "Update now" }) as HTMLButtonElement;
	expect(update.disabled).toBe(false);
});

test("a failed status is announced by the shell on a page that is not configuration", async () => {
	stubApi(DATABASE, {
		responses: {
			"GET /api/config/status": new Response(JSON.stringify({ error: "gone" }), { status: 404 }),
			"GET /api/overview?period=24h": {
				period: "24h",
				since: 0,
				until: 86400,
				bucket_seconds: 1800,
				totals: { queries: 0, blocked: 0, clients: 0, avg_response_time_us: null },
				buckets: [],
				clients: [],
				other: [],
				types: [],
				routes: [],
				coverage: { complete: true, available_since: 0 },
			},
		},
	});
	renderRoute("/overview");

	await waitFor(() => expect(screen.getByText(/file authority and pending restarts cannot be shown/i)).toBeTruthy());
});

test("database authority renders the editable groups form", async () => {
	stubApi(DATABASE);
	await renderPage("/configuration/protection", "Protection");

	expect(await screen.findByRole("button", { name: "Create" })).toBeTruthy();
	expect(screen.getByLabelText("New group")).toBeTruthy();
	expect(screen.queryByText(/loaded from/i)).toBeNull();
});

test("file authority renders definitions with their exact ZON keys, not a form", async () => {
	stubApi(MANAGED_FILE);
	await renderPage("/configuration/protection", "Protection");

	const note = await screen.findByText(/are loaded from/i);
	expect(note.textContent).toContain(CONFIG_PATH);

	const detail = await screen.findByText("Safe search");
	expect(within(detail).getByText("groups[].safe_search")).toBeTruthy();
	expect(screen.queryByRole("button", { name: "Create" })).toBeNull();
});

test("file mode: Protection renders zero mutation controls (D12)", async () => {
	stubApi(MANAGED_FILE);
	await renderPage("/configuration/protection", "Protection");
	await screen.findByText("groups[].safe_search");

	expect(mutationControls()).toHaveLength(0);
	for (const label of buttonLabels()) expect(RUNTIME_BUTTONS).toContain(label);
});

test("file mode: the Sources tab shows the catalogue with its key and only Update now", async () => {
	stubApi(MANAGED_FILE);
	await renderPage("/configuration/protection?tab=sources", "Protection");
	await screen.findByText("blocklist_sources");

	expect(mutationControls()).toHaveLength(0);
	expect(buttonLabels()).toEqual(["Update now"]);
});

test("file mode: the catalogue keeps provenance and both skipped counters", async () => {
	stubApi(MANAGED_FILE);
	await renderPage("/configuration/protection?tab=sources", "Protection");
	await screen.findByText("blocklist_sources");

	// Trackers is the suggested source, and the one with skipped lines: hiding
	// either fact under file authority would hide a parse failure.
	const row = screen.getByText("Trackers").closest("tr")!;
	expect(within(row).getByText("Suggested")).toBeTruthy();
	expect(screen.getByRole("columnheader", { name: "Skipped regex" })).toBeTruthy();
	expect(screen.getByRole("columnheader", { name: "Skipped unsupported" })).toBeTruthy();
	const cells = [...row.querySelectorAll("td")].map((cell) => cell.textContent);
	expect(cells).toContain("3");
	expect(cells).toContain("4");
	expect(
		screen.getByText(/Skipped unsupported lines are syntax nxdns cannot translate into a DNS decision/),
	).toBeTruthy();
});

test("file mode: the Resolution upstream pool is a table with no controls", async () => {
	stubApi(MANAGED_FILE);
	await renderPage("/configuration/resolution", "Resolution");
	await screen.findByText("upstreams");

	expect(mutationControls()).toHaveLength(0);
	expect(buttonLabels()).toEqual([]);
});

test("file mode: the Records tab is a table with no controls", async () => {
	stubApi(MANAGED_FILE);
	await renderPage("/configuration/resolution?tab=records", "Resolution");
	await screen.findByText("local_records");

	expect(mutationControls()).toHaveLength(0);
	expect(buttonLabels()).toEqual([]);
});

test("file mode: the Forward zones tab is a table with no controls", async () => {
	stubApi(MANAGED_FILE);
	await renderPage("/configuration/resolution?tab=zones", "Resolution");
	await screen.findByText("forward_zones");

	expect(mutationControls()).toHaveLength(0);
	expect(buttonLabels()).toEqual([]);
});

test("file mode: System renders scalars as definitions and keeps Reload certificates", async () => {
	stubApi(MANAGED_FILE);
	await renderPage("/configuration/system", "System");

	const retention = await screen.findByText("Retention days");
	expect(within(retention).getByText("logging.retention_days")).toBeTruthy();
	expect(mutationControls()).toHaveLength(0);
	expect(buttonLabels()).toEqual(["Reload certificates"]);
});

test("file mode: authentication is a derived status with no invented key (D13)", async () => {
	stubApi(MANAGED_FILE);
	await renderPage("/configuration/system", "System");

	const auth = await screen.findByText("Authentication");
	expect(auth.textContent).toBe("Authentication");
	expect(within(auth).queryByText(/web\.auth_enabled/)).toBeNull();
	expect(screen.queryByText("web.auth_enabled")).toBeNull();
});
