import { act } from "react";
import { QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen } from "@testing-library/react";
import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { safeRedirect } from "@/auth/LoginPage";
import { AuthProvider, resetAuthProbeForTests } from "@/auth/store";
import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";

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

beforeEach(() => {
	sessionStorage.clear();
	resetAuthProbeForTests();
});

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

async function flushAll() {
	for (let i = 0; i < 20; i++) {
		await act(async () => {
			vi.advanceTimersByTime(0);
			await Promise.resolve();
		});
	}
}

function renderLoginRoute() {
	const queryClient = createQueryClient();
	const router = createAppRouter(createMemoryHistory({ initialEntries: ["/login"] }), queryClient);
	render(
		<AuthProvider>
			<QueryClientProvider client={queryClient}>
				<RouterProvider router={router} />
			</QueryClientProvider>
		</AuthProvider>,
	);
}

test("429 login shows a ticking countdown and keeps submit disabled until it ends", async () => {
	vi.useFakeTimers();
	const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
		if (String(input) !== "/api/auth/login") return jsonResponse({ error: "not stubbed" }, 404);
		const body = JSON.parse(String(init?.body)) as { password: string };
		if (body.password === "") return jsonResponse({ error: "password required" }, 401);
		return jsonResponse({ error: "rate limited" }, 429, { "retry-after": "3" });
	});
	vi.stubGlobal("fetch", fetchMock);

	renderLoginRoute();
	await flushAll();

	const input = screen.getByLabelText("Password");
	fireEvent.change(input, { target: { value: "wrong" } });
	fireEvent.submit(input.closest("form") as HTMLFormElement);
	await flushAll();

	const button = screen.getByRole("button", { name: "Log in" }) as HTMLButtonElement;
	expect(screen.getByRole("alert").textContent).toBe("Too many attempts. Try again in 3s.");
	expect(button.disabled).toBe(true);

	const callsAtLockout = fetchMock.mock.calls.length;
	fireEvent.submit(input.closest("form") as HTMLFormElement);
	await flushAll();
	expect(fetchMock.mock.calls.length).toBe(callsAtLockout);

	act(() => {
		vi.advanceTimersByTime(1000);
	});
	expect(screen.getByRole("alert").textContent).toBe("Too many attempts. Try again in 2s.");
	expect(button.disabled).toBe(true);

	act(() => {
		vi.advanceTimersByTime(2000);
	});
	expect(screen.getByRole("alert").textContent).toBe("Too many attempts. Try again shortly.");
	expect(button.disabled).toBe(false);
});

test("safeRedirect only allows same-origin absolute paths", () => {
	expect(safeRedirect(undefined)).toBe("/");
	expect(safeRedirect("/activity")).toBe("/activity");
	expect(safeRedirect("/activity?x=1")).toBe("/activity?x=1");
	expect(safeRedirect("//evil.example")).toBe("/");
	expect(safeRedirect("https://evil.example")).toBe("/");
	expect(safeRedirect("/\\evil.example")).toBe("/");
	expect(safeRedirect("/\\\\evil.example")).toBe("/");
	expect(safeRedirect("\\evil")).toBe("/");
});
