import { describe, it, expect, beforeEach, vi, type Mock } from "vitest";
import { OpenAiProvider, type OpenAiProviderConfig } from "./openai.provider";
import type { ChatRequestDto, EmbedRequestDto } from "../dto";

// Mock the openai module
vi.mock("openai", () => {
  return {
    default: vi.fn().mockImplementation(function (this: unknown) {
      return {
        chat: {
          completions: {
            create: vi.fn(),
          },
        },
        embeddings: {
          create: vi.fn(),
        },
        models: {
          list: vi.fn(),
        },
      };
    }),
  };
});

describe("OpenAiProvider", () => {
  let provider: OpenAiProvider;
  let config: OpenAiProviderConfig;
  let mockOpenAiInstance: {
    chat: { completions: { create: Mock } };
    embeddings: { create: Mock };
    models: { list: Mock };
  };

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

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

    provider = new OpenAiProvider(config);

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

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

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

    it("should support organization ID in config", () => {
      const configWithOrg: OpenAiProviderConfig = {
        endpoint: "https://api.openai.com/v1",
        apiKey: "sk-test-1234567890",
        organization: "org-test123",
      };

      const providerWithOrg = new OpenAiProvider(configWithOrg);
      const returnedConfig = providerWithOrg.getConfig();

      expect(returnedConfig.organization).toBe("org-test123");
    });
  });

  describe("checkHealth", () => {
    it("should return healthy status when OpenAI is reachable", async () => {
      const mockModels = {
        data: [{ id: "gpt-4" }, { id: "gpt-3.5-turbo" }, { id: "text-embedding-ada-002" }],
      };
      mockOpenAiInstance.models.list.mockResolvedValue(mockModels);

      const health = await provider.checkHealth();

      expect(health).toEqual({
        healthy: true,
        provider: "openai",
        endpoint: config.endpoint,
        models: ["gpt-4", "gpt-3.5-turbo", "text-embedding-ada-002"],
      });
      expect(mockOpenAiInstance.models.list).toHaveBeenCalledOnce();
    });

    it("should return unhealthy status when OpenAI is unreachable", async () => {
      const error = new Error("API key invalid");
      mockOpenAiInstance.models.list.mockRejectedValue(error);

      const health = await provider.checkHealth();

      expect(health).toEqual({
        healthy: false,
        provider: "openai",
        endpoint: config.endpoint,
        error: "API key invalid",
      });
    });

    it("should handle non-Error exceptions", async () => {
      mockOpenAiInstance.models.list.mockRejectedValue("string error");

      const health = await provider.checkHealth();

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

  describe("listModels", () => {
    it("should return array of model names", async () => {
      const mockModels = {
        data: [{ id: "gpt-4" }, { id: "gpt-3.5-turbo" }, { id: "gpt-4-turbo" }],
      };
      mockOpenAiInstance.models.list.mockResolvedValue(mockModels);

      const models = await provider.listModels();

      expect(models).toEqual(["gpt-4", "gpt-3.5-turbo", "gpt-4-turbo"]);
      expect(mockOpenAiInstance.models.list).toHaveBeenCalledOnce();
    });

    it("should throw error when listing models fails", async () => {
      const error = new Error("Failed to connect");
      mockOpenAiInstance.models.list.mockRejectedValue(error);

      await expect(provider.listModels()).rejects.toThrow("Failed to list models");
    });
  });

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

      const mockResponse = {
        id: "chatcmpl-123",
        object: "chat.completion",
        created: 1677652288,
        model: "gpt-4",
        choices: [
          {
            index: 0,
            message: {
              role: "assistant",
              content: "Hello! How can I assist you today?",
            },
            finish_reason: "stop",
          },
        ],
        usage: {
          prompt_tokens: 10,
          completion_tokens: 8,
          total_tokens: 18,
        },
      };

      mockOpenAiInstance.chat.completions.create.mockResolvedValue(mockResponse);

      const response = await provider.chat(request);

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

      expect(mockOpenAiInstance.chat.completions.create).toHaveBeenCalledWith({
        model: "gpt-4",
        messages: [{ role: "user", content: "Hello" }],
        stream: false,
      });
    });

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

      mockOpenAiInstance.chat.completions.create.mockResolvedValue({
        model: "gpt-4",
        choices: [
          {
            message: { role: "assistant", content: "Hi!" },
            finish_reason: "stop",
          },
        ],
        usage: { prompt_tokens: 15, completion_tokens: 2, total_tokens: 17 },
      });

      await provider.chat(request);

      expect(mockOpenAiInstance.chat.completions.create).toHaveBeenCalledWith({
        model: "gpt-4",
        messages: [
          { role: "system", content: "You are a helpful assistant" },
          { role: "user", content: "Hello" },
        ],
        stream: false,
      });
    });

    it("should not duplicate system prompt when already in messages", async () => {
      const request: ChatRequestDto = {
        model: "gpt-4",
        messages: [
          { role: "system", content: "Existing system prompt" },
          { role: "user", content: "Hello" },
        ],
        systemPrompt: "New system prompt (should be ignored)",
      };

      mockOpenAiInstance.chat.completions.create.mockResolvedValue({
        model: "gpt-4",
        choices: [
          {
            message: { role: "assistant", content: "Hi!" },
            finish_reason: "stop",
          },
        ],
        usage: { prompt_tokens: 15, completion_tokens: 2, total_tokens: 17 },
      });

      await provider.chat(request);

      expect(mockOpenAiInstance.chat.completions.create).toHaveBeenCalledWith({
        model: "gpt-4",
        messages: [
          { role: "system", content: "Existing system prompt" },
          { role: "user", content: "Hello" },
        ],
        stream: false,
      });
    });

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

      mockOpenAiInstance.chat.completions.create.mockResolvedValue({
        model: "gpt-4",
        choices: [
          {
            message: { role: "assistant", content: "Hi!" },
            finish_reason: "stop",
          },
        ],
        usage: { prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 },
      });

      await provider.chat(request);

      expect(mockOpenAiInstance.chat.completions.create).toHaveBeenCalledWith({
        model: "gpt-4",
        messages: [{ role: "user", content: "Hello" }],
        stream: false,
        temperature: 0.7,
        max_tokens: 100,
      });
    });

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

      mockOpenAiInstance.chat.completions.create.mockRejectedValue(
        new Error("Model not available")
      );

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

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

      const mockChunks = [
        {
          id: "chatcmpl-123",
          object: "chat.completion.chunk",
          created: 1677652288,
          model: "gpt-4",
          choices: [
            {
              index: 0,
              delta: { role: "assistant", content: "Hello" },
              finish_reason: null,
            },
          ],
        },
        {
          id: "chatcmpl-123",
          object: "chat.completion.chunk",
          created: 1677652288,
          model: "gpt-4",
          choices: [
            {
              index: 0,
              delta: { content: "!" },
              finish_reason: null,
            },
          ],
        },
        {
          id: "chatcmpl-123",
          object: "chat.completion.chunk",
          created: 1677652288,
          model: "gpt-4",
          choices: [
            {
              index: 0,
              delta: {},
              finish_reason: "stop",
            },
          ],
        },
      ];

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

      mockOpenAiInstance.chat.completions.create.mockResolvedValue(mockStreamGenerator());

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

      expect(chunks).toHaveLength(3);
      expect(chunks[0]).toEqual({
        model: "gpt-4",
        message: { role: "assistant", content: "Hello" },
        done: false,
      });
      expect(chunks[1]).toEqual({
        model: "gpt-4",
        message: { role: "assistant", content: "!" },
        done: false,
      });
      expect(chunks[2].done).toBe(true);

      expect(mockOpenAiInstance.chat.completions.create).toHaveBeenCalledWith({
        model: "gpt-4",
        messages: [{ role: "user", content: "Hello" }],
        stream: true,
      });
    });

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

      async function* mockStreamGenerator() {
        yield {
          model: "gpt-4",
          choices: [{ delta: { role: "assistant", content: "Hi" }, finish_reason: "stop" }],
        };
      }

      mockOpenAiInstance.chat.completions.create.mockResolvedValue(mockStreamGenerator());

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

      expect(mockOpenAiInstance.chat.completions.create).toHaveBeenCalledWith({
        model: "gpt-4",
        messages: [{ role: "user", content: "Hello" }],
        stream: true,
        temperature: 0.5,
        max_tokens: 50,
      });
    });

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

      mockOpenAiInstance.chat.completions.create.mockRejectedValue(new Error("Stream error"));

      const generator = provider.chatStream(request);

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

  describe("embed", () => {
    it("should generate embeddings successfully", async () => {
      const request: EmbedRequestDto = {
        model: "text-embedding-ada-002",
        input: ["Hello world", "Test embedding"],
      };

      const mockResponse = {
        object: "list",
        data: [
          {
            object: "embedding",
            index: 0,
            embedding: [0.1, 0.2, 0.3],
          },
          {
            object: "embedding",
            index: 1,
            embedding: [0.4, 0.5, 0.6],
          },
        ],
        model: "text-embedding-ada-002",
        usage: {
          prompt_tokens: 10,
          total_tokens: 10,
        },
      };

      mockOpenAiInstance.embeddings.create.mockResolvedValue(mockResponse);

      const response = await provider.embed(request);

      expect(response).toEqual({
        model: "text-embedding-ada-002",
        embeddings: [
          [0.1, 0.2, 0.3],
          [0.4, 0.5, 0.6],
        ],
      });

      expect(mockOpenAiInstance.embeddings.create).toHaveBeenCalledWith({
        model: "text-embedding-ada-002",
        input: ["Hello world", "Test embedding"],
      });
    });

    it("should handle single string input", async () => {
      const request: EmbedRequestDto = {
        model: "text-embedding-ada-002",
        input: ["Single text"],
      };

      mockOpenAiInstance.embeddings.create.mockResolvedValue({
        data: [{ embedding: [0.1, 0.2] }],
        model: "text-embedding-ada-002",
        usage: { prompt_tokens: 5, total_tokens: 5 },
      });

      await provider.embed(request);

      expect(mockOpenAiInstance.embeddings.create).toHaveBeenCalledWith({
        model: "text-embedding-ada-002",
        input: ["Single text"],
      });
    });

    it("should throw error when embedding fails", async () => {
      const request: EmbedRequestDto = {
        model: "text-embedding-ada-002",
        input: ["Test"],
      };

      mockOpenAiInstance.embeddings.create.mockRejectedValue(new Error("Embedding error"));

      await expect(provider.embed(request)).rejects.toThrow("Embedding failed");
    });
  });

  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-modified-key";

      const secondCall = provider.getConfig();
      expect(secondCall.apiKey).toBe("sk-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);
    });
  });
});
