import { beforeEach, describe, expect, it, vi } from "vitest";
import { LLMErrorCode } from "@dexto/core";
import { processStream } from "./processStream.js";
const { captureAnalyticsMock } = vi.hoisted(() => ({
  captureAnalyticsMock: vi.fn()
}));
vi.mock("../host/index.js", () => ({
  captureAnalytics: captureAnalyticsMock
}));
function isStateUpdater(action) {
  return typeof action === "function";
}
function createState(initial) {
  let state = initial;
  return {
    get: () => state,
    set: (action) => {
      state = isStateUpdater(action) ? action(state) : action;
    }
  };
}
async function* eventStream(events) {
  for (const event of events) {
    yield event;
  }
}
function llmResponse(overrides) {
  return {
    name: "llm:response",
    sessionId: "test-session",
    content: "Hello",
    provider: "openai",
    model: "gpt-4",
    finishReason: "stop",
    tokenUsage: {
      inputTokens: 1,
      outputTokens: 1,
      totalTokens: 2
    },
    ...overrides
  };
}
function createSetters({
  steerMessages: initialSteerMessages = [],
  queuedMessages: initialQueuedMessages = []
} = {}) {
  const messages = createState([]);
  const pendingMessages = createState([]);
  const dequeuedBuffer = createState([]);
  const steerMessages = createState(initialSteerMessages);
  const queuedMessages = createState(initialQueuedMessages);
  const ui = createState({
    isProcessing: false,
    isCancelling: false,
    isThinking: false,
    isCompacting: false,
    activeOverlay: "none",
    showReasoning: true,
    exitWarningShown: false,
    exitWarningTimestamp: null,
    mcpWizardServerType: null,
    copyModeEnabled: false,
    pendingModelSwitch: null,
    selectedMcpServer: null,
    historySearch: {
      isActive: false,
      query: "",
      matchIndex: 0,
      originalInput: "",
      lastMatch: ""
    },
    promptAddWizard: null,
    autoApproveEdits: false,
    todoExpanded: true,
    backgroundTasksRunning: 0,
    backgroundTasksExpanded: false,
    backgroundTasks: [],
    chatgptRateLimitStatus: null,
    insufficientCredits: null,
    planModeActive: false,
    planModeInitialized: false,
    commandOutput: null,
    bypassPermissions: false
  });
  const session = createState({
    id: "test-session",
    hasActiveSession: true,
    modelName: "test-model"
  });
  const createNoopDispatch = () => () => void 0;
  return {
    getMessages: messages.get,
    getPendingMessages: pendingMessages.get,
    getSteerMessages: steerMessages.get,
    getQueuedMessages: queuedMessages.get,
    getUi: ui.get,
    setters: {
      setMessages: messages.set,
      setPendingMessages: pendingMessages.set,
      setDequeuedBuffer: dequeuedBuffer.set,
      setUi: ui.set,
      setSession: session.set,
      setSteerMessages: steerMessages.set,
      setQueuedMessages: queuedMessages.set,
      setApproval: createNoopDispatch(),
      setApprovalQueue: createNoopDispatch()
    }
  };
}
describe("processStream (reasoning)", () => {
  beforeEach(() => {
    captureAnalyticsMock.mockClear();
  });
  it("removes only dequeued queue ids from steer and follow-up state", async () => {
    const steer = {
      id: "steer-1",
      content: [{ type: "text", text: "active steer" }],
      queuedAt: Date.now(),
      kind: "default"
    };
    const followUp = {
      id: "follow-up-1",
      content: [{ type: "text", text: "later" }],
      queuedAt: Date.now(),
      kind: "default"
    };
    const { getSteerMessages, getQueuedMessages, setters } = createSetters({
      steerMessages: [steer],
      queuedMessages: [followUp]
    });
    await processStream(
      eventStream([
        {
          name: "message:dequeued",
          sessionId: "test-session",
          count: 1,
          ids: [steer.id],
          queue: "steer",
          coalesced: false,
          content: steer.content,
          messages: [steer]
        }
      ]),
      setters,
      {
        useStreaming: true,
        autoApproveEditsRef: { current: false },
        bypassPermissionsRef: { current: false },
        eventBus: { emit: vi.fn() }
      }
    );
    expect(getSteerMessages()).toEqual([]);
    expect(getQueuedMessages()).toEqual([followUp]);
  });
  it("renders blocked interactions as final assistant messages", async () => {
    const { getMessages, getUi, setters } = createSetters();
    await processStream(
      eventStream([
        { name: "llm:thinking", sessionId: "test-session" },
        {
          name: "interaction:blocked",
          sessionId: "test-session",
          content: "Error: blocked by policy",
          provider: "openai",
          model: "gpt-4",
          messageId: "blocked-message"
        }
      ]),
      setters,
      {
        useStreaming: true,
        autoApproveEditsRef: { current: false },
        bypassPermissionsRef: { current: false },
        eventBus: { emit: vi.fn() }
      }
    );
    expect(getUi().isThinking).toBe(false);
    expect(getMessages()).toEqual([
      expect.objectContaining({
        id: "blocked-message",
        role: "assistant",
        content: "Error: blocked by policy",
        isStreaming: false
      })
    ]);
  });
  it("attaches streamed reasoning chunks to the assistant message", async () => {
    const { getMessages, getPendingMessages, setters } = createSetters();
    const events = [
      { name: "llm:thinking", sessionId: "test-session" },
      {
        name: "llm:chunk",
        sessionId: "test-session",
        chunkType: "reasoning",
        content: "R1"
      },
      {
        name: "llm:chunk",
        sessionId: "test-session",
        chunkType: "reasoning",
        content: "R2"
      },
      {
        name: "llm:chunk",
        sessionId: "test-session",
        chunkType: "text",
        content: "Hello"
      },
      llmResponse({
        content: "Hello"
      }),
      {
        name: "run:complete",
        sessionId: "test-session",
        finishReason: "stop",
        stepCount: 1,
        durationMs: 1
      }
    ];
    const iterator = eventStream(events);
    await processStream(iterator, setters, {
      useStreaming: true,
      autoApproveEditsRef: { current: false },
      bypassPermissionsRef: { current: false },
      eventBus: { emit: vi.fn() }
    });
    expect(getPendingMessages()).toEqual([]);
    const assistantMessages = getMessages().filter((m) => m.role === "assistant");
    expect(assistantMessages).toHaveLength(1);
    expect(assistantMessages[0]?.content).toBe("Hello");
    expect(assistantMessages[0]?.reasoning).toBe("R1R2");
  });
  it("does not duplicate reasoning when progressive splitting occurs", async () => {
    const { getMessages, setters } = createSetters();
    const longText = "A".repeat(120) + "\n\n" + "B".repeat(120) + "\n\n" + "C".repeat(120);
    const events = [
      { name: "llm:thinking", sessionId: "test-session" },
      {
        name: "llm:chunk",
        sessionId: "test-session",
        chunkType: "reasoning",
        content: "R"
      },
      {
        name: "llm:chunk",
        sessionId: "test-session",
        chunkType: "text",
        content: longText
      },
      llmResponse({
        content: longText
      }),
      {
        name: "run:complete",
        sessionId: "test-session",
        finishReason: "stop",
        stepCount: 1,
        durationMs: 1
      }
    ];
    const iterator = eventStream(events);
    await processStream(iterator, setters, {
      useStreaming: true,
      autoApproveEditsRef: { current: false },
      bypassPermissionsRef: { current: false },
      eventBus: { emit: vi.fn() }
    });
    const assistantMessages = getMessages().filter((m) => m.role === "assistant");
    expect(assistantMessages.length).toBeGreaterThanOrEqual(2);
    const messagesWithReasoning = assistantMessages.filter((m) => m.reasoning);
    expect(messagesWithReasoning).toHaveLength(1);
    expect(messagesWithReasoning[0]?.reasoning).toBe("R");
  });
  it("includes reasoning in non-streaming mode (accumulated from chunks)", async () => {
    const { getMessages, setters } = createSetters();
    const events = [
      { name: "llm:thinking", sessionId: "test-session" },
      {
        name: "llm:chunk",
        sessionId: "test-session",
        chunkType: "reasoning",
        content: "R"
      },
      {
        name: "llm:chunk",
        sessionId: "test-session",
        chunkType: "text",
        content: "Hello"
      },
      llmResponse({
        content: "Hello"
      }),
      {
        name: "run:complete",
        sessionId: "test-session",
        finishReason: "stop",
        stepCount: 1,
        durationMs: 1
      }
    ];
    const iterator = eventStream(events);
    await processStream(iterator, setters, {
      useStreaming: false,
      autoApproveEditsRef: { current: false },
      bypassPermissionsRef: { current: false },
      eventBus: { emit: vi.fn() }
    });
    const assistantMessages = getMessages().filter((m) => m.role === "assistant");
    expect(assistantMessages).toHaveLength(1);
    expect(assistantMessages[0]?.content).toBe("Hello");
    expect(assistantMessages[0]?.reasoning).toBe("R");
  });
  it("does not duplicate reasoning in non-streaming mode when reasoning is emitted before a tool call", async () => {
    const { getMessages, setters } = createSetters();
    const events = [
      { name: "llm:thinking", sessionId: "test-session" },
      {
        name: "llm:chunk",
        sessionId: "test-session",
        chunkType: "reasoning",
        content: "R"
      },
      {
        name: "llm:tool-call",
        sessionId: "test-session",
        toolName: "test-tool",
        callId: "call-1",
        args: {}
      },
      llmResponse({
        content: "Final",
        reasoning: "R"
      }),
      {
        name: "run:complete",
        sessionId: "test-session",
        finishReason: "stop",
        stepCount: 1,
        durationMs: 1
      }
    ];
    const iterator = eventStream(events);
    await processStream(iterator, setters, {
      useStreaming: false,
      autoApproveEditsRef: { current: false },
      bypassPermissionsRef: { current: false },
      eventBus: { emit: vi.fn() }
    });
    const assistantMessages = getMessages().filter((m) => m.role === "assistant");
    expect(assistantMessages).toHaveLength(2);
    expect(assistantMessages[0]?.content).toBe("");
    expect(assistantMessages[0]?.reasoning).toBe("R");
    expect(assistantMessages[1]?.content).toBe("Final");
    expect(assistantMessages[1]?.reasoning).toBeUndefined();
  });
  it("shows the billing recovery overlay for insufficient credits", async () => {
    const { getMessages, getUi, setters } = createSetters();
    const error = Object.assign(new Error("Insufficient Dexto credits. Balance: $0.00"), {
      code: LLMErrorCode.INSUFFICIENT_CREDITS,
      context: {
        balance: 0
      },
      recovery: "Run `dexto billing` to check your balance"
    });
    const events = [
      {
        name: "llm:error",
        sessionId: "test-session",
        error
      }
    ];
    await processStream(eventStream(events), setters, {
      useStreaming: true,
      autoApproveEditsRef: { current: false },
      bypassPermissionsRef: { current: false },
      eventBus: { emit: vi.fn() }
    });
    const ui = getUi();
    expect(ui.activeOverlay).toBe("insufficient-credits");
    expect(ui.insufficientCredits).toEqual({ balanceUsd: 0 });
    expect(ui.isProcessing).toBe(false);
    const latestMessage = getMessages().at(-1);
    expect(latestMessage?.content).toContain("Out of Dexto Nova credits");
    expect(latestMessage?.content).not.toContain("Error:");
  });
  it("captures analytics cost fields for priced llm responses", async () => {
    const { setters } = createSetters();
    const events = [
      { name: "llm:thinking", sessionId: "test-session" },
      llmResponse({
        content: "Priced response",
        provider: "openai",
        model: "gpt-4",
        estimatedCost: 15e-4,
        costBreakdown: {
          inputUsd: 1e-3,
          outputUsd: 5e-4,
          reasoningUsd: 0,
          cacheReadUsd: 0,
          cacheWriteUsd: 0,
          totalUsd: 15e-4
        },
        tokenUsage: {
          inputTokens: 10,
          outputTokens: 20,
          totalTokens: 30
        }
      }),
      {
        name: "run:complete",
        sessionId: "test-session",
        finishReason: "stop",
        stepCount: 1,
        durationMs: 1
      }
    ];
    await processStream(eventStream(events), setters, {
      useStreaming: false,
      autoApproveEditsRef: { current: false },
      bypassPermissionsRef: { current: false },
      eventBus: { emit: vi.fn() }
    });
    expect(captureAnalyticsMock).toHaveBeenCalledWith(
      "dexto_llm_tokens_consumed",
      expect.objectContaining({
        source: "cli",
        sessionId: "test-session",
        estimatedCostUsd: 15e-4,
        inputCostUsd: 1e-3,
        outputCostUsd: 5e-4,
        reasoningCostUsd: 0,
        cacheReadCostUsd: 0,
        cacheWriteCostUsd: 0
      })
    );
  });
});
