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

import { useForeshadow } from "./useForeshadow";
import type { ForeshadowView } from "@/lib/api/types";

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

function makeRow(code: string, status = "OPEN"): ForeshadowView {
  return { code, title: code, status } as ForeshadowView;
}

const registerInput = { projectId: "p1", code: "FS-01", title: "线索" };

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

  it("初始 items 取自传入、busy=false", () => {
    const initial = [makeRow("FS-01")];
    const { result } = renderHook(() => useForeshadow(initial));
    expect(result.current.items).toEqual(initial);
    expect(result.current.busy).toBe(false);
  });

  it("登记成功：追加返回行并弹成功 toast", async () => {
    const row = makeRow("FS-02");
    post.mockResolvedValue({ data: row, error: null });

    const { result } = renderHook(() => useForeshadow([makeRow("FS-01")]));
    let ok;
    await act(async () => {
      ok = await result.current.register(registerInput);
    });

    expect(ok).toBe(true);
    expect(result.current.items).toHaveLength(2);
    expect(result.current.items[1]).toEqual(row);
    expect(result.current.busy).toBe(false);
    expect(toast).toHaveBeenCalledWith("已登记伏笔 FS-02", "success");
  });

  it("登记失败（422 duplicate）：不改 items 并弹 reason toast", async () => {
    post.mockResolvedValue({
      data: null,
      error: { error: { details: { reason: "duplicate" } } },
    });

    const initial = [makeRow("FS-01")];
    const { result } = renderHook(() => useForeshadow(initial));
    let ok;
    await act(async () => {
      ok = await result.current.register(registerInput);
    });

    expect(ok).toBe(false);
    expect(result.current.items).toEqual(initial);
    expect(toast).toHaveBeenCalledWith("该伏笔代号已存在，请换一个。", "error");
  });

  it("转移成功：乐观改 status 后用服务端权威行替换", async () => {
    const server = makeRow("FS-01", "CLOSED");
    patch.mockResolvedValue({ data: server, error: null });

    const { result } = renderHook(() => useForeshadow([makeRow("FS-01")]));
    let ok;
    await act(async () => {
      ok = await result.current.transition("FS-01", {
        projectId: "p1",
        toStatus: "CLOSED",
      });
    });

    expect(ok).toBe(true);
    expect(result.current.items[0]).toEqual(server);
    expect(toast).toHaveBeenCalledWith("已更新 FS-01", "success");
  });

  it("转移失败：回滚到旧 items 并弹 reason toast", async () => {
    patch.mockResolvedValue({
      data: null,
      error: { error: { details: { reason: "invalid_transition" } } },
    });

    const initial = [makeRow("FS-01", "PARTIAL")];
    const { result } = renderHook(() => useForeshadow(initial));
    let ok;
    await act(async () => {
      ok = await result.current.transition("FS-01", {
        projectId: "p1",
        toStatus: "CLOSED",
      });
    });

    expect(ok).toBe(false);
    expect(result.current.items).toEqual(initial);
    expect(toast).toHaveBeenCalledWith(
      "非法状态转移（CLOSED 为终态，不可再改）。",
      "error",
    );
  });

  it("转移无 toStatus 时不乐观改、仅按服务端结果替换", async () => {
    const server = makeRow("FS-01", "OPEN");
    patch.mockResolvedValue({ data: server, error: null });

    const { result } = renderHook(() => useForeshadow([makeRow("FS-01")]));
    let ok;
    await act(async () => {
      ok = await result.current.transition("FS-01", {
        projectId: "p1",
        progressEntry: { chapter: 5, note: "推进" },
      });
    });

    expect(ok).toBe(true);
    expect(result.current.items[0]).toEqual(server);
  });
});
