import "@testing-library/jest-dom/vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ConnectError, Code } from "@connectrpc/connect";

import { AuthError } from "../src/api/auth";
import {
	classifyError,
	isRetryable,
	type ErrorKind,
} from "../src/lib/error/classify";
import { errorMessageKey, reportError } from "../src/lib/error/report";
import { toast } from "../src/lib/toast.svelte";

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

describe("classifyError", () => {
	it("maps HTTP statuses (AuthError) to kinds", () => {
		const at = (status: number) =>
			classifyError(new AuthError("x", "msg", status));
		expect(at(401)).toBe("auth");
		expect(at(403)).toBe("forbidden");
		expect(at(404)).toBe("notFound");
		expect(at(409)).toBe("conflict");
		expect(at(429)).toBe("rateLimit");
		expect(at(500)).toBe("server");
		expect(at(400)).toBe("unknown");
	});

	it("maps Connect codes to kinds", () => {
		const at = (code: Code) => classifyError(new ConnectError("msg", code));
		expect(at(Code.Unauthenticated)).toBe("auth");
		expect(at(Code.PermissionDenied)).toBe("forbidden");
		expect(at(Code.NotFound)).toBe("notFound");
		expect(at(Code.FailedPrecondition)).toBe("conflict");
		expect(at(Code.ResourceExhausted)).toBe("rateLimit");
		expect(at(Code.Unavailable)).toBe("network");
		expect(at(Code.Internal)).toBe("server");
	});

	it("treats a fetch TypeError as a network error", () => {
		expect(classifyError(new TypeError("Failed to fetch"))).toBe("network");
	});

	it("duck-types any object with a numeric status", () => {
		expect(classifyError({ status: 409 })).toBe("conflict");
	});

	it("falls back to unknown for a plain error", () => {
		expect(classifyError(new Error("boom"))).toBe("unknown");
	});

	it("short-circuits to offline when the browser is offline", () => {
		Object.defineProperty(navigator, "onLine", {
			configurable: true,
			value: false,
		});
		try {
			expect(classifyError(new AuthError("x", "m", 500))).toBe("offline");
		} finally {
			Object.defineProperty(navigator, "onLine", {
				configurable: true,
				value: true,
			});
		}
	});

	it("marks only transient kinds retryable", () => {
		const retryable: ErrorKind[] = ["offline", "network", "server"];
		const notRetryable: ErrorKind[] = [
			"auth",
			"forbidden",
			"conflict",
			"notFound",
			"rateLimit",
			"unknown",
		];
		expect(retryable.every(isRetryable)).toBe(true);
		expect(notRetryable.some(isRetryable)).toBe(false);
	});
});

describe("reportError / errorMessageKey", () => {
	it("returns the translated message key for inline use", () => {
		expect(errorMessageKey(new AuthError("x", "m", 403))).toBe("error.forbidden");
		expect(errorMessageKey(new Error("?"))).toBe("error.unknown");
	});

	it("raises an auto-dismiss toast for a non-retryable error", () => {
		const kind = reportError(new AuthError("x", "m", 403));
		expect(kind).toBe("forbidden");
		expect(toast.current?.messageKey).toBe("error.forbidden");
		expect(toast.current?.actionLabelKey).toBeUndefined();
		expect(toast.current?.durationMs).toBe(8000);
	});

	it("raises a sticky Retry toast for a retryable error with onRetry", () => {
		const onRetry = vi.fn();
		const kind = reportError(new TypeError("Failed to fetch"), { onRetry });
		expect(kind).toBe("network");
		expect(toast.current?.messageKey).toBe("error.network");
		expect(toast.current?.actionLabelKey).toBe("common.retry");
		expect(toast.current?.durationMs).toBeNull();
		toast.current?.onAction?.();
		expect(onRetry).toHaveBeenCalledOnce();
	});

	it("does not add a Retry action to a non-retryable error", () => {
		reportError(new AuthError("x", "m", 403), { onRetry: vi.fn() });
		expect(toast.current?.actionLabelKey).toBeUndefined();
	});
});
