/**
 * The Pause control, migrated from the header PauseWidget it replaces. Every
 * behaviour that widget pinned is pinned here, now driven by `Health.protection`
 * rather than by a second poll of `/api/pause`.
 */

import { act } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import PauseControl from "@/features/pause/PauseControl";
import { formatClock } from "@/lib/format";
import { createQueryClient } from "@/lib/queryClient";
import { health } from "@/lib/healthFixture";
import { queryKeys } from "@/lib/queries";
import type { Health, PausePost, PauseState } from "@/lib/types";

let protection: Health["protection"];
let postBodies: PausePost[];
let postFailure: (() => Response) | null;
let healthFails: boolean;

function jsonResponse(payload: unknown): Response {
	return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" } });
}

beforeEach(() => {
	protection = { state: "active", until: null };
	postBodies = [];
	postFailure = null;
	healthFails = false;
	vi.stubGlobal(
		"fetch",
		vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
			const url = String(input);
			if (url === "/api/health") {
				if (healthFails) {
					return new Response(JSON.stringify({ error: "nope" }), {
						status: 400,
						headers: { "content-type": "application/json" },
					});
				}
				return jsonResponse(health({ protection }));
			}
			if (url === "/api/pause" && init?.method === "POST") {
				const body = JSON.parse(String(init.body)) as PausePost;
				postBodies.push(body);
				if (postFailure !== null) return postFailure();
				protection = body.paused
					? {
							state: "paused",
							until:
								body.duration_seconds == null
									? null
									: Math.floor(Date.now() / 1000) + body.duration_seconds,
						}
					: { state: "active", until: null };
				const echo: PauseState = { paused: body.paused, until: protection.until };
				return jsonResponse(echo);
			}
			return jsonResponse({ error: "not stubbed" });
		}),
	);
});

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

function renderControl(client?: QueryClient) {
	render(
		<QueryClientProvider client={client ?? createQueryClient()}>
			<PauseControl />
		</QueryClientProvider>,
	);
}

async function findPauseTrigger(): Promise<HTMLButtonElement> {
	await waitFor(() => {
		const button = screen.getByRole("button", { name: "Pause" }) as HTMLButtonElement;
		expect(button.disabled).toBe(false);
	});
	return screen.getByRole("button", { name: "Pause" }) as HTMLButtonElement;
}

test("unpaused: duration menu pauses with the picked duration_seconds", async () => {
	renderControl();

	const trigger = await findPauseTrigger();
	expect(trigger.getAttribute("aria-expanded")).toBe("false");

	fireEvent.click(trigger);
	expect(trigger.getAttribute("aria-expanded")).toBe("true");
	expect(screen.getAllByRole("menuitem").map((item) => item.textContent)).toEqual([
		"60 seconds",
		"5 minutes",
		"30 minutes",
		"Indefinitely",
	]);

	fireEvent.click(screen.getByRole("menuitem", { name: "5 minutes" }));
	await waitFor(() => expect(postBodies).toEqual([{ paused: true, duration_seconds: 300 }]));

	await screen.findByRole("button", { name: "Resume" });
});

test("indefinite pause sends no duration_seconds", async () => {
	renderControl();

	fireEvent.click(await findPauseTrigger());
	fireEvent.click(screen.getByRole("menuitem", { name: "Indefinitely" }));
	await waitFor(() => expect(postBodies).toEqual([{ paused: true }]));

	await screen.findByRole("button", { name: "Resume" });
});

test("resume posts paused false and returns to the Pause button", async () => {
	protection = { state: "paused", until: null };
	renderControl();

	fireEvent.click(await screen.findByRole("button", { name: "Resume" }));
	await waitFor(() => expect(postBodies).toEqual([{ paused: false }]));
	await screen.findByRole("button", { name: "Pause" });
});

test("a timed pause says until when, beside the control that would end it", () => {
	const nowSec = Math.floor(Date.now() / 1000);
	const client = createQueryClient();
	client.setQueryData(queryKeys.health, health({ protection: { state: "paused", until: nowSec + 90 } }));
	renderControl(client);

	// The same clock format the Diagnostics health strip writes, off the same
	// reading, so the two cannot say different things about one pause.
	expect(screen.getByText(`Paused until ${formatClock(nowSec + 90)}`)).toBeTruthy();
	expect(screen.getByRole("button", { name: "Resume" })).toBeTruthy();
});

test("a pause with no end says so without inventing a time", () => {
	const client = createQueryClient();
	client.setQueryData(queryKeys.health, health({ protection: { state: "paused", until: null } }));
	renderControl(client);

	expect(screen.getByText("Paused")).toBeTruthy();
	expect(screen.queryByText(/until/)).toBeNull();
});

test("an active resolver states nothing: the button already says Pause", async () => {
	renderControl();

	expect(await findPauseTrigger()).toBeTruthy();
	expect(screen.queryByText(/^Paused/)).toBeNull();
});

test("escape closes the duration menu", async () => {
	renderControl();

	fireEvent.click(await findPauseTrigger());
	expect(screen.getByRole("menuitem", { name: "Indefinitely" })).toBeTruthy();

	// Opening the menu moves focus into it, so Escape is pressed where the reader
	// is, not on the trigger they left.
	fireEvent.keyDown(screen.getByRole("menu"), { key: "Escape" });

	await waitFor(() => expect(screen.queryByRole("menuitem", { name: "Indefinitely" })).toBeNull());
	expect(screen.getByRole("button", { name: "Pause" }).getAttribute("aria-expanded")).toBe("false");
});

