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

import { useChapterRewrite } from "./useChapterRewrite";

// fetch（SSE 流）是 hook 的外部副作用边界，单测一律 stub 全局 fetch。
const fetchMock = vi.fn();

function sseStream(chunks: string[]): ReadableStream<Uint8Array> {
  const enc = new TextEncoder();
  return new ReadableStream({
    start(c) {
      for (const ch of chunks) c.enqueue(enc.encode(ch));
      c.close();
    },
  });
}

function sseResponse(chunks: string[]): Response {
  return new Response(sseStream(chunks), {
    status: 200,
    headers: { "Content-Type": "text/event-stream" },
  });
}

describe("useChapterRewrite", () => {
  beforeEach(() => {
    fetchMock.mockReset();
    vi.stubGlobal("fetch", fetchMock);
  });
  afterEach(() => {
    vi.unstubAllGlobals();
    vi.clearAllMocks();
  });

  it("初始为 idle、无版本", () => {
    const { result } = renderHook(() => useChapterRewrite());
    expect(result.current.state.phase).toBe("idle");
    expect(result.current.versions).toEqual([]);
    expect(result.current.latest).toBeNull();
  });

  it("重写完成（token+done）：流式累积正文并压入版本栈（带作者意见）", async () => {
    fetchMock.mockResolvedValue(
      sseResponse([
        'event:token\ndata:{"text":"新版"}\n\n',
        'event:token\ndata:{"text":"正文"}\n\n',
        'event:done\ndata:{"length":4}\n\n',
      ]),
    );

    const { result } = renderHook(() => useChapterRewrite());
    let returned: unknown;
    await act(async () => {
      returned = await result.current.send("p1", 3, "节奏太慢", "旧草稿");
    });

    // POST body 带 feedback + prior_draft（snake_case）。
    const call = fetchMock.mock.calls[0];
    expect(call?.[0]).toContain("/projects/p1/chapters/3/rewrite");
    expect(JSON.parse((call?.[1]?.body as string) ?? "")).toEqual({
      feedback: "节奏太慢",
      prior_draft: "旧草稿",
    });
    expect(result.current.state.phase).toBe("done");
    expect(result.current.versions).toEqual([
      { feedback: "节奏太慢", text: "新版正文" },
    ]);
    expect(result.current.latest?.text).toBe("新版正文");
    // 追加断言：send 返回本版（供 ChapterRewritePanel 结束后留痕）。
    expect(returned).toEqual({ feedback: "节奏太慢", text: "新版正文" });
  });

  it("多轮迭代：第二轮再压一版，latest 为最新版", async () => {
    fetchMock
      .mockResolvedValueOnce(
        sseResponse([
          'event:token\ndata:{"text":"第一版"}\n\n',
          'event:done\ndata:{"length":3}\n\n',
        ]),
      )
      .mockResolvedValueOnce(
        sseResponse([
          'event:token\ndata:{"text":"第二版"}\n\n',
          'event:done\ndata:{"length":3}\n\n',
        ]),
      );

    const { result } = renderHook(() => useChapterRewrite());
    await act(async () => {
      await result.current.send("p1", 1, "意见一", "原草稿");
    });
    await act(async () => {
      await result.current.send("p1", 1, "意见二", "第一版");
    });

    expect(result.current.versions).toHaveLength(2);
    expect(result.current.latest).toEqual({ feedback: "意见二", text: "第二版" });
  });

  it("error 帧：phase=error 且不入版本栈", async () => {
    fetchMock.mockResolvedValue(
      sseResponse([
        'event:error\ndata:{"code":"RATE_LIMIT","message":"配额不足"}\n\n',
      ]),
    );

    const { result } = renderHook(() => useChapterRewrite());
    await act(async () => {
      await result.current.send("p1", 1, "改", "草稿");
    });

    expect(result.current.state.phase).toBe("error");
    expect(result.current.state.error).toMatchObject({ code: "RATE_LIMIT" });
    expect(result.current.versions).toEqual([]);
  });

  it("流前错误（!res.ok）：解析 JSON 信封提取错误码", async () => {
    fetchMock.mockResolvedValue(
      new Response(
        JSON.stringify({ error: { code: "LLM_UNAVAILABLE", message: "无可用凭据" } }),
        { status: 503 },
      ),
    );

    const { result } = renderHook(() => useChapterRewrite());
    await act(async () => {
      await result.current.send("p1", 1, "改", "草稿");
    });

    expect(result.current.state.error).toMatchObject({ code: "LLM_UNAVAILABLE" });
    expect(result.current.versions).toEqual([]);
  });

  it("网络抛异常（非 Abort）：phase=error 且 code=NETWORK", async () => {
    fetchMock.mockRejectedValue(new Error("connection reset"));

    const { result } = renderHook(() => useChapterRewrite());
    await act(async () => {
      await result.current.send("p1", 1, "改", "草稿");
    });

    expect(result.current.state.error).toMatchObject({
      code: "NETWORK",
      message: "connection reset",
    });
  });

  it("reset 清回 idle 与空版本栈", async () => {
    fetchMock.mockResolvedValue(
      sseResponse([
        'event:token\ndata:{"text":"x"}\n\n',
        'event:done\ndata:{"length":1}\n\n',
      ]),
    );
    const { result } = renderHook(() => useChapterRewrite());
    await act(async () => {
      await result.current.send("p1", 1, "改", "草稿");
    });
    act(() => result.current.reset());
    expect(result.current.state.phase).toBe("idle");
    expect(result.current.versions).toEqual([]);
  });
});
