import { describe, it, expect, beforeEach, vi, type Mock } from "vitest";
import { ClaudeProvider, type ClaudeProviderConfig } from "./claude.provider";
import type { ChatRequestDto, EmbedRequestDto } from "../dto";

// Mock the @anthropic-ai/sdk module
vi.mock("@anthropic-ai/sdk", () => {
  return {
    default: vi.fn().mockImplementation(function (this: unknown) {
      return {
        messages: {
          create: vi.fn(),
          stream: vi.fn(),
        },
      };
    }),
  };
});

describe("ClaudeProvider", () => {
  let provider: ClaudeProvider;
  let config: ClaudeProviderConfig;
  let mockAnthropicInstance: {
    messages: {
      create: Mock;
      stream: Mock;
    };
  };

  beforeEach(() => {
    // Reset all mocks
    vi.clearAllMocks();

    // Setup test configuration
    config = {
      endpoint: "https://api.anthropic.com",
      apiKey: "sk-ant-test-1234567890",
      timeout: 30000,
    };

    provider = new ClaudeProvider(config);

    // Get the mock instance created by the constructor
    mockAnthropicInstance = (provider as any).client;
  });

  describe("constructor and initialization", () => {
    it("should create provider with correct name and type", () => {
      expect(provider.name).toBe("Claude");
      expect(provider.type).toBe("claude");
    });

    it("should initialize successfully", async () => {
      await expect(provider.initialize()).resolves.toBeUndefined();
    });

    it("should use default endpoint when not provided", () => {
      const configWithoutEndpoint: ClaudeProviderConfig = {
        endpoint: "https://api.anthropic.com",
        apiKey: "sk-ant-test-1234567890",
      };

      const providerWithDefaults = new ClaudeProvider(configWithoutEndpoint);
      const returnedConfig = providerWithDefaults.getConfig();

      expect(returnedConfig.endpoint).toBe("https://api.anthropic.com");
    });
  });

  describe("checkHealth", () => {
    it("should return healthy status when Claude API is reachable", async () => {
      // Claude doesn't have a health check endpoint, so we test that it returns static models
      const health = await provider.checkHealth();

      expect(health.healthy).toBe(true);
      expect(health.provider).toBe("claude");
      expect(health.endpoint).toBe(config.endpoint);
      expect(health.models).toBeDefined();
      expect(health.models?.length).toBeGreaterThan(0);
      expect(health.models).toContain("claude-opus-4-20250514");
    });

    it("should return unhealthy status when Claude API is unreachable", async () => {
      // Mock a failing API call
      mockAnthropicInstance.messages.create.mockRejectedValue(new Error("API key invalid"));

      const health = await provider.checkHealth();

      expect(health.healthy).toBe(false);
      expect(health.provider).toBe("claude");
      expect(health.endpoint).toBe(config.endpoint);
      expect(health.error).toBe("API key invalid");
    });

    it("should handle non-Error exceptions", async () => {
      mockAnthropicInstance.messages.create.mockRejectedValue("string error");

      const health = await provider.checkHealth();

      expect(health.healthy).toBe(false);
      expect(health.error).toBe("string error");
    });
  });

  describe("listModels", () => {
    it("should return static list of Claude models", async () => {
      const models = await provider.listModels();

      expect(models).toBeDefined();
      expect(Array.isArray(models)).toBe(true);
      expect(models.length).toBeGreaterThan(0);
      expect(models).toContain("claude-opus-4-20250514");
      expect(models).toContain("claude-sonnet-4-20250514");
      expect(models).toContain("claude-3-5-sonnet-20241022");
      expect(models).toContain("claude-3-5-haiku-20241022");
    });
  });

  describe("chat", () => {
    it("should perform chat completion successfully", async () => {
      const request: ChatRequestDto = {
        model: "claude-opus-4-20250514",
        messages: [{ role: "user", content: "Hello" }],
      };

      const mockResponse = {
        id: "msg_123",
        type: "message",
        role: "assistant",
        content: [
          {
            type: "text",
            text: "Hello! How can I assist you today?",
          },
        ],
        model: "claude-opus-4-20250514",
        stop_reason: "end_turn",
        stop_sequence: null,
        usage: {
          input_tokens: 10,
          output_tokens: 8,
        },
      };

      mockAnthropicInstance.messages.create.mockResolvedValue(mockResponse);

      const response = await provider.chat(request);

      expect(response).toEqual({
        model: "claude-opus-4-20250514",
        message: { role: "assistant", content: "Hello! How can I assist you today?" },
        done: true,
        promptEvalCount: 10,
        evalCount: 8,
      });

      expect(mockAnthropicInstance.messages.create).toHaveBeenCalledWith({
        model: "claude-opus-4-20250514",
        max_tokens: 1024,
        messages: [{ role: "user", content: "Hello" }],
      });
    });

    it("should include system prompt separately", async () => {
      const request: ChatRequestDto = {
        model: "claude-opus-4-20250514",
        messages: [{ role: "user", content: "Hello" }],
        systemPrompt: "You are a helpful assistant",
      };

      mockAnthropicInstance.messages.create.mockResolvedValue({
        id: "msg_123",
        type: "message",
        role: "assistant",
        content: [{ type: "text", text: "Hi!" }],
        model: "claude-opus-4-20250514",
        stop_reason: "end_turn",
        usage: { input_tokens: 15, output_tokens: 2 },
      });

      await provider.chat(request);

      expect(mockAnthropicInstance.messages.create).toHaveBeenCalledWith({
        model: "claude-opus-4-20250514",
        max_tokens: 1024,
        system: "You are a helpful assistant",
        messages: [{ role: "user", content: "Hello" }],
      });
    });

    it("should filter out system messages from messages array", async () => {
      const request: ChatRequestDto = {
        model: "claude-opus-4-20250514",
        messages: [
          { role: "system", content: "System prompt from messages" },
          { role: "user", content: "Hello" },
        ],
      };

      mockAnthropicInstance.messages.create.mockResolvedValue({
        id: "msg_123",
        type: "message",
        role: "assistant",
        content: [{ type: "text", text: "Hi!" }],
        model: "claude-opus-4-20250514",
        stop_reason: "end_turn",
        usage: { input_tokens: 15, output_tokens: 2 },
      });

      await provider.chat(request);

      // System message should be moved to system field, not in messages array
      expect(mockAnthropicInstance.messages.create).toHaveBeenCalledWith({
        model: "claude-opus-4-20250514",
        max_tokens: 1024,
        system: "System prompt from messages",
        messages: [{ role: "user", content: "Hello" }],
      });
    });

    it("should pass temperature and maxTokens as parameters", async () => {
      const request: ChatRequestDto = {
        model: "claude-opus-4-20250514",
        messages: [{ role: "user", content: "Hello" }],
        temperature: 0.7,
        maxTokens: 2000,
      };

      mockAnthropicInstance.messages.create.mockResolvedValue({
        id: "msg_123",
        type: "message",
        role: "assistant",
        content: [{ type: "text", text: "Hi!" }],
        model: "claude-opus-4-20250514",
        stop_reason: "end_turn",
        usage: { input_tokens: 10, output_tokens: 2 },
      });

      await provider.chat(request);

      expect(mockAnthropicInstance.messages.create).toHaveBeenCalledWith({
        model: "claude-opus-4-20250514",
        max_tokens: 2000,
        messages: [{ role: "user", content: "Hello" }],
        temperature: 0.7,
      });
    });

    it("should throw error when chat fails", async () => {
      const request: ChatRequestDto = {
        model: "claude-opus-4-20250514",
        messages: [{ role: "user", content: "Hello" }],
      };

      mockAnthropicInstance.messages.create.mockRejectedValue(new Error("Model not available"));

      await expect(provider.chat(request)).rejects.toThrow("Chat completion failed");
    });

    it("should handle multiple content blocks", async () => {
      const request: ChatRequestDto = {
        model: "claude-opus-4-20250514",
        messages: [{ role: "user", content: "Hello" }],
      };

      const mockResponse = {
        id: "msg_123",
        type: "message",
        role: "assistant",
        content: [
          { type: "text", text: "Hello! " },
          { type: "text", text: "How can I help?" },
        ],
        model: "claude-opus-4-20250514",
        stop_reason: "end_turn",
        usage: { input_tokens: 10, output_tokens: 8 },
      };

      mockAnthropicInstance.messages.create.mockResolvedValue(mockResponse);

      const response = await provider.chat(request);

      expect(response.message.content).toBe("Hello! How can I help?");
    });
  });

  describe("chatStream", () => {
    it("should stream chat completion chunks", async () => {
      const request: ChatRequestDto = {
        model: "claude-opus-4-20250514",
        messages: [{ role: "user", content: "Hello" }],
      };

      // Mock stream events
      const mockEvents = [
        {
          type: "message_start",
          message: {
            id: "msg_123",
            type: "message",
            role: "assistant",
            content: [],
            model: "claude-opus-4-20250514",
            usage: { input_tokens: 10, output_tokens: 0 },
          },
        },
        {
          type: "content_block_start",
          index: 0,
          content_block: { type: "text", text: "" },
        },
        {
          type: "content_block_delta",
          index: 0,
          delta: { type: "text_delta", text: "Hello" },
        },
        {
          type: "content_block_delta",
          index: 0,
          delta: { type: "text_delta", text: "!" },
        },
        {
          type: "content_block_stop",
          index: 0,
        },
        {
          type: "message_delta",
          delta: { stop_reason: "end_turn", stop_sequence: null },
          usage: { output_tokens: 2 },
        },
        {
          type: "message_stop",
        },
      ];

      // Mock async generator
      async function* mockStreamGenerator() {
        for (const event of mockEvents) {
          yield event;
        }
      }

      mockAnthropicInstance.messages.stream.mockReturnValue(mockStreamGenerator());

      const chunks = [];
      for await (const chunk of provider.chatStream(request)) {
        chunks.push(chunk);
      }

      expect(chunks.length).toBeGreaterThan(0);
      expect(chunks[0].message.content).toBe("Hello");
      expect(chunks[1].message.content).toBe("!");
      expect(chunks[chunks.length - 1].done).toBe(true);

      expect(mockAnthropicInstance.messages.stream).toHaveBeenCalledWith({
        model: "claude-opus-4-20250514",
        max_tokens: 1024,
        messages: [{ role: "user", content: "Hello" }],
      });
    });

    it("should pass options in streaming mode", async () => {
      const request: ChatRequestDto = {
        model: "claude-opus-4-20250514",
        messages: [{ role: "user", content: "Hello" }],
        temperature: 0.5,
        maxTokens: 500,
      };

      async function* mockStreamGenerator() {
        yield {
          type: "content_block_delta",
          delta: { type: "text_delta", text: "Hi" },
        };
        yield {
          type: "message_stop",
        };
      }

      mockAnthropicInstance.messages.stream.mockReturnValue(mockStreamGenerator());

      const generator = provider.chatStream(request);
      await generator.next();

      expect(mockAnthropicInstance.messages.stream).toHaveBeenCalledWith({
        model: "claude-opus-4-20250514",
        max_tokens: 500,
        messages: [{ role: "user", content: "Hello" }],
        temperature: 0.5,
      });
    });

    it("should throw error when streaming fails", async () => {
      const request: ChatRequestDto = {
        model: "claude-opus-4-20250514",
        messages: [{ role: "user", content: "Hello" }],
      };

      mockAnthropicInstance.messages.stream.mockRejectedValue(new Error("Stream error"));

      const generator = provider.chatStream(request);

      await expect(generator.next()).rejects.toThrow("Streaming failed");
    });
  });

  describe("embed", () => {
    it("should throw error indicating embeddings not supported", async () => {
      const request: EmbedRequestDto = {
        model: "claude-opus-4-20250514",
        input: ["Hello world", "Test embedding"],
      };

      await expect(provider.embed(request)).rejects.toThrow(
        "Claude provider does not support embeddings"
      );
    });
  });

  describe("getConfig", () => {
    it("should return copy of configuration", () => {
      const returnedConfig = provider.getConfig();

      expect(returnedConfig).toEqual(config);
      expect(returnedConfig).not.toBe(config); // Should be a copy, not reference
    });

    it("should prevent external modification of config", () => {
      const returnedConfig = provider.getConfig();
      returnedConfig.apiKey = "sk-ant-modified-key";

      const secondCall = provider.getConfig();
      expect(secondCall.apiKey).toBe("sk-ant-test-1234567890"); // Original unchanged
    });

    it("should not expose API key in logs", () => {
      const returnedConfig = provider.getConfig();

      // API key should be present in config
      expect(returnedConfig.apiKey).toBeDefined();
      expect(returnedConfig.apiKey.length).toBeGreaterThan(0);
    });
  });
});
