import { describe, it, expect, vi, beforeEach } from "vitest";
import { APICallError } from "ai";

const { mockCheckAndIncrement, mockRefund } = vi.hoisted(() => ({
  mockCheckAndIncrement: vi.fn(),
  mockRefund: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("@/lib/tiers", async () => {
  const actual = await vi.importActual<typeof import("@/lib/tiers")>("@/lib/tiers");
  return {
    ...actual,
    checkAndIncrementTierLimit: mockCheckAndIncrement,
    refundAiCall: mockRefund,
  };
});

const { aiErrorResponse, withAiQuota } = await import("../ai-error");
const { TierLimitError } = await import("@/lib/tiers");

beforeEach(() => {
  vi.clearAllMocks();
  mockCheckAndIncrement.mockResolvedValue(undefined);
});

function makeApiCallError(isRetryable: boolean) {
  return new APICallError({
    message: "Provider returned error",
    url: "https://openrouter.ai/api/v1/responses",
    requestBodyValues: {},
    statusCode: isRetryable ? 503 : 400,
    isRetryable,
  });
}

describe("aiErrorResponse", () => {
  it("maps a retryable APICallError to a 503 with a friendly message", async () => {
    const res = aiErrorResponse(makeApiCallError(true));
    expect(res.status).toBe(503);
    const body = await res.json() as { error: string; retryable: boolean };
    expect(body.retryable).toBe(true);
    expect(body.error).not.toMatch(/openrouter|nvidia|nemotron/i);
  });

  it("maps a non-retryable APICallError to a 502", async () => {
    const res = aiErrorResponse(makeApiCallError(false));
    expect(res.status).toBe(502);
    const body = await res.json() as { retryable: boolean };
    expect(body.retryable).toBe(false);
  });

  it("maps an unknown error to a generic 502", async () => {
    const res = aiErrorResponse(new Error("boom"));
    expect(res.status).toBe(502);
  });
});

describe("withAiQuota", () => {
  it("returns 403 when the tier limit is already reached, without calling fn", async () => {
    mockCheckAndIncrement.mockRejectedValue(new TierLimitError("aiCall", "free"));
    const fn = vi.fn();
    const result = await withAiQuota("user-1", "free", fn);
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.response.status).toBe(403);
    expect(fn).not.toHaveBeenCalled();
  });

  it("refunds the charged credit when fn throws", async () => {
    const fn = vi.fn().mockRejectedValue(makeApiCallError(true));
    const result = await withAiQuota("user-1", "free", fn);
    expect(result.ok).toBe(false);
    expect(mockRefund).toHaveBeenCalledWith("user-1");
    if (!result.ok) expect(result.response.status).toBe(503);
  });

  it("returns fn's result and does not refund on success", async () => {
    const fn = vi.fn().mockResolvedValue({ title: "Pasta" });
    const result = await withAiQuota("user-1", "free", fn);
    expect(result.ok).toBe(true);
    if (result.ok) expect(result.data).toEqual({ title: "Pasta" });
    expect(mockRefund).not.toHaveBeenCalled();
  });
});
