import { ApiError, deleteGroup, getOverview, getQueries, listGroups, login, putGroupSources } from "@/lib/api";

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

const fetchMock = vi.fn<typeof fetch>();

beforeEach(() => {
	fetchMock.mockReset();
	vi.stubGlobal("fetch", fetchMock);
});

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

test("sends same-origin credentials and parses JSON", async () => {
	fetchMock.mockResolvedValue(jsonResponse({ authenticated: true, auth_required: true }));
	const response = await login({ password: "hunter2" });
	expect(response.auth_required).toBe(true);

	const [url, init] = fetchMock.mock.calls[0]!;
	expect(url).toBe("/api/auth/login");
	expect(init?.credentials).toBe("same-origin");
	expect(init?.method).toBe("POST");
	expect(init?.body).toBe(JSON.stringify({ password: "hunter2" }));
	const headers = (init?.headers ?? {}) as Record<string, string>;
	expect(headers["content-type"]).toBe("application/json");
});

test("throws ApiError with the {error} envelope message", async () => {
	fetchMock.mockResolvedValue(jsonResponse({ error: "duplicate group name" }, 409));
	const failure = await listGroups().catch((e: unknown) => e);
	expect(failure).toBeInstanceOf(ApiError);
	expect((failure as ApiError).status).toBe(409);
	expect((failure as ApiError).message).toBe("duplicate group name");
	expect((failure as ApiError).retryAfter).toBeUndefined();
});

test("falls back to a status message on a non-JSON error body", async () => {
	fetchMock.mockResolvedValue(new Response("<html>bad gateway</html>", { status: 502 }));
	const failure = await listGroups().catch((e: unknown) => e);
	expect(failure).toBeInstanceOf(ApiError);
	expect((failure as ApiError).message).toBe("HTTP 502");
});

test("parses Retry-After on 429", async () => {
	fetchMock.mockResolvedValue(jsonResponse({ error: "rate limited" }, 429, { "Retry-After": "17" }));
	const failure = await getOverview("1h").catch((e: unknown) => e);
	expect(failure).toBeInstanceOf(ApiError);
	expect((failure as ApiError).status).toBe(429);
	expect((failure as ApiError).retryAfter).toBe(17);
});

test("ignores a malformed Retry-After header", async () => {
	fetchMock.mockResolvedValue(jsonResponse({ error: "rate limited" }, 429, { "Retry-After": "soon" }));
	const failure = await getOverview().catch((e: unknown) => e);
	expect((failure as ApiError).retryAfter).toBeUndefined();
});

test("resolves void on 204", async () => {
	fetchMock.mockResolvedValue(new Response(null, { status: 204 }));
	await expect(deleteGroup(3)).resolves.toBeUndefined();
	expect(fetchMock.mock.calls[0]![0]).toBe("/api/groups/3");
	expect(fetchMock.mock.calls[0]![1]?.method).toBe("DELETE");
});

test("unwraps list envelopes", async () => {
	fetchMock.mockResolvedValue(jsonResponse({ groups: [{ id: 1, name: "default", safe_search: false }] }));
	const groups = await listGroups();
	expect(groups).toEqual([{ id: 1, name: "default", safe_search: false }]);
});

test("serializes query filters, omitting undefined", async () => {
	fetchMock.mockResolvedValue(jsonResponse({ queries: [], next_before: null }));
	await getQueries({ limit: 50, blocked: true, domain: "ads.example", before: undefined });
	expect(fetchMock.mock.calls[0]![0]).toBe("/api/queries?limit=50&blocked=true&domain=ads.example");
});

test("requests with no filters carry no query string", async () => {
	fetchMock.mockResolvedValue(jsonResponse({ queries: [], next_before: null }));
	await getQueries();
	expect(fetchMock.mock.calls[0]![0]).toBe("/api/queries");
});

test("wraps and unwraps group sources", async () => {
	fetchMock.mockResolvedValue(jsonResponse({ source_ids: [2, 5] }));
	const stored = await putGroupSources(4, [5, 2]);
	expect(stored).toEqual([2, 5]);
	expect(fetchMock.mock.calls[0]![0]).toBe("/api/groups/4/sources");
	expect(fetchMock.mock.calls[0]![1]?.body).toBe(JSON.stringify({ source_ids: [5, 2] }));
});
