// Verifies the wire shape of `sendEmailCode` / `confirmEmailCode`
// against the gateway public auth REST surface defined in
// `backend/openapi.yaml`. The transport is mocked through
// `globalThis.fetch`; the helpers themselves are exercised in the
// e2e Playwright spec against a Connect-Web mock that adds back the
// real network stack (still not a live gateway).

import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { AuthError, confirmEmailCode, sendEmailCode } from "../src/api/auth";

const BASE_URL = "https://gateway.test";

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

describe("sendEmailCode", () => {
	let fetchSpy: ReturnType<typeof vi.fn>;

	beforeEach(() => {
		fetchSpy = vi.fn();
		globalThis.fetch = fetchSpy as unknown as typeof fetch;
	});

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

	test("posts a JSON body with the email and returns the challenge id", async () => {
		fetchSpy.mockResolvedValueOnce(
			jsonResponse(200, { challenge_id: "ch-123" }),
		);

		const result = await sendEmailCode(BASE_URL, "pilot@example.com");

		expect(result).toEqual({ challengeId: "ch-123" });
		expect(fetchSpy).toHaveBeenCalledTimes(1);
		const [url, init] = fetchSpy.mock.calls[0]!;
		expect(url).toBe(`${BASE_URL}/api/v1/public/auth/send-email-code`);
		expect(init?.method).toBe("POST");
		expect(init?.headers).toEqual({ "content-type": "application/json" });
		expect(JSON.parse(init?.body as string)).toEqual({
			email: "pilot@example.com",
		});
	});

	test("strips a trailing slash from the base URL", async () => {
		fetchSpy.mockResolvedValueOnce(jsonResponse(200, { challenge_id: "ch-1" }));
		await sendEmailCode(`${BASE_URL}/`, "pilot@example.com");
		expect(fetchSpy.mock.calls[0]![0]).toBe(
			`${BASE_URL}/api/v1/public/auth/send-email-code`,
		);
	});

	test("forwards the locale option in the JSON body", async () => {
		fetchSpy.mockResolvedValueOnce(jsonResponse(200, { challenge_id: "ch-1" }));
		await sendEmailCode(BASE_URL, "pilot@example.com", { locale: "ru" });
		const [, init] = fetchSpy.mock.calls[0]!;
		expect(init?.headers).toEqual({ "content-type": "application/json" });
		expect(JSON.parse(init?.body as string)).toEqual({
			email: "pilot@example.com",
			locale: "ru",
		});
	});

	test("omits the locale field when no locale is provided", async () => {
		fetchSpy.mockResolvedValueOnce(jsonResponse(200, { challenge_id: "ch-1" }));
		await sendEmailCode(BASE_URL, "pilot@example.com");
		const [, init] = fetchSpy.mock.calls[0]!;
		expect(JSON.parse(init?.body as string)).toEqual({
			email: "pilot@example.com",
		});
	});

	test("throws AuthError carrying gateway code and message on 400", async () => {
		fetchSpy.mockResolvedValueOnce(
			jsonResponse(400, {
				error: { code: "invalid_request", message: "email must be valid" },
			}),
		);

		await expect(sendEmailCode(BASE_URL, "bad")).rejects.toMatchObject({
			name: "AuthError",
			code: "invalid_request",
			message: "email must be valid",
			status: 400,
		});
	});

	test("falls back to internal_error on a 5xx without an envelope", async () => {
		fetchSpy.mockResolvedValueOnce(
			new Response("oops", { status: 503 }),
		);

		const err = await sendEmailCode(BASE_URL, "pilot@example.com").catch(
			(e: unknown) => e,
		);
		expect(err).toBeInstanceOf(AuthError);
		expect((err as AuthError).code).toBe("internal_error");
		expect((err as AuthError).status).toBe(503);
	});

	test("throws on a malformed success body", async () => {
		fetchSpy.mockResolvedValueOnce(jsonResponse(200, { challenge_id: 42 }));
		await expect(sendEmailCode(BASE_URL, "p@x")).rejects.toBeInstanceOf(
			AuthError,
		);
	});
});

describe("confirmEmailCode", () => {
	let fetchSpy: ReturnType<typeof vi.fn>;

	beforeEach(() => {
		fetchSpy = vi.fn();
		globalThis.fetch = fetchSpy as unknown as typeof fetch;
	});

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

	test("base64-encodes the public key and forwards the time zone", async () => {
		fetchSpy.mockResolvedValueOnce(
			jsonResponse(200, { device_session_id: "dev-uuid-1" }),
		);

		const publicKey = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
		const result = await confirmEmailCode(BASE_URL, {
			challengeId: "ch-1",
			code: "123456",
			publicKey,
			timeZone: "Europe/Berlin",
		});

		expect(result).toEqual({ deviceSessionId: "dev-uuid-1" });
		const [url, init] = fetchSpy.mock.calls[0]!;
		expect(url).toBe(`${BASE_URL}/api/v1/public/auth/confirm-email-code`);
		const body = JSON.parse(init?.body as string) as Record<string, unknown>;
		expect(body.challenge_id).toBe("ch-1");
		expect(body.code).toBe("123456");
		expect(body.time_zone).toBe("Europe/Berlin");
		expect(body.client_public_key).toBe(btoa("\xde\xad\xbe\xef"));
	});

	test("AuthError on 400 carries the gateway error code", async () => {
		fetchSpy.mockResolvedValueOnce(
			jsonResponse(400, {
				error: { code: "invalid_request", message: "bad code" },
			}),
		);

		await expect(
			confirmEmailCode(BASE_URL, {
				challengeId: "ch",
				code: "wrong",
				publicKey: new Uint8Array(32),
				timeZone: "UTC",
			}),
		).rejects.toMatchObject({ code: "invalid_request", status: 400 });
	});
});
