/**
 * apiFetch の成功時・失敗時の挙動。
 *
 * 画面はここが投げる `ApiFetchError` の `serverMessage` を見て
 * 「サーバー文言を出す / 画面既定文言を出す」を切り替えるため、
 * その境界（ApiError として解釈できたか）をテストで固定する。
 */
import { afterEach, describe, expect, test, vi } from "vitest";
import { apiFetch, ApiFetchError } from "@/lib/api/client";
import type { ApiError, CleanLogItem, ListResponse } from "@/lib/types";

/** fetch の戻りを差し替える。json() の失敗も再現できるようにしてある */
function mockFetch(response: {
  ok?: boolean;
  status?: number;
  json: () => unknown;
}): ReturnType<typeof vi.fn> {
  const status = response.status ?? 200;
  const fn = vi.fn(async () => ({
    ok: response.ok ?? (status >= 200 && status < 300),
    status,
    json: async () => response.json(),
  }));
  vi.stubGlobal("fetch", fn);
  return fn;
}

afterEach(() => {
  vi.unstubAllGlobals();
  vi.restoreAllMocks();
});

describe("apiFetch 成功時", () => {
  test("パース済みの本文を T として返す", async () => {
    const body: ListResponse<CleanLogItem> = {
      items: [],
      meta: { total: 3, page: 1, per_page: 50 },
    };
    mockFetch({ status: 200, json: () => body });

    const result = await apiFetch<ListResponse<CleanLogItem>>(
      "/api/clean-logs?date_from=2026-08-01"
    );
    expect(result.meta.total).toBe(3);
    expect(result.items).toEqual([]);
  });

  test("相対パスと init をそのまま fetch へ渡す", async () => {
    const fn = mockFetch({ status: 200, json: () => ({ ok: true }) });
    const init: RequestInit = { method: "PATCH", body: '{"rule":"R1"}' };

    await apiFetch("/api/settings/advice", init);

    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn.mock.calls[0][0]).toBe("/api/settings/advice");
    expect(fn.mock.calls[0][1]).toBe(init);
  });

  test("成功でも本文が JSON でなければ ApiFetchError（serverMessage は null）", async () => {
    mockFetch({
      status: 200,
      json: () => {
        throw new SyntaxError("Unexpected token <");
      },
    });

    const error = await apiFetch("/api/clean-logs").catch((e: unknown) => e);
    expect(error).toBeInstanceOf(ApiFetchError);
    expect((error as ApiFetchError).status).toBe(200);
    expect((error as ApiFetchError).serverMessage).toBeNull();
  });
});

describe("apiFetch 失敗時（ApiError のパース）", () => {
  test("ApiError を解釈して code / message / status を持つ", async () => {
    const body: ApiError = {
      error: {
        code: "validation_failed",
        message: "date_from〜date_toは731日以内で指定してください",
        details: { date_from: ["範囲が広すぎます"] },
      },
    };
    mockFetch({ status: 422, json: () => body });

    const error = (await apiFetch("/api/clean-logs").catch(
      (e: unknown) => e
    )) as ApiFetchError;

    expect(error).toBeInstanceOf(ApiFetchError);
    expect(error.status).toBe(422);
    expect(error.code).toBe("validation_failed");
    expect(error.serverMessage).toBe(
      "date_from〜date_toは731日以内で指定してください"
    );
    expect(error.details).toEqual({ date_from: ["範囲が広すぎます"] });
  });

  // ★元は 501 / `not_implemented` のケースだった★（エリア別完了率の仮実装 D-034）。
  //   ★D-119 で本実装になり、501 を返す実装は0件になった★ため、
  //   ★汎用の 5xx に置き換えてある（指示書AE 段階3-1 ③）★。
  //   ★テスト自体は消さないこと★ — 500 以外の 5xx でも `serverMessage` が取れることの検証は今も有効。
  test("500 以外の 5xx でも定型メッセージが serverMessage で取れる", async () => {
    const body: ApiError = {
      error: {
        code: "internal_error",
        message: "サーバーエラーが発生しました",
        details: null,
      },
    };
    mockFetch({ status: 503, json: () => body });

    const error = (await apiFetch("/api/clean-logs/1/areas").catch(
      (e: unknown) => e
    )) as ApiFetchError;

    expect(error.status).toBe(503);
    expect(error.serverMessage).toBe("サーバーエラーが発生しました");
  });

  test("ApiError の形をしていない本文は serverMessage / code が null", async () => {
    mockFetch({ status: 500, json: () => ({ message: "oops" }) });

    const error = (await apiFetch("/api/clean-logs").catch(
      (e: unknown) => e
    )) as ApiFetchError;

    expect(error.status).toBe(500);
    expect(error.code).toBeNull();
    expect(error.serverMessage).toBeNull();
  });

  test("HTMLのエラーページ（JSON不正）でも serverMessage は null", async () => {
    mockFetch({
      status: 502,
      json: () => {
        throw new SyntaxError("Unexpected token <");
      },
    });

    const error = (await apiFetch("/api/clean-logs").catch(
      (e: unknown) => e
    )) as ApiFetchError;

    expect(error.status).toBe(502);
    expect(error.serverMessage).toBeNull();
    // 画面に "<!DOCTYPE html>..." を出さないための境界
    expect(error.message).not.toContain("<");
  });

  test("code が無くても message は採用する（移行前の挙動を維持）", async () => {
    mockFetch({ status: 500, json: () => ({ error: { message: "保存に失敗" } }) });

    const error = (await apiFetch("/api/settings/advice").catch(
      (e: unknown) => e
    )) as ApiFetchError;

    expect(error.serverMessage).toBe("保存に失敗");
    expect(error.code).toBeNull();
  });

  test("error.message が文字列でなければ解釈しない", async () => {
    mockFetch({
      status: 500,
      json: () => ({ error: { code: "internal_error", message: 123 } }),
    });

    const error = (await apiFetch("/api/clean-logs").catch(
      (e: unknown) => e
    )) as ApiFetchError;

    expect(error.serverMessage).toBeNull();
    expect(error.code).toBeNull();
  });

  test("ネットワーク到達不能は status 0 で包み、原因を cause に残す", async () => {
    const cause = new TypeError("Failed to fetch");
    vi.stubGlobal(
      "fetch",
      vi.fn(async () => {
        throw cause;
      })
    );

    const error = (await apiFetch("/api/clean-logs").catch(
      (e: unknown) => e
    )) as ApiFetchError;

    expect(error).toBeInstanceOf(ApiFetchError);
    expect(error.status).toBe(0);
    expect(error.serverMessage).toBeNull();
    expect(error.cause).toBe(cause);
  });
});

describe("apiFetch は自アプリ専用", () => {
  test("絶対URLは受け付けない", async () => {
    const fn = mockFetch({ status: 200, json: () => ({}) });

    await expect(apiFetch("https://example.com/api")).rejects.toThrow(
      TypeError
    );
    await expect(apiFetch("//example.com/api")).rejects.toThrow(TypeError);
    expect(fn).not.toHaveBeenCalled();
  });
});
