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

import { useAiConversation } from "./useAiConversation";

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

const LIST_PATH = "/projects/{project_id}/ai-messages";

function serverRow(overrides: Record<string, unknown> = {}) {
  return {
    id: overrides.id ?? crypto.randomUUID(),
    project_id: "p1",
    chapter_no: 1,
    thread_id: "t1",
    seq: 0,
    kind: "refine",
    tool_key: null,
    role: "ai",
    content: "server",
    meta: {},
    created_at: "2026-07-09T00:00:00Z",
    ...overrides,
  };
}

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

  it("初始 idle，未触发前不发 GET", () => {
    const { result } = renderHook(() => useAiConversation("p1", 1));
    expect(get).not.toHaveBeenCalled();
    expect(result.current.status).toBe("idle");
    expect(result.current.messages).toEqual([]);
  });

  it("ensureLoaded 懒加载一次，缓存不重复拉取", async () => {
    get.mockResolvedValue({ data: { messages: [serverRow()] }, error: null });
    const { result } = renderHook(() => useAiConversation("p1", 1));
    act(() => result.current.ensureLoaded());
    await waitFor(() => expect(result.current.status).toBe("ready"));
    expect(result.current.messages).toHaveLength(1);
    act(() => result.current.ensureLoaded());
    expect(get).toHaveBeenCalledTimes(1);
  });

  it("GET 传 chapter_no 过滤（本章 ∪ 项目级）", async () => {
    get.mockResolvedValue({ data: { messages: [] }, error: null });
    const { result } = renderHook(() => useAiConversation("p1", 5));
    act(() => result.current.ensureLoaded());
    await waitFor(() => expect(result.current.status).toBe("ready"));
    expect(get).toHaveBeenCalledWith(
      LIST_PATH,
      expect.objectContaining({
        params: expect.objectContaining({
          path: { project_id: "p1" },
          query: { chapter_no: 5 },
        }),
      }),
    );
  });

  it("加载失败 → error 态 + toast，reload 可重试", async () => {
    get.mockResolvedValueOnce({ data: null, error: { detail: "boom" } });
    const { result } = renderHook(() => useAiConversation("p1", 1));
    act(() => result.current.ensureLoaded());
    await waitFor(() => expect(result.current.status).toBe("error"));
    expect(toast).toHaveBeenCalledWith(expect.any(String), "error");

    get.mockResolvedValueOnce({ data: { messages: [serverRow()] }, error: null });
    act(() => result.current.reload());
    await waitFor(() => expect(result.current.status).toBe("ready"));
    expect(result.current.messages).toHaveLength(1);
  });

  it("appendMessages：乐观插入 → 成功后按服务器行正位对账", async () => {
    get.mockResolvedValue({ data: { messages: [] }, error: null });
    const persisted = serverRow({ id: "srv-1", content: "refined", role: "ai" });
    post.mockResolvedValue({ data: { messages: [persisted] }, error: null });
    const { result } = renderHook(() => useAiConversation("p1", 1));
    act(() => result.current.ensureLoaded());
    await waitFor(() => expect(result.current.status).toBe("ready"));

    await act(async () => {
      await result.current.appendMessages({
        threadId: "t1",
        kind: "refine",
        chapterNo: 1,
        messages: [{ role: "ai", content: "refined" }],
      });
    });

    expect(post).toHaveBeenCalledTimes(1);
    // 临时行被服务器行替换（对账后只剩真实 id）。
    expect(result.current.messages).toHaveLength(1);
    expect(result.current.messages[0]?.id).toBe("srv-1");
  });

  it("appendMessages 失败 → 回滚乐观行 + toast，绝不 throw", async () => {
    get.mockResolvedValue({ data: { messages: [] }, error: null });
    post.mockResolvedValue({ data: null, error: { detail: "nope" } });
    const { result } = renderHook(() => useAiConversation("p1", 1));
    act(() => result.current.ensureLoaded());
    await waitFor(() => expect(result.current.status).toBe("ready"));

    await act(async () => {
      await expect(
        result.current.appendMessages({
          threadId: "t1",
          kind: "refine",
          chapterNo: 1,
          messages: [{ role: "ai", content: "x" }],
        }),
      ).resolves.toBeUndefined();
    });

    expect(result.current.messages).toHaveLength(0);
    expect(toast).toHaveBeenCalledWith(expect.any(String), "error");
  });

  it("silent 模式：不 GET、不 toast，append 只 fire-and-forget POST", async () => {
    post.mockResolvedValue({ data: { messages: [serverRow()] }, error: null });
    const { result } = renderHook(() =>
      useAiConversation("p1", 0, { silent: true }),
    );
    act(() => result.current.ensureLoaded());
    expect(get).not.toHaveBeenCalled();
    expect(result.current.status).toBe("idle");

    await act(async () => {
      await result.current.appendMessages({
        threadId: "t9",
        kind: "generator",
        toolKey: "book-title",
        chapterNo: null,
        messages: [{ role: "author", content: "brief" }],
      });
    });
    expect(post).toHaveBeenCalledTimes(1);
    expect(toast).not.toHaveBeenCalled();
  });

  it("silent 模式 append 失败也不 toast、不 throw", async () => {
    post.mockResolvedValue({ data: null, error: { detail: "x" } });
    const { result } = renderHook(() =>
      useAiConversation("p1", 0, { silent: true }),
    );
    await act(async () => {
      await result.current.appendMessages({
        threadId: "t9",
        kind: "generator",
        chapterNo: null,
        messages: [{ role: "author", content: "brief" }],
      });
    });
    expect(toast).not.toHaveBeenCalled();
  });

  it("切换项目 → 清缓存并复位（重新可加载）", async () => {
    get.mockResolvedValue({ data: { messages: [serverRow()] }, error: null });
    const { result, rerender } = renderHook(
      ({ pid }: { pid: string }) => useAiConversation(pid, 1),
      { initialProps: { pid: "p1" } },
    );
    act(() => result.current.ensureLoaded());
    await waitFor(() => expect(result.current.status).toBe("ready"));

    rerender({ pid: "p2" });
    expect(result.current.status).toBe("idle");
    expect(result.current.messages).toEqual([]);
  });
});
