import { describe, expect, it } from "vitest";

import {
  SseFrameBuffer,
  initialStreamState,
  parseSseBlock,
  reduceStream,
  type SseEvent,
} from "./sse";

describe("parseSseBlock", () => {
  it("parses a token frame", () => {
    const evt = parseSseBlock('event:token\ndata:{"text":"夜"}');
    expect(evt).toEqual({ event: "token", data: { text: "夜" } });
  });

  it("parses a done frame", () => {
    const evt = parseSseBlock('event:done\ndata:{"length":12}');
    expect(evt).toEqual({ event: "done", data: { length: 12 } });
  });

  it("parses an error frame with request_id", () => {
    const evt = parseSseBlock(
      'event:error\ndata:{"code":"INTERNAL","message":"boom","request_id":"r1"}',
    );
    expect(evt).toEqual({
      event: "error",
      data: { code: "INTERNAL", message: "boom", request_id: "r1" },
    });
  });

  it("ignores comment/heartbeat lines and tolerates leading space after colon", () => {
    const evt = parseSseBlock(': keep-alive\nevent: token\ndata: {"text":"x"}');
    expect(evt).toEqual({ event: "token", data: { text: "x" } });
  });

  it("returns null for unknown event types", () => {
    expect(parseSseBlock('event:section\ndata:{"x":1}')).toBeNull();
  });

  it("returns null for malformed json", () => {
    expect(parseSseBlock("event:token\ndata:{not json")).toBeNull();
  });

  // P1-8：逐事件类型守卫——已知事件但 data 形状不符也拒绝（不再强转放行）。
  it("returns null when token frame lacks string text", () => {
    expect(parseSseBlock('event:token\ndata:{"text":42}')).toBeNull();
  });

  it("returns null when error frame lacks code/message", () => {
    expect(parseSseBlock('event:error\ndata:{"code":"X"}')).toBeNull();
  });

  it("coerces missing request_id to null on error frame", () => {
    expect(
      parseSseBlock('event:error\ndata:{"code":"X","message":"m"}'),
    ).toEqual({
      event: "error",
      data: { code: "X", message: "m", request_id: null },
    });
  });
});

describe("SseFrameBuffer", () => {
  it("emits complete blocks split by blank line and buffers the tail", () => {
    const buf = new SseFrameBuffer();
    const first = buf.push('event:token\ndata:{"text":"a"}\n\nevent:tok');
    expect(first).toEqual([{ event: "token", data: { text: "a" } }]);
    const second = buf.push('en\ndata:{"text":"b"}\n\n');
    expect(second).toEqual([{ event: "token", data: { text: "b" } }]);
  });

  it("handles CRLF boundaries", () => {
    const buf = new SseFrameBuffer();
    const out = buf.push('event:token\r\ndata:{"text":"z"}\r\n\r\n');
    expect(out).toEqual([{ event: "token", data: { text: "z" } }]);
  });
});

describe("reduceStream", () => {
  it("accumulates token text and marks streaming", () => {
    const tokens: SseEvent[] = [
      { event: "token", data: { text: "夜色" } },
      { event: "token", data: { text: "如墨" } },
    ];
    const state = tokens.reduce(reduceStream, initialStreamState);
    expect(state.text).toBe("夜色如墨");
    expect(state.phase).toBe("streaming");
  });

  it("marks done on done event preserving text", () => {
    let s = reduceStream(initialStreamState, {
      event: "token",
      data: { text: "abc" },
    });
    s = reduceStream(s, { event: "done", data: { length: 3 } });
    expect(s.phase).toBe("done");
    expect(s.text).toBe("abc");
  });

  it("captures error and retains partial text", () => {
    let s = reduceStream(initialStreamState, {
      event: "token",
      data: { text: "partial" },
    });
    s = reduceStream(s, {
      event: "error",
      data: { code: "LLM_UNAVAILABLE", message: "no key" },
    });
    expect(s.phase).toBe("error");
    expect(s.text).toBe("partial");
    expect(s.error?.code).toBe("LLM_UNAVAILABLE");
  });
});
