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

import { useAccept } from "./useAccept";
import type { DecisionDraft } from "./decisions";

// 后端客户端与 Toast 是 hook 的外部副作用边界，单测一律 mock。
const post = vi.fn();
const toast = vi.fn();
vi.mock("@/lib/api/client", () => ({ api: { POST: (...a: unknown[]) => post(...a) } }));
vi.mock("@/components/Toast", () => ({ useToast: () => toast }));

const drafts: readonly DecisionDraft[] = [{ verdict: "accept", note: "" }];

describe("useAccept", () => {
  beforeEach(() => {
    post.mockReset();
    toast.mockReset();
  });
  afterEach(() => vi.clearAllMocks());

  it("初始为 idle、无结果", () => {
    const { result } = renderHook(() => useAccept());
    expect(result.current.status).toBe("idle");
    expect(result.current.result).toBeNull();
  });

  it("验收成功：status=accepted 并回填结果且弹成功 toast", async () => {
    const data = { updated: ["digest"] };
    post.mockResolvedValue({ data, error: null });

    const { result } = renderHook(() => useAccept());
    let outcome;
    await act(async () => {
      outcome = await result.current.accept("p1", 3, "正文", drafts);
    });

    expect(result.current.status).toBe("accepted");
    expect(result.current.result).toEqual(data);
    expect(outcome).toEqual({
      result: data,
      missingIndices: [],
      conflictUnresolved: false,
    });
    expect(toast).toHaveBeenCalledWith("本章已验收", "success");
  });

  it("409 CONFLICT_UNRESOLVED：解析缺判下标且不弹 toast", async () => {
    post.mockResolvedValue({
      data: null,
      error: {
        error: {
          code: "CONFLICT_UNRESOLVED",
          details: { missing_conflict_indices: [0, 2] },
        },
      },
    });

    const { result } = renderHook(() => useAccept());
    let outcome;
    await act(async () => {
      outcome = await result.current.accept("p1", 3, "正文", drafts);
    });

    expect(result.current.status).toBe("error");
    expect(outcome).toEqual({
      result: null,
      missingIndices: [0, 2],
      conflictUnresolved: true,
    });
    expect(toast).not.toHaveBeenCalled();
  });

  it("CONFLICT_UNRESOLVED 缺 details 时缺判下标回退为空数组", async () => {
    post.mockResolvedValue({
      data: null,
      error: { error: { code: "CONFLICT_UNRESOLVED" } },
    });

    const { result } = renderHook(() => useAccept());
    let outcome;
    await act(async () => {
      outcome = await result.current.accept("p1", 3, "正文", drafts);
    });

    expect(outcome).toEqual({
      result: null,
      missingIndices: [],
      conflictUnresolved: true,
    });
  });

  it("其它错误：status=error 并弹失败 toast（正文未丢）", async () => {
    post.mockResolvedValue({
      data: null,
      error: { error: { code: "INTERNAL" } },
    });

    const { result } = renderHook(() => useAccept());
    let outcome;
    await act(async () => {
      outcome = await result.current.accept("p1", 3, "正文", drafts);
    });

    expect(result.current.status).toBe("error");
    expect(outcome).toEqual({
      result: null,
      missingIndices: [],
      conflictUnresolved: false,
    });
    expect(toast).toHaveBeenCalledWith("验收失败，请重试（正文未丢失）", "error");
  });

  it("error 为空但 data 也为空时走通用失败分支", async () => {
    post.mockResolvedValue({ data: null, error: undefined });

    const { result } = renderHook(() => useAccept());
    let outcome;
    await act(async () => {
      outcome = await result.current.accept("p1", 3, "正文", drafts);
    });

    expect(result.current.status).toBe("error");
    expect(outcome).toEqual({
      result: null,
      missingIndices: [],
      conflictUnresolved: false,
    });
    expect(toast).toHaveBeenCalledWith("验收失败，请重试（正文未丢失）", "error");
  });
});
