import { describe, it, expect, vi, beforeEach } from "vitest";
import { requestJson, ApiRequestError } from "./fetcher";

const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

beforeEach(() => {
  vi.clearAllMocks();
});

describe("requestJson", () => {
  it("returns parsed JSON payload on success", async () => {
    mockFetch.mockResolvedValueOnce(
      new Response(JSON.stringify({ ok: true }), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      }),
    );

    const data = await requestJson<{ ok: boolean }>("/api/demo");

    expect(data).toEqual({ ok: true });
  });

  it("serializes body and injects application/json header by default", async () => {
    mockFetch.mockResolvedValueOnce(
      new Response(JSON.stringify({ done: true }), { status: 200 }),
    );

    await requestJson("/api/demo", {
      method: "POST",
      body: { name: "alice" },
    });

    const init = mockFetch.mock.calls[0][1] as RequestInit;
    expect(init.body).toBe(JSON.stringify({ name: "alice" }));
    expect(new Headers(init.headers).get("Content-Type")).toBe("application/json");
  });

  it("keeps caller provided Content-Type header", async () => {
    mockFetch.mockResolvedValueOnce(
      new Response(JSON.stringify({ done: true }), { status: 200 }),
    );

    await requestJson("/api/demo", {
      method: "POST",
      headers: { "Content-Type": "text/plain" },
      body: { raw: true },
    });

    const init = mockFetch.mock.calls[0][1] as RequestInit;
    expect(new Headers(init.headers).get("Content-Type")).toBe("text/plain");
  });

  it("returns undefined for 204 responses", async () => {
    mockFetch.mockResolvedValueOnce(new Response(null, { status: 204 }));

    const data = await requestJson("/api/no-content");

    expect(data).toBeUndefined();
  });

  it("throws ApiRequestError with JSON error payload", async () => {
    mockFetch.mockResolvedValueOnce(
      new Response(JSON.stringify({ error: "参数错误" }), {
        status: 400,
        headers: { "Content-Type": "application/json" },
      }),
    );

    const promise = requestJson("/api/fail");
    await expect(promise).rejects.toBeInstanceOf(ApiRequestError);
    await expect(promise).rejects.toMatchObject({
      message: "参数错误",
      status: 400,
      payload: { error: "参数错误" },
    });
  });

  it("falls back to plain text message on failed responses", async () => {
    mockFetch.mockResolvedValueOnce(new Response("服务不可用", { status: 503 }));

    await expect(requestJson("/api/fail")).rejects.toMatchObject({
      message: "服务不可用",
      status: 503,
    });
  });

  it("parses payload when response only provides json()", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValue({ fromJsonOnly: true }),
    } as unknown as Response);

    const data = await requestJson<{ fromJsonOnly: boolean }>("/api/mock-only-json");

    expect(data).toEqual({ fromJsonOnly: true });
  });
});
