/* @vitest-environment node */

import { describe, it, expect, vi, beforeEach } from "vitest";
import {
	apiFetch,
	ApiClientError,
	getFiles,
	login,
	getMe,
	search,
	changePassword,
	adminListUsers,
	adminCreateUser,
	adminUpdateUser,
	adminDeleteUser,
	adminResetUserPassword,
} from "./apiClient.js";

beforeEach(() => {
	vi.restoreAllMocks();
	global.fetch = vi.fn();
});

describe("lib/frontend/apiClient", () => {
	it("apiFetch uses credentials=include and cache=no-store", async () => {
		fetch.mockResolvedValue(
			new Response(JSON.stringify({ ok: true }), {
				status: 200,
				headers: { "Content-Type": "application/json" },
			}),
		);

		await apiFetch("/api/health");

		const [url, init] = fetch.mock.calls[0];
		expect(url).toBe("/api/health");
		expect(init.credentials).toBe("include");
		expect(init.cache).toBe("no-store");
	});

	it("apiFetch maps standardized backend errors into ApiClientError", async () => {
		fetch.mockResolvedValue(
			new Response(
				JSON.stringify({
					error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
				}),
				{ status: 401, headers: { "Content-Type": "application/json" } },
			),
		);

		await expect(apiFetch("/api/branches")).rejects.toMatchObject({
			name: "ApiClientError",
			status: 401,
			code: "AUTH_UNAUTHENTICATED",
		});
	});

	it("apiFetch maps network failures to CLIENT_NETWORK_ERROR", async () => {
		fetch.mockRejectedValue(new Error("connection refused"));

		try {
			await apiFetch("/api/branches");
			throw new Error("Expected apiFetch to throw");
		} catch (err) {
			expect(err).toBeInstanceOf(ApiClientError);
			expect(err.code).toBe("CLIENT_NETWORK_ERROR");
			expect(err.status).toBe(0);
		}
	});

	it("getFiles builds the expected query string", async () => {
		fetch.mockResolvedValue(
			new Response(JSON.stringify({ files: [] }), {
				status: 200,
				headers: { "Content-Type": "application/json" },
			}),
		);

		await getFiles("NL01", "2024", "10", "23");

		const [url] = fetch.mock.calls[0];
		expect(url).toContain("/api/files?");
		expect(url).toContain("branch=NL01");
		expect(url).toContain("year=2024");
		expect(url).toContain("month=10");
		expect(url).toContain("day=23");
	});

	it("getMe calls /api/auth/me", async () => {
		fetch.mockResolvedValue(
			new Response(JSON.stringify({ user: null }), {
				status: 200,
				headers: { "Content-Type": "application/json" },
			}),
		);

		const res = await getMe();
		expect(res).toEqual({ user: null });

		const [url, init] = fetch.mock.calls[0];
		expect(url).toBe("/api/auth/me");
		expect(init.method).toBe("GET");
	});

	it("search builds expected query string", async () => {
		fetch.mockResolvedValue(
			new Response(JSON.stringify({ items: [], nextCursor: null }), {
				status: 200,
				headers: { "Content-Type": "application/json" },
			}),
		);

		await search({ q: "x", branch: "NL01", limit: 100 });

		const [url] = fetch.mock.calls[0];
		const u = new URL(url, "http://localhost");
		expect(u.pathname).toBe("/api/search");
		expect(u.searchParams.get("q")).toBe("x");
		expect(u.searchParams.get("branch")).toBe("NL01");
		expect(u.searchParams.get("limit")).toBe("100");
	});

	it("changePassword calls POST /api/auth/change-password", async () => {
		fetch.mockResolvedValue(
			new Response(JSON.stringify({ ok: true }), {
				status: 200,
				headers: { "Content-Type": "application/json" },
			}),
		);

		await changePassword({ currentPassword: "a", newPassword: "b" });

		const [url, init] = fetch.mock.calls[0];
		expect(url).toBe("/api/auth/change-password");
		expect(init.method).toBe("POST");
	});

	it("adminUpdateUser calls PATCH /api/admin/users/:id", async () => {
		fetch.mockResolvedValue(
			new Response(JSON.stringify({ ok: true }), {
				status: 200,
				headers: { "Content-Type": "application/json" },
			}),
		);

		await adminUpdateUser("507f1f77bcf86cd799439011", { role: "admin" });

		const [url, init] = fetch.mock.calls[0];
		expect(url).toBe("/api/admin/users/507f1f77bcf86cd799439011");
		expect(init.method).toBe("PATCH");
	});

	it("adminListUsers includes sort in query string", async () => {
		fetch.mockResolvedValue(
			new Response(JSON.stringify({ items: [], nextCursor: null }), {
				status: 200,
				headers: { "Content-Type": "application/json" },
			}),
		);

		await adminListUsers({ q: "dev", sort: "role_rights", limit: 50 });

		const [url, init] = fetch.mock.calls[0];
		const u = new URL(url, "http://localhost");
		expect(u.pathname).toBe("/api/admin/users");
		expect(u.searchParams.get("q")).toBe("dev");
		expect(u.searchParams.get("sort")).toBe("role_rights");
		expect(u.searchParams.get("limit")).toBe("50");
		expect(init.method).toBe("GET");
	});

	it("adminDeleteUser calls DELETE /api/admin/users/:id", async () => {
		fetch.mockResolvedValue(
			new Response(JSON.stringify({ ok: true }), {
				status: 200,
				headers: { "Content-Type": "application/json" },
			}),
		);

		await adminDeleteUser("507f1f77bcf86cd799439099");

		const [url, init] = fetch.mock.calls[0];
		expect(url).toBe("/api/admin/users/507f1f77bcf86cd799439099");
		expect(init.method).toBe("DELETE");
	});

	it("adminResetUserPassword calls POST /api/admin/users/:id", async () => {
		fetch.mockResolvedValue(
			new Response(JSON.stringify({ ok: true, temporaryPassword: "x" }), {
				status: 200,
				headers: { "Content-Type": "application/json" },
			}),
		);

		await adminResetUserPassword("507f1f77bcf86cd799439099");

		const [url, init] = fetch.mock.calls[0];
		expect(url).toBe("/api/admin/users/507f1f77bcf86cd799439099");
		expect(init.method).toBe("POST");
	});
});
