// @vitest-environment jsdom
import { act, renderHook, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { useInjection } from "./useInjection";
import type { InjectionResponse } from "./injection";

// api 客户端是 hook 的外部副作用边界；GET 拉确定性结果，PUT 写覆盖。单测一律 mock。
const get = vi.fn();
const put = vi.fn();
vi.mock("@/lib/api/client", () => ({
  api: {
    GET: (...a: unknown[]) => get(...a),
    PUT: (...a: unknown[]) => put(...a),
  },
}));

// 最小 InjectionResponse 桩（仅覆盖纯逻辑读取的字段）。
function makeResponse(over: Partial<InjectionResponse> = {}): InjectionResponse {
  return {
    pinned: [],
    excluded: [],
    recent_n: 3,
    entities: [],
    ...over,
  } as InjectionResponse;
}

const CHAR = { kind: "character", name: "张三" } as const;

describe("useInjection", () => {
  beforeEach(() => {
    get.mockReset();
    put.mockReset();
  });
  afterEach(() => vi.clearAllMocks());

  it("挂载成功：拉到确定性结果并落到 data，loading 归位", async () => {
    const body = makeResponse({ recent_n: 5 });
    get.mockResolvedValue({ data: body, error: null });

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(result.current.data).toEqual(body);
    expect(result.current.loadError).toBeNull();
    expect(result.current.saveError).toBeNull();
  });

  it("挂载后端返回 error：给可读文案、data 置空", async () => {
    get.mockResolvedValue({ data: null, error: { detail: "boom" } });

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(result.current.loadError).toBe("注入信息暂不可用");
    expect(result.current.data).toBeNull();
  });

  it("挂载网络层抛错：捕获并给可读文案", async () => {
    get.mockRejectedValue(new Error("network down"));

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    expect(result.current.loadError).toBe("注入信息暂不可用");
    expect(result.current.data).toBeNull();
  });

  it("togglePin 成功：PUT 覆盖后以服务端确定结果回放 data", async () => {
    get.mockResolvedValue({ data: makeResponse(), error: null });
    const updated = makeResponse({ pinned: [{ ...CHAR }] });
    put.mockResolvedValue({ data: updated, error: null });

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    await act(async () => {
      await result.current.togglePin(CHAR);
    });

    expect(put).toHaveBeenCalledTimes(1);
    expect(result.current.data).toEqual(updated);
    expect(result.current.saving).toBe(false);
    expect(result.current.saveError).toBeNull();
  });

  it("exclude / restore / setRecentN 各自经 PUT 提交覆盖", async () => {
    get.mockResolvedValue({ data: makeResponse(), error: null });
    put.mockResolvedValue({ data: makeResponse(), error: null });

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    await act(async () => {
      await result.current.exclude(CHAR);
    });
    await act(async () => {
      await result.current.restore(CHAR);
    });
    await act(async () => {
      await result.current.setRecentN(8);
    });

    expect(put).toHaveBeenCalledTimes(3);
    // 最后一次 setRecentN 的 body.recent_n 已钳到合法区间。
    expect(put).toHaveBeenLastCalledWith(
      "/projects/{project_id}/chapters/{chapter_no}/injection",
      expect.objectContaining({ body: expect.objectContaining({ recent_n: 8 }) }),
    );
  });

  it("保存后端返回 error：给可读文案、本地 data 不变（天然回滚）", async () => {
    const loaded = makeResponse({ recent_n: 4 });
    get.mockResolvedValue({ data: loaded, error: null });
    put.mockResolvedValue({ data: null, error: { detail: "422" } });

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    await act(async () => {
      await result.current.togglePin(CHAR);
    });

    expect(result.current.saveError).toBe("保存注入设置失败，请重试");
    expect(result.current.data).toEqual(loaded);
  });

  it("保存网络层抛错：本地状态不动、给可读文案、不抛", async () => {
    const loaded = makeResponse();
    get.mockResolvedValue({ data: loaded, error: null });
    put.mockRejectedValue(new Error("network down"));

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    await act(async () => {
      await result.current.togglePin(CHAR);
    });

    expect(result.current.saveError).toBe("保存注入设置失败，请重试");
    expect(result.current.data).toEqual(loaded);
  });

  it("保存失败不清空 data，且 loadError 与 saveError 互不影响", async () => {
    const loaded = makeResponse({ recent_n: 4 });
    get.mockResolvedValue({ data: loaded, error: null });
    put.mockResolvedValue({ data: null, error: { detail: "422" } });

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    // 加载成功 → loadError 为空。
    expect(result.current.loadError).toBeNull();

    await act(async () => {
      await result.current.togglePin(CHAR);
    });

    // 保存失败只设 saveError；data 与 loadError 不受影响。
    expect(result.current.saveError).toBe("保存注入设置失败，请重试");
    expect(result.current.loadError).toBeNull();
    expect(result.current.data).toEqual(loaded);

    // clearSaveError 关闭悬浮提示，data 仍在。
    act(() => result.current.clearSaveError());
    expect(result.current.saveError).toBeNull();
    expect(result.current.data).toEqual(loaded);
  });

  it("retrySave 重放上一次失败的覆盖，成功后回放 data、清空 saveError", async () => {
    const loaded = makeResponse({ recent_n: 4 });
    get.mockResolvedValue({ data: loaded, error: null });
    put.mockResolvedValueOnce({ data: null, error: { detail: "422" } });
    const updated = makeResponse({ recent_n: 4, pinned: [{ ...CHAR }] });
    put.mockResolvedValueOnce({ data: updated, error: null });

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    await act(async () => {
      await result.current.togglePin(CHAR);
    });
    expect(result.current.saveError).toBe("保存注入设置失败，请重试");

    await act(async () => {
      await result.current.retrySave();
    });

    expect(put).toHaveBeenCalledTimes(2);
    expect(result.current.saveError).toBeNull();
    expect(result.current.data).toEqual(updated);
  });

  it("data 未就绪时 mutate 早返回、不发 PUT", async () => {
    get.mockResolvedValue({ data: null, error: { detail: "fail" } });

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    await act(async () => {
      await result.current.togglePin(CHAR);
    });

    expect(put).not.toHaveBeenCalled();
  });

  it("并发锁：保存在途时再次触发被拦下，只发一次 PUT", async () => {
    get.mockResolvedValue({ data: makeResponse(), error: null });
    // 第一次 PUT 挂起，制造在途窗口。
    let resolveFirst: (v: unknown) => void = () => {};
    put.mockImplementationOnce(
      () =>
        new Promise((res) => {
          resolveFirst = res;
        }),
    );

    const { result } = renderHook(() => useInjection("p1", 3));
    await waitFor(() => expect(result.current.loading).toBe(false));

    await act(async () => {
      // 不 await 第一次：保存在途时立即触发第二次，应被 savingRef 拦下。
      const first = result.current.togglePin(CHAR);
      await result.current.exclude(CHAR);
      resolveFirst({ data: makeResponse(), error: null });
      await first;
    });

    expect(put).toHaveBeenCalledTimes(1);
  });
});
