import { describe, it, expect } from "bun:test";
import { Stream } from "effect";
import type { AgentStreamEvent, StreamDensity } from "../src/stream-types.js";
import { AgentStream } from "../src/agent-stream.js";

describe("AgentStreamEvent types", () => {
  it("TextDelta has _tag and text", () => {
    const e: AgentStreamEvent = { _tag: "TextDelta", text: "hello" };
    expect(e._tag).toBe("TextDelta");
  });

  it("StreamCompleted carries output and metadata", () => {
    const e: AgentStreamEvent = {
      _tag: "StreamCompleted",
      output: "result",
      metadata: { duration: 100, cost: 0, tokensUsed: 50, stepsCount: 1 },
    };
    expect(e._tag).toBe("StreamCompleted");
  });

  it("StreamError carries cause", () => {
    const e: AgentStreamEvent = { _tag: "StreamError", cause: "timeout" };
    expect(e.cause).toBe("timeout");
  });

  it("StreamDensity is tokens or full", () => {
    const d1: StreamDensity = "tokens";
    const d2: StreamDensity = "full";
    expect(d1).toBe("tokens");
    expect(d2).toBe("full");
  });
});

describe("AgentStreamEvent new variants", () => {
  it("IterationProgress is a valid AgentStreamEvent shape", () => {
    const event: AgentStreamEvent = {
      _tag: "IterationProgress",
      iteration: 3,
      maxIterations: 10,
      toolsCalledThisStep: ["web-search"],
      status: "thinking",
    };
    expect(event._tag).toBe("IterationProgress");
  });

  it("StreamCancelled is a valid AgentStreamEvent shape", () => {
    const event: AgentStreamEvent = {
      _tag: "StreamCancelled",
      reason: "User cancelled",
      iterationsCompleted: 5,
    };
    expect(event._tag).toBe("StreamCancelled");
  });

  it("StreamCompleted with toolSummary is valid", () => {
    const event: AgentStreamEvent = {
      _tag: "StreamCompleted",
      output: "done",
      metadata: { stepsCount: 3, tokensUsed: 500, duration: 1200, cost: 0 },
      toolSummary: [{ name: "web-search", calls: 2, avgMs: 150 }],
    };
    expect(event._tag).toBe("StreamCompleted");
    expect((event as any).toolSummary).toHaveLength(1);
  });

  it("type discrimination works for IterationProgress", () => {
    const event: AgentStreamEvent = {
      _tag: "IterationProgress",
      iteration: 1,
      maxIterations: 5,
      status: "acting",
    };
    if (event._tag === "IterationProgress") {
      expect(event.iteration).toBe(1);
      expect(event.maxIterations).toBe(5);
    }
  });
});

describe("AgentStream adapters", () => {
  it("collect() resolves on StreamCompleted", async () => {
    const stream = (Stream.make as (...args: AgentStreamEvent[]) => Stream.Stream<AgentStreamEvent, never, never>)(
      { _tag: "TextDelta", text: "hello " },
      { _tag: "TextDelta", text: "world" },
      {
        _tag: "StreamCompleted",
        output: "hello world",
        metadata: { duration: 100, cost: 0, tokensUsed: 10, stepsCount: 1 },
      },
    );
    const result = await AgentStream.collect(stream);
    expect(result.output).toBe("hello world");
    expect(result.success).toBe(true);
  });

  it("collect() rejects on StreamError", async () => {
    const stream = (Stream.make as (...args: AgentStreamEvent[]) => Stream.Stream<AgentStreamEvent, never, never>)(
      { _tag: "StreamError", cause: "test failure" },
    );
    await expect(AgentStream.collect(stream)).rejects.toThrow("test failure");
  });

  it("collect() rejects if stream ends without terminal event", async () => {
    const stream = (Stream.make as (...args: AgentStreamEvent[]) => Stream.Stream<AgentStreamEvent, never, never>)(
      { _tag: "TextDelta", text: "partial" },
    );
    await expect(AgentStream.collect(stream)).rejects.toThrow(
      "Stream ended without StreamCompleted event",
    );
  });

  it("toAsyncIterable() yields events in order", async () => {
    const events: AgentStreamEvent[] = [
      { _tag: "TextDelta", text: "a" },
      { _tag: "TextDelta", text: "b" },
      { _tag: "StreamCompleted", output: "ab", metadata: { duration: 50, cost: 0, tokensUsed: 5, stepsCount: 1 } },
    ];
    const stream = Stream.fromIterable(events);
    const received: AgentStreamEvent[] = [];
    for await (const event of AgentStream.toAsyncIterable(stream)) {
      received.push(event);
    }
    expect(received.length).toBe(3);
    expect(received[0]?._tag).toBe("TextDelta");
    expect(received[2]?._tag).toBe("StreamCompleted");
  });
});