test("failed pause with 429 shows a ticking retry countdown", async () => {
	vi.useFakeTimers({ shouldAdvanceTime: true });
	postFailure = () =>
		new Response(JSON.stringify({ error: "rate limited" }), {
			status: 429,
			headers: { "content-type": "application/json", "Retry-After": "30" },
		});
	renderControl();

	fireEvent.click(await findPauseTrigger());
	fireEvent.click(screen.getByRole("menuitem", { name: "5 minutes" }));

	const alert = await screen.findByRole("alert");
	expect(alert.textContent).toBe("Rate limited. Try again in 30s.");
	act(() => {
		vi.advanceTimersByTime(1000);
	});
	expect(alert.textContent).toBe("Rate limited. Try again in 29s.");
});

test("failed pause with 503 shows the degraded message", async () => {
	postFailure = () =>
		new Response(JSON.stringify({ error: "unavailable" }), {
			status: 503,
			headers: { "content-type": "application/json" },
		});
	renderControl();

	fireEvent.click(await findPauseTrigger());
	fireEvent.click(screen.getByRole("menuitem", { name: "60 seconds" }));

	const alert = await screen.findByRole("alert");
	expect(alert.textContent).toBe("The server is starting or degraded. Try again shortly.");
});

test("a successful pause clears the previous mutation error", async () => {
	postFailure = () =>
		new Response(JSON.stringify({ error: "unavailable" }), {
			status: 503,
			headers: { "content-type": "application/json" },
		});
	renderControl();

	fireEvent.click(await findPauseTrigger());
	fireEvent.click(screen.getByRole("menuitem", { name: "5 minutes" }));
	await screen.findByRole("alert");

	postFailure = null;
	fireEvent.click(screen.getByRole("button", { name: "Pause" }));
	fireEvent.click(screen.getByRole("menuitem", { name: "5 minutes" }));

	await screen.findByRole("button", { name: "Resume" });
	expect(screen.queryByRole("alert")).toBeNull();
});

test("no control at all while protection is unavailable", async () => {
	protection = { state: "unavailable", until: null };
	const client = createQueryClient();
	renderControl(client);

	await waitFor(() => expect(client.getQueryData(queryKeys.health)).toBeDefined());
	expect(screen.queryByRole("button")).toBeNull();
});

test("a failed poll after a good one withdraws the control rather than acting on a stale state", async () => {
	vi.useFakeTimers({ shouldAdvanceTime: true });
	renderControl();
	await vi.waitFor(() =>
		expect((screen.getByRole("button", { name: "Pause" }) as HTMLButtonElement).disabled).toBe(false),
	);

	healthFails = true;
	await vi.advanceTimersByTimeAsync(11_000);

	await vi.waitFor(() => expect(screen.queryByRole("button")).toBeNull());
	vi.useRealTimers();
});

test("a menu left open when the control withdraws does not come back open", async () => {
	vi.useFakeTimers({ shouldAdvanceTime: true });
	renderControl();
	fireEvent.click(await findPauseTrigger());
	expect(screen.getByRole("menuitem", { name: "Indefinitely" })).toBeTruthy();

	protection = { state: "unavailable", until: null };
	await vi.advanceTimersByTimeAsync(11_000);
	await vi.waitFor(() => expect(screen.queryByRole("button")).toBeNull());

	protection = { state: "active", until: null };
	await vi.advanceTimersByTimeAsync(11_000);

	// Protection is back, and so is the trigger — but the menu is a thing the
	// reader opened, and nobody opened this one.
	const trigger = await vi.waitFor(() => screen.getByRole("button", { name: "Pause" }));
	expect(trigger.getAttribute("aria-expanded")).toBe("false");
	expect(screen.queryByRole("menuitem", { name: "Indefinitely" })).toBeNull();
	vi.useRealTimers();
});

test("a menu open when someone else pauses does not reopen when that pause ends", async () => {
	vi.useFakeTimers({ shouldAdvanceTime: true });
	renderControl();
	fireEvent.click(await findPauseTrigger());
	expect(screen.getByRole("menuitem", { name: "Indefinitely" })).toBeTruthy();

	// Filtering is paused from somewhere else, and this browser learns it from
	// the poll. The Resume rendering has no menu.
	protection = { state: "paused", until: null };
	await vi.advanceTimersByTimeAsync(11_000);
	await vi.waitFor(() => expect(screen.getByRole("button", { name: "Resume" })).toBeTruthy());

	protection = { state: "active", until: null };
	await vi.advanceTimersByTimeAsync(11_000);

	const trigger = await vi.waitFor(() => screen.getByRole("button", { name: "Pause" }));
	expect(trigger.getAttribute("aria-expanded")).toBe("false");
	expect(screen.queryByRole("menuitem", { name: "Indefinitely" })).toBeNull();
	vi.useRealTimers();
});

test("protection unknown offers no control at all, by the same rule as unavailable", () => {
	// Health has not answered. Which of Pause and Resume applies is exactly what
	// it has not said, so the control names neither.
	renderControl();

	expect(screen.queryByRole("button")).toBeNull();
});
