import { describe, expect, it, beforeEach, vi } from "vitest";
import type { Mock } from "vitest";
import { ApiError, get, post, put, del, isAuthError, setUnauthorizedHandler } from "../client";

describe("api/client", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
    setUnauthorizedHandler(null);
  });

  // =========================================================================
  // Authorization tests
  // =========================================================================

  it("invokes unauthorized handler for 401 responses", async () => {
    const handler = vi.fn();
    setUnauthorizedHandler(handler);

    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      text: vi.fn().mockResolvedValue("Unauthorized"),
    });

    await expect(get("/test")).rejects.toBeInstanceOf(ApiError);
    expect(handler).toHaveBeenCalledTimes(1);
  });

  it("invokes unauthorized handler for 403 responses", async () => {
    const handler = vi.fn();
    setUnauthorizedHandler(handler);

    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 403,
      text: vi.fn().mockResolvedValue("Forbidden"),
    });

    await expect(get("/test")).rejects.toBeInstanceOf(ApiError);
    expect(handler).toHaveBeenCalledTimes(1);
  });

  it("does not invoke handler when handler is null", async () => {
    const handler = vi.fn();
    setUnauthorizedHandler(null);

    global.fetch = vi.fn().mockResolvedValue({
      ok: false,
      status: 401,
      text: vi.fn().mockResolvedValue("Unauthorized"),
    });

    await expect(get("/test")).rejects.toBeInstanceOf(ApiError);
    expect(handler).not.toHaveBeenCalled();
  });

  it("does not treat non-auth errors as auth errors", () => {
    const error = new ApiError(500, "Server error");
    expect(isAuthError(error)).toBe(false);
  });

  it("recognizes 401 and 403 ApiError as auth errors", () => {
    expect(isAuthError(new ApiError(401, "Unauthorized"))).toBe(true);
    expect(isAuthError(new ApiError(403, "Forbidden"))).toBe(true);
  });

  // =========================================================================
  // Header tests: Content-Type
  // =========================================================================

  it("does not set Content-Type header for GET requests", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValue({ data: "test" }),
    });

    await get("/test");

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    const headers = options?.headers as Record<string, string>;
    expect(headers?.["Content-Type"]).toBeUndefined();
  });

  it("sets Content-Type header for POST requests with body", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValue({ data: "test" }),
    });

    await post("/test", { key: "value" });

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    const headers = options?.headers as Record<string, string>;
    expect(headers?.["Content-Type"]).toBe("application/json");
  });

  it("sets Content-Type header for PUT requests with body", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValue({ data: "test" }),
    });

    await put("/test", { key: "value" });

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    const headers = options?.headers as Record<string, string>;
    expect(headers?.["Content-Type"]).toBe("application/json");
  });

  it("sets Content-Type header for DELETE requests with body", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 204,
    });

    await del("/test", { key: "value" });

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    const headers = options?.headers as Record<string, string>;
    expect(headers?.["Content-Type"]).toBe("application/json");
  });

  it("does not set Content-Type header for DELETE requests without body", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 204,
    });

    await del("/test");

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    const headers = options?.headers as Record<string, string>;
    expect(headers?.["Content-Type"]).toBeUndefined();
  });

  // =========================================================================
  // Header tests: X-BanGUI-Request (CSRF)
  // =========================================================================

  it("does not set X-BanGUI-Request header for GET requests", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValue({ data: "test" }),
    });

    await get("/test");

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    const headers = options?.headers as Record<string, string>;
    expect(headers?.["X-BanGUI-Request"]).toBeUndefined();
  });

  it("sets X-BanGUI-Request header for POST requests", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValue({ data: "test" }),
    });

    await post("/test", { key: "value" });

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    const headers = options?.headers as Record<string, string>;
    expect(headers?.["X-BanGUI-Request"]).toBe("1");
  });

  it("sets X-BanGUI-Request header for PUT requests", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValue({ data: "test" }),
    });

    await put("/test", { key: "value" });

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    const headers = options?.headers as Record<string, string>;
    expect(headers?.["X-BanGUI-Request"]).toBe("1");
  });

  it("sets X-BanGUI-Request header for DELETE requests", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 204,
    });

    await del("/test");

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    const headers = options?.headers as Record<string, string>;
    expect(headers?.["X-BanGUI-Request"]).toBe("1");
  });

  it("includes credentials on all requests", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValue({ data: "test" }),
    });

    await get("/test");

    const fetchMock = global.fetch as Mock;
    const options = fetchMock.mock.calls[0]?.[1] as Record<string, unknown>;
    expect(options?.credentials).toBe("include");
  });
});
