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

import {
  DRAFT_FIRST_CHAPTER,
  DRAFT_PLACEHOLDER_TITLE,
  useDeferredDraft,
} from "./useDeferredDraft";

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

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

  it("初始为 idle", () => {
    const { result } = renderHook(() => useDeferredDraft());
    expect(result.current.status).toBe("idle");
  });

  it("首次落库：懒建占位项目 + 用已敲入正文种首章草稿，返回新项目 id", async () => {
    post.mockResolvedValue({ data: { id: "p1" }, error: null });
    put.mockResolvedValue({ data: {}, error: null });

    const { result } = renderHook(() => useDeferredDraft());
    let id: string | null = null;
    await act(async () => {
      id = await result.current.ensureCreated("第一段正文");
    });

    expect(id).toBe("p1");
    expect(result.current.status).toBe("created");
    expect(post).toHaveBeenCalledTimes(1);
    expect(post).toHaveBeenCalledWith("/projects", {
      body: { title: DRAFT_PLACEHOLDER_TITLE },
    });
    expect(put).toHaveBeenCalledWith(
      "/projects/{project_id}/chapters/{chapter_no}/draft",
      {
        params: { path: { project_id: "p1", chapter_no: DRAFT_FIRST_CHAPTER } },
        body: { text: "第一段正文" },
      },
    );
    expect(toast).not.toHaveBeenCalled();
  });

  it("幂等：已建后再次调用复用同一 id，绝不重复 POST", async () => {
    post.mockResolvedValue({ data: { id: "p1" }, error: null });
    put.mockResolvedValue({ data: {}, error: null });

    const { result } = renderHook(() => useDeferredDraft());
    await act(async () => {
      await result.current.ensureCreated("正文一");
    });
    let second: string | null = null;
    await act(async () => {
      second = await result.current.ensureCreated("正文二");
    });

    expect(second).toBe("p1");
    expect(post).toHaveBeenCalledTimes(1);
  });

  it("正文为空白：仍懒建项目但跳过草稿落库", async () => {
    post.mockResolvedValue({ data: { id: "p1" }, error: null });

    const { result } = renderHook(() => useDeferredDraft());
    await act(async () => {
      await result.current.ensureCreated("   ");
    });

    expect(post).toHaveBeenCalledTimes(1);
    expect(put).not.toHaveBeenCalled();
  });

  it("创建失败：status=error、弹错误 toast、返回 null、不落草稿", async () => {
    post.mockResolvedValue({ data: null, error: { detail: "配额不足" } });

    const { result } = renderHook(() => useDeferredDraft());
    let id: string | null = "sentinel";
    await act(async () => {
      id = await result.current.ensureCreated("正文");
    });

    expect(id).toBeNull();
    expect(result.current.status).toBe("error");
    expect(put).not.toHaveBeenCalled();
    expect(toast).toHaveBeenCalledWith(expect.any(String), "error");
  });

  it("草稿落库失败为非致命：项目已建仍返回 id，提示但不阻断", async () => {
    post.mockResolvedValue({ data: { id: "p1" }, error: null });
    put.mockResolvedValue({ data: null, error: { detail: "写库失败" } });

    const { result } = renderHook(() => useDeferredDraft());
    let id: string | null = null;
    await act(async () => {
      id = await result.current.ensureCreated("正文");
    });

    expect(id).toBe("p1");
    expect(result.current.status).toBe("created");
    expect(toast).toHaveBeenCalledWith(expect.any(String), "info");
  });

  it("请求抛异常：status=error、弹网络异常 toast、返回 null", async () => {
    post.mockRejectedValue(new Error("network down"));

    const { result } = renderHook(() => useDeferredDraft());
    let id: string | null = "sentinel";
    await act(async () => {
      id = await result.current.ensureCreated("正文");
    });

    expect(id).toBeNull();
    expect(result.current.status).toBe("error");
    expect(toast).toHaveBeenCalledWith(expect.any(String), "error");
  });
});
