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

import { useDraftStream } from "./useDraftStream";

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

// 把若干 SSE 文本块封进 ReadableStream，模拟后端逐块下发的 chunked body。
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();
    },
  });
}

// 构造一个流式成功响应（200 + text/event-stream body）。
function sseResponse(chunks: string[]): Response {
  return new Response(sseStream(chunks), {
    status: 200,
    headers: { "Content-Type": "text/event-stream" },
  });
}

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

  it("初始为 idle、空文本、未在流式", () => {
    const { result } = renderHook(() => useDraftStream());
    expect(result.current.state.phase).toBe("idle");
    expect(result.current.state.text).toBe("");
    expect(result.current.isStreaming).toBe(false);
  });

  it("流式累积 token：多个 token 帧拼成完整文本", async () => {
    fetchMock.mockResolvedValue(
      sseResponse([
        'event:token\ndata:{"text":"你好"}\n\n',
        'event:token\ndata:{"text":"，世界"}\n\n',
      ]),
    );

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 1);
    });

    expect(result.current.state.text).toBe("你好，世界");
    expect(result.current.state.error).toBeNull();
  });

  it("收到 done 帧后 phase 走到 done", async () => {
    fetchMock.mockResolvedValue(
      sseResponse([
        'event:token\ndata:{"text":"abc"}\n\n',
        'event:done\ndata:{"length":3}\n\n',
      ]),
    );

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 1);
    });

    expect(result.current.state.phase).toBe("done");
    expect(result.current.state.text).toBe("abc");
  });

  it("带本章指令时以 JSON body POST 指令", async () => {
    fetchMock.mockResolvedValue(sseResponse(['event:done\ndata:{"length":0}\n\n']));

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 2, "  写得热血一点  ");
    });

    const [, init] = fetchMock.mock.calls[0];
    expect(init.method).toBe("POST");
    expect(init.headers["Content-Type"]).toBe("application/json");
    expect(JSON.parse(init.body as string)).toEqual({ directive: "写得热血一点" });
  });

  it("指令为空白时退回裸 POST（不带 JSON body）", async () => {
    fetchMock.mockResolvedValue(sseResponse(['event:done\ndata:{"length":0}\n\n']));

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 2, "   ");
    });

    const [, init] = fetchMock.mock.calls[0];
    expect(init.body).toBeUndefined();
    expect(init.headers["Content-Type"]).toBeUndefined();
  });

  it("收到 error 帧：phase=error 且带错误码与文案", async () => {
    fetchMock.mockResolvedValue(
      sseResponse([
        'event:error\ndata:{"code":"RATE_LIMIT","message":"配额不足"}\n\n',
      ]),
    );

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 1);
    });

    expect(result.current.state.phase).toBe("error");
    expect(result.current.state.error).toMatchObject({
      code: "RATE_LIMIT",
      message: "配额不足",
    });
  });

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

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 1);
    });

    expect(result.current.state.phase).toBe("error");
    expect(result.current.state.error).toMatchObject({
      code: "LLM_UNAVAILABLE",
      message: "无可用凭据",
    });
  });

  it("流前错误且非 JSON 信封：沿用默认 STREAM_FAILED 文案", async () => {
    fetchMock.mockResolvedValue(new Response("<html>oops</html>", { status: 500 }));

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 1);
    });

    expect(result.current.state.phase).toBe("error");
    expect(result.current.state.error?.code).toBe("STREAM_FAILED");
    expect(result.current.state.error?.message).toContain("500");
  });

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

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 1);
    });

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

  it("非 Error 抛出：回退为未知网络错误文案", async () => {
    fetchMock.mockRejectedValue("boom-string");

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 1);
    });

    expect(result.current.state.error?.message).toBe("未知网络错误");
  });

  it("AbortError 被吞掉：不进入 error，phase 维持 streaming", async () => {
    fetchMock.mockRejectedValue(new DOMException("aborted", "AbortError"));

    const { result } = renderHook(() => useDraftStream());
    await act(async () => {
      await result.current.start("p1", 1);
    });

    expect(result.current.state.phase).toBe("streaming");
    expect(result.current.state.error).toBeNull();
  });

  it("stop() 主动停止：abort 连接并将 phase 置 aborted", async () => {
    // 用一个永不结束的 body，让流挂起后被 stop 中断（已生成部分保留）。
    const hangingBody = new ReadableStream<Uint8Array>({});
    fetchMock.mockResolvedValue(new Response(hangingBody, { status: 200 }));

    const { result } = renderHook(() => useDraftStream());
    act(() => {
      void result.current.start("p1", 1);
    });
    await act(async () => {
      result.current.stop();
    });

    expect(result.current.state.phase).toBe("aborted");
    expect(result.current.isStreaming).toBe(false);
  });

  it("reset(text) 用给定文本重置回 idle", () => {
    const { result } = renderHook(() => useDraftStream());
    act(() => result.current.reset("已落草稿"));
    expect(result.current.state.phase).toBe("idle");
    expect(result.current.state.text).toBe("已落草稿");
  });
});
