/**
 * API Client (fetchWithAuth) 单元测试
 *
 * 覆盖：带 token 请求、错误处理（401/500/204）
 */

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { fetchWithAuth, API_BASE, getApiUrl } from "@/lib/api/client";

// ── Mock next-auth/react ──────────────────────────────────────────────────────

vi.mock("next-auth/react", () => ({
  getSession: vi.fn(() =>
    Promise.resolve({ accessToken: "session-token-123" })
  ),
}));

import { getSession } from "next-auth/react";
const mockGetSession = vi.mocked(getSession);

// ── Mock global fetch ─────────────────────────────────────────────────────────

const mockFetch = vi.fn();
const originalFetch = global.fetch;

beforeEach(() => {
  global.fetch = mockFetch;
  vi.clearAllMocks();
});

afterEach(() => {
  global.fetch = originalFetch;
});

// ── 辅助 ──────────────────────────────────────────────────────────────────────

function mockFetchResponse(options: {
  ok: boolean;
  status: number;
  json?: () => Promise<unknown>;
}) {
  return {
    ok: options.ok,
    status: options.status,
    json: options.json ?? (() => Promise.resolve({})),
  };
}

// ── getApiUrl ─────────────────────────────────────────────────────────────────

describe("getApiUrl", () => {
  it("应拼接 API_BASE 和路径", () => {
    const url = getApiUrl("/api/v1/brands");
    expect(url).toBe(`${API_BASE}/api/v1/brands`);
  });
});

// ── fetchWithAuth ─────────────────────────────────────────────────────────────

describe("fetchWithAuth", () => {
  it("显式传入 token 时应添加 Authorization 头", async () => {
    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ data: "ok" }) })
    );

    await fetchWithAuth("/api/test", {}, "my-secret-token");

    const [url, options] = mockFetch.mock.calls[0];
    expect(url).toBe(`${API_BASE}/api/test`);
    expect(options.headers.Authorization).toBe("Bearer my-secret-token");
    expect(options.headers["Content-Type"]).toBe("application/json");
  });

  it("未传入 token 且在浏览器环境下应从 session 获取", async () => {
    // 模拟浏览器环境
    const originalWindow = global.window;
    Object.defineProperty(global, "window", {
      value: { location: {} },
      writable: true,
    });

    mockGetSession.mockResolvedValueOnce({
      accessToken: "session-token-123",
    });

    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ data: "ok" }) })
    );

    await fetchWithAuth("/api/test", {});

    const options = mockFetch.mock.calls[0][1];
    expect(options.headers.Authorization).toBe("Bearer session-token-123");

    Object.defineProperty(global, "window", {
      value: originalWindow,
      writable: true,
    });
  });

  it("session 无 accessToken 时不应添加 Authorization 头", async () => {
    // jsdom 环境下 typeof window !== "undefined"，所以会走 session 路径
    mockGetSession.mockResolvedValueOnce({}); // session 没有 accessToken

    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ data: "ok" }) })
    );

    await fetchWithAuth("/api/test", {});

    const options = mockFetch.mock.calls[0][1];
    expect(options.headers.Authorization).toBeUndefined();
  });

  it("session 获取失败时不应添加 Authorization 头", async () => {
    mockGetSession.mockRejectedValueOnce(new Error("session error"));

    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ data: "ok" }) })
    );

    await fetchWithAuth("/api/test", {});

    const options = mockFetch.mock.calls[0][1];
    expect(options.headers.Authorization).toBeUndefined();
  });

  it("应合并自定义 headers", async () => {
    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({}) })
    );

    await fetchWithAuth("/api/test", {
      headers: { "X-Custom": "value" },
    }, "token");

    const options = mockFetch.mock.calls[0][1];
    expect(options.headers["Content-Type"]).toBe("application/json");
    expect(options.headers["X-Custom"]).toBe("value");
    expect(options.headers.Authorization).toBe("Bearer token");
  });

  // ── 401 错误 ───────────────────────────────────────────────────────────

  it("401 应抛出'登录已过期'错误", async () => {
    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({ ok: false, status: 401 })
    );

    await expect(fetchWithAuth("/api/test", {}, "token")).rejects.toThrow(
      "登录已过期，请重新登录"
    );
  });

  // ── 500 错误 ───────────────────────────────────────────────────────────

  it("500 错误应抛出包含状态码的错误", async () => {
    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({
        ok: false,
        status: 500,
        json: () => Promise.resolve({ detail: "内部服务器错误" }),
      })
    );

    await expect(fetchWithAuth("/api/test", {}, "token")).rejects.toThrow(
      "内部服务器错误"
    );
  });

  it("500 错误且 JSON 解析失败应使用默认错误信息", async () => {
    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({
        ok: false,
        status: 500,
        json: () => Promise.reject(new Error("invalid json")),
      })
    );

    await expect(fetchWithAuth("/api/test", {}, "token")).rejects.toThrow(
      "请求失败 (HTTP 500)"
    );
  });

  it("其他错误状态码应正确抛出", async () => {
    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({
        ok: false,
        status: 422,
        json: () => Promise.resolve({ detail: "参数校验失败" }),
      })
    );

    await expect(fetchWithAuth("/api/test", {}, "token")).rejects.toThrow(
      "参数校验失败"
    );
  });

  // ── 成功响应 ────────────────────────────────────────────────────────────

  it("200 应返回 JSON 数据", async () => {
    const mockData = { id: "1", name: "品牌" };
    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({
        ok: true,
        status: 200,
        json: () => Promise.resolve(mockData),
      })
    );

    const result = await fetchWithAuth("/api/test", {}, "token");
    expect(result).toEqual(mockData);
  });

  it("204 应返回 null", async () => {
    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({ ok: true, status: 204 })
    );

    const result = await fetchWithAuth("/api/test", {}, "token");
    expect(result).toBeNull();
  });

  // ── 错误响应的 message 字段 ────────────────────────────────────────────

  it("错误响应包含 message 字段时应优先使用", async () => {
    mockFetch.mockResolvedValueOnce(
      mockFetchResponse({
        ok: false,
        status: 400,
        json: () => Promise.resolve({ message: "品牌名称已存在" }),
      })
    );

    await expect(fetchWithAuth("/api/test", {}, "token")).rejects.toThrow(
      "品牌名称已存在"
    );
  });
});
