import { ToolDeniedError } from "@voltagent/core";
import { describe, expect, it, vi } from "vitest";
import { processAgentOptions } from "../utils/options";
import { handleChatStream, handleGenerateText } from "./agent.handlers";

describe("server-core: agent.handlers ClientHTTPError mapping", () => {
  it("handleGenerateText should map ClientHTTPError (ToolDeniedError) to ApiResponse error fields", async () => {
    const logger = { error: vi.fn() } as any;

    const mockAgent = {
      generateText: vi.fn(async () => {
        throw new ToolDeniedError({
          toolName: "web-search",
          message: "Quota exceeded for web-search",
          code: "TOOL_QUOTA_EXCEEDED",
          httpStatus: 429,
        });
      }),
    } as any;

    const deps = {
      agentRegistry: {
        getAgent: vi.fn(() => mockAgent),
      },
    } as any;

    const res = await handleGenerateText("agent-1", { input: "hi" }, deps, logger);

    expect(res).toMatchObject({
      success: false,
      error: "Quota exceeded for web-search",
      code: "TOOL_QUOTA_EXCEEDED",
      name: "web-search",
      httpStatus: 429,
    });
  });

  it("handleGenerateText should fallback for non-ClientHTTPError", async () => {
    const logger = { error: vi.fn() } as any;

    const mockAgent = {
      generateText: vi.fn(async () => {
        throw new Error("Model timeout");
      }),
    } as any;

    const deps = {
      agentRegistry: {
        getAgent: vi.fn(() => mockAgent),
      },
    } as any;

    const res = await handleGenerateText("agent-1", { input: "hi" }, deps, logger);

    expect(res).toMatchObject({
      success: false,
      error: "Model timeout",
    });
  });

  it("handleGenerateText should pass request headers into agent options", async () => {
    const logger = { error: vi.fn() } as any;

    const mockAgent = {
      generateText: vi.fn(async () => ({
        text: "ok",
        usage: undefined,
        finishReason: "stop",
        toolCalls: [],
        toolResults: [],
        feedback: null,
      })),
    } as any;

    const deps = {
      agentRegistry: {
        getAgent: vi.fn(() => mockAgent),
      },
    } as any;

    const headers = new Headers({
      Authorization: "Bearer test-token",
      "X-Tenant-ID": "tenant-1",
    });

    const res = await handleGenerateText(
      "agent-1",
      { input: "hi" },
      deps,
      logger,
      undefined,
      headers,
    );

    expect(res.success).toBe(true);
    expect(mockAgent.generateText).toHaveBeenCalledWith(
      "hi",
      expect.objectContaining({
        requestHeaders: {
          authorization: "Bearer test-token",
          "x-tenant-id": "tenant-1",
        },
      }),
    );
  });
});

describe("server-core: processAgentOptions", () => {
  it("processAgentOptions should lowercase Headers entries", () => {
    const headers = new Headers();
    vi.spyOn(headers, "entries").mockReturnValue(
      [
        ["Authorization", "Bearer test-token"],
        ["X-Tenant-ID", "tenant-1"],
      ][Symbol.iterator]() as HeadersIterator<[string, string]>,
    );

    const options = processAgentOptions({ options: {} }, undefined, headers);

    expect(options.requestHeaders).toEqual({
      authorization: "Bearer test-token",
      "x-tenant-id": "tenant-1",
    });
  });
});

describe("server-core: agent.handlers resumable memory envelope", () => {
  it("handleChatStream should resolve conversationId/userId from options.memory for resumable streams", async () => {
    const logger = {
      error: vi.fn(),
      warn: vi.fn(),
    } as any;

    const streamText = vi.fn(async () => ({
      toUIMessageStreamResponse: vi.fn(() => new Response("ok", { status: 200 })),
    }));

    const deps = {
      agentRegistry: {
        getAgent: vi.fn(() => ({ streamText })),
      },
      resumableStreamDefault: false,
      resumableStream: {
        clearActiveStream: vi.fn(async () => undefined),
      },
    } as any;

    const res = await handleChatStream(
      "agent-1",
      {
        input: "hello",
        options: {
          resumableStream: true,
          conversationId: "legacy-conv",
          userId: "legacy-user",
          memory: {
            conversationId: "conv-1",
            userId: "user-1",
          },
        },
      },
      deps,
      logger,
    );

    expect(res.status).toBe(200);
    expect(streamText).toHaveBeenCalledWith(
      "hello",
      expect.objectContaining({
        resumableStream: true,
        memory: {
          conversationId: "conv-1",
          userId: "user-1",
        },
      }),
    );
    expect(deps.resumableStream.clearActiveStream).toHaveBeenCalledWith({
      conversationId: "conv-1",
      agentId: "agent-1",
      userId: "user-1",
    });
  });

  it("handleChatStream should ignore blank memory.conversationId and fall back to legacy conversationId", async () => {
    const logger = {
      error: vi.fn(),
      warn: vi.fn(),
    } as any;

    const streamText = vi.fn(async () => ({
      toUIMessageStreamResponse: vi.fn(() => new Response("ok", { status: 200 })),
    }));

    const deps = {
      agentRegistry: {
        getAgent: vi.fn(() => ({ streamText })),
      },
      resumableStreamDefault: false,
      resumableStream: {
        clearActiveStream: vi.fn(async () => undefined),
      },
    } as any;

    const res = await handleChatStream(
      "agent-1",
      {
        input: "hello",
        options: {
          resumableStream: true,
          conversationId: "legacy-conv",
          userId: "legacy-user",
          memory: {
            conversationId: "   ",
            userId: "user-1",
          },
        },
      },
      deps,
      logger,
    );

    expect(res.status).toBe(200);
    expect(deps.resumableStream.clearActiveStream).toHaveBeenCalledWith({
      conversationId: "legacy-conv",
      agentId: "agent-1",
      userId: "user-1",
    });
  });
});
