import { describe, it, expect, beforeEach, vi } from "vitest";
import {
  parseAIProviders,
  isProviderAvailable,
  analyzeImageWithProvider,
  getDefaultModelForProvider,
  determineProviderAndModel,
} from "../../../src/utils/ai-providers";
import { AIProvider } from "../../../src/types";
import OpenAI from "openai";

const mockLogger = {
  info: vi.fn(),
  error: vi.fn(),
  debug: vi.fn(),
  warn: vi.fn(),
} as any;

global.fetch = vi.fn();

// Centralized mock for OpenAI().chat.completions.create
const mockChatCompletionsCreate = vi.fn();

vi.mock("openai", () => {
  const MockedOpenAI = vi.fn().mockImplementation(() => {
    return {
      chat: {
        completions: {
          create: mockChatCompletionsCreate,
        },
      },
    };
  });
  return { default: MockedOpenAI }; // Simulate default export
});
// No need for `let mockOpenAICreate` outside, use mockChatCompletionsCreate directly.

describe("AI Providers Utility", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    delete process.env.PEEKABOO_OLLAMA_BASE_URL;
    delete process.env.OPENAI_API_KEY;
    delete process.env.ANTHROPIC_API_KEY;
    (global.fetch as vi.Mock).mockReset();
    mockChatCompletionsCreate.mockReset(); // Reset the shared mock function
  });

  describe("parseAIProviders", () => {
    it("should return empty array for empty or whitespace string", () => {
      expect(parseAIProviders("")).toEqual([]);
      expect(parseAIProviders("   ")).toEqual([]);
    });

    it("should parse a single provider string", () => {
      expect(parseAIProviders("ollama/llava")).toEqual([
        { provider: "ollama", model: "llava" },
      ]);
    });

    it("should parse multiple comma-separated providers", () => {
      const expected: AIProvider[] = [
        { provider: "ollama", model: "llava" },
        { provider: "openai", model: "gpt-4o" },
      ];
      expect(parseAIProviders("ollama/llava, openai/gpt-4o")).toEqual(expected);
    });

    it("should handle extra whitespace", () => {
      expect(parseAIProviders("  ollama/llava ,  openai/gpt-4o  ")).toEqual([
        { provider: "ollama", model: "llava" },
        { provider: "openai", model: "gpt-4o" },
      ]);
    });

    it("should filter out entries without a model or provider name", () => {
      expect(
        parseAIProviders("ollama/, /gpt-4o, openai/llama3, incomplete"),
      ).toEqual([{ provider: "openai", model: "llama3" }]);
    });
    it("should filter out entries with only provider or only model or no slash or empty parts", () => {
      expect(parseAIProviders("ollama/")).toEqual([]);
      expect(parseAIProviders("/gpt-4o")).toEqual([]);
      expect(parseAIProviders("ollama")).toEqual([]);
      expect(parseAIProviders("ollama/,,openai/gpt4")).toEqual([
        { provider: "openai", model: "gpt4" },
      ]);
    });
  });

  describe("isProviderAvailable", () => {
    it("should return true for available Ollama (fetch ok)", async () => {
      (global.fetch as vi.Mock).mockResolvedValue({ ok: true });
      const result = await isProviderAvailable(
        { provider: "ollama", model: "llava" },
        mockLogger,
      );
      expect(result).toBe(true);
      expect(global.fetch).toHaveBeenCalledWith(
        "http://localhost:11434/api/tags",
      );
    });

    it("should use PEEKABOO_OLLAMA_BASE_URL for Ollama check", async () => {
      process.env.PEEKABOO_OLLAMA_BASE_URL = "http://custom-ollama:11434";
      (global.fetch as vi.Mock).mockResolvedValue({ ok: true });
      await isProviderAvailable(
        { provider: "ollama", model: "llava" },
        mockLogger,
      );
      expect(global.fetch).toHaveBeenCalledWith(
        "http://custom-ollama:11434/api/tags",
      );
    });

    it("should return false for unavailable Ollama (fetch fails)", async () => {
      (global.fetch as vi.Mock).mockRejectedValue(new Error("Network Error"));
      const result = await isProviderAvailable(
        { provider: "ollama", model: "llava" },
        mockLogger,
      );
      expect(result).toBe(false);
      expect(mockLogger.debug).toHaveBeenCalledWith(
        { error: new Error("Network Error") },
        "Ollama not available",
      );
    });

    it("should return false for unavailable Ollama (response not ok)", async () => {
      (global.fetch as vi.Mock).mockResolvedValue({ ok: false });
      const result = await isProviderAvailable(
        { provider: "ollama", model: "llava" },
        mockLogger,
      );
      expect(result).toBe(false);
    });

    it("should return true for available OpenAI (API key set)", async () => {
      process.env.OPENAI_API_KEY = "test-key";
      const result = await isProviderAvailable(
        { provider: "openai", model: "gpt-4o" },
        mockLogger,
      );
      expect(result).toBe(true);
    });

    it("should return false for unavailable OpenAI (API key not set)", async () => {
      const result = await isProviderAvailable(
        { provider: "openai", model: "gpt-4o" },
        mockLogger,
      );
      expect(result).toBe(false);
    });

    it("should return true for available Anthropic (API key set)", async () => {
      process.env.ANTHROPIC_API_KEY = "test-key";
      const result = await isProviderAvailable(
        { provider: "anthropic", model: "claude-3" },
        mockLogger,
      );
      expect(result).toBe(true);
    });

    it("should return false for unavailable Anthropic (API key not set)", async () => {
      const result = await isProviderAvailable(
        { provider: "anthropic", model: "claude-3" },
        mockLogger,
      );
      expect(result).toBe(false);
    });

    it("should return false and log warning for unknown provider", async () => {
      const result = await isProviderAvailable(
        { provider: "unknown", model: "test" },
        mockLogger,
      );
      expect(result).toBe(false);
      expect(mockLogger.warn).toHaveBeenCalledWith(
        { provider: "unknown" },
        "Unknown AI provider",
      );
    });

    it("should handle errors during ollama availability check gracefully (fetch throws)", async () => {
      const fetchError = new Error("Unexpected fetch error");
      (global.fetch as vi.Mock).mockImplementationOnce(() => {
        // Ensure this mock is specific to the ollama check path that uses fetch
        if (
          (global.fetch as vi.Mock).mock.calls.some((call) =>
            call[0].includes("/api/tags"),
          )
        ) {
          throw fetchError;
        }
        // Fallback for other fetches if any, though not expected in this test path
        return Promise.resolve({ ok: true, json: async () => ({}) });
      });
      const result = await isProviderAvailable(
        { provider: "ollama", model: "llava" },
        mockLogger,
      );
      expect(result).toBe(false);
      expect(mockLogger.debug).toHaveBeenCalledWith(
        { error: fetchError },
        "Ollama not available",
      );
      expect(mockLogger.error).not.toHaveBeenCalledWith(
        expect.objectContaining({ error: fetchError, provider: "ollama" }),
        "Error checking provider availability",
      );
    });
  });

  describe("analyzeImageWithProvider", () => {
    const imageBase64 = "test-base64-image";
    const question = "What is this?";

    it("should call analyzeWithOllama for ollama provider", async () => {
      (global.fetch as vi.Mock).mockResolvedValueOnce({
        ok: true,
        json: async () => ({ response: "Ollama says hello" }),
      });
      const result = await analyzeImageWithProvider(
        { provider: "ollama", model: "llava" },
        "path/img.png",
        imageBase64,
        question,
        mockLogger,
      );
      expect(result).toBe("Ollama says hello");
      expect(global.fetch).toHaveBeenCalledWith(
        "http://localhost:11434/api/generate",
        expect.any(Object),
      );
      expect(
        JSON.parse((global.fetch as vi.Mock).mock.calls[0][1].body),
      ).toEqual(
        expect.objectContaining({
          model: "llava",
          prompt: question,
          images: [imageBase64],
        }),
      );
    });

    it("should throw Ollama API error if response not ok", async () => {
      (global.fetch as vi.Mock).mockResolvedValueOnce({
        ok: false,
        status: 500,
        text: async () => "Internal Server Error",
      });
      await expect(
        analyzeImageWithProvider(
          { provider: "ollama", model: "llava" },
          "path/img.png",
          imageBase64,
          question,
          mockLogger,
        ),
      ).rejects.toThrow("Ollama API error: 500 - Internal Server Error");
    });

    it("should call analyzeWithOpenAI for openai provider", async () => {
      process.env.OPENAI_API_KEY = "test-key";
      mockChatCompletionsCreate.mockResolvedValueOnce({
        choices: [{ message: { content: "OpenAI says hello" } }],
      });

      const result = await analyzeImageWithProvider(
        { provider: "openai", model: "gpt-4o" },
        "path/img.png",
        imageBase64,
        question,
        mockLogger,
      );
      expect(result).toBe("OpenAI says hello");
      expect(mockChatCompletionsCreate).toHaveBeenCalledWith(
        expect.objectContaining({
          model: "gpt-4o",
          messages: expect.arrayContaining([
            expect.objectContaining({
              role: "user",
              content: expect.arrayContaining([
                { type: "text", text: question },
                {
                  type: "image_url",
                  image_url: { url: `data:image/jpeg;base64,${imageBase64}` },
                },
              ]),
            }),
          ]),
        }),
      );
    });

    it("should throw error if OpenAI API key is missing for openai provider", async () => {
      await expect(
        analyzeImageWithProvider(
          { provider: "openai", model: "gpt-4o" },
          "path/img.png",
          imageBase64,
          question,
          mockLogger,
        ),
      ).rejects.toThrow("OpenAI API key not configured");
    });

    it("should return default message if OpenAI provides no response content", async () => {
      process.env.OPENAI_API_KEY = "test-key";
      mockChatCompletionsCreate.mockResolvedValueOnce({
        choices: [{ message: { content: null } }],
      });

      const result = await analyzeImageWithProvider(
        { provider: "openai", model: "gpt-4o" },
        "path/img.png",
        imageBase64,
        question,
        mockLogger,
      );
      expect(result).toBe("No response from OpenAI");
    });

    it("should return default message if Ollama provides no response content", async () => {
      (global.fetch as vi.Mock).mockResolvedValueOnce({
        ok: true,
        json: async () => ({ response: null }),
      });
      const result = await analyzeImageWithProvider(
        { provider: "ollama", model: "llava" },
        "path/img.png",
        imageBase64,
        question,
        mockLogger,
      );
      expect(result).toBe("No response from Ollama");
    });

    it("should throw error for anthropic provider (not implemented)", async () => {
      await expect(
        analyzeImageWithProvider(
          { provider: "anthropic", model: "claude-3" },
          "path/img.png",
          imageBase64,
          question,
          mockLogger,
        ),
      ).rejects.toThrow("Anthropic support not yet implemented");
    });

    it("should throw error for unsupported provider", async () => {
      await expect(
        analyzeImageWithProvider(
          { provider: "unknown", model: "test" },
          "path/img.png",
          imageBase64,
          question,
          mockLogger,
        ),
      ).rejects.toThrow("Unsupported AI provider: unknown");
    });
  });

  describe("getDefaultModelForProvider", () => {
    it("should return correct default models", () => {
      expect(getDefaultModelForProvider("ollama")).toBe("llava:latest");
      expect(getDefaultModelForProvider("openai")).toBe("gpt-4o");
      expect(getDefaultModelForProvider("anthropic")).toBe(
        "claude-3-sonnet-20240229",
      );
      expect(getDefaultModelForProvider("unknown")).toBe("unknown");
    });
  });

  describe("determineProviderAndModel", () => {
    let configuredProviders: AIProvider[];

    beforeEach(() => {
      configuredProviders = [
        { provider: "ollama", model: "llava:custom" },
        { provider: "openai", model: "gpt-4o-mini" },
      ];
    });

    it("should select a specifically requested and available provider", async () => {
      process.env.OPENAI_API_KEY = "test-key";
      const result = await determineProviderAndModel(
        { type: "openai" },
        configuredProviders,
        mockLogger,
      );
      expect(result.provider).toBe("openai");
      expect(result.model).toBe("gpt-4o-mini");
    });

    it("should use a requested model over the configured default", async () => {
      process.env.OPENAI_API_KEY = "test-key";
      const result = await determineProviderAndModel(
        { type: "openai", model: "gpt-4-turbo" },
        configuredProviders,
        mockLogger,
      );
      expect(result.provider).toBe("openai");
      expect(result.model).toBe("gpt-4-turbo");
    });

    it("should throw if requested provider is not configured", async () => {
      await expect(
        determineProviderAndModel(
          { type: "anthropic" },
          configuredProviders,
          mockLogger,
        ),
      ).rejects.toThrow(
        "Provider 'anthropic' is not enabled in server's PEEKABOO_AI_PROVIDERS configuration.",
      );
    });

    it("should throw if requested provider is not available", async () => {
      // OPENAI_API_KEY is not set
      await expect(
        determineProviderAndModel(
          { type: "openai" },
          configuredProviders,
          mockLogger,
        ),
      ).rejects.toThrow(
        "Provider 'openai' is configured but not currently available.",
      );
    });

    it("should auto-select the first available provider", async () => {
      (global.fetch as vi.Mock).mockResolvedValue({ ok: true }); // Ollama is available
      process.env.OPENAI_API_KEY = "test-key"; // OpenAI is also available

      const result = await determineProviderAndModel(
        undefined, // auto mode
        configuredProviders,
        mockLogger,
      );

      // Should pick the first one in the list: Ollama
      expect(result.provider).toBe("ollama");
      expect(result.model).toBe("llava:custom");
    });

    it("should fall back to the next available provider in auto mode", async () => {
      (global.fetch as vi.Mock).mockResolvedValue({ ok: false }); // Ollama is NOT available
      process.env.OPENAI_API_KEY = "test-key"; // OpenAI IS available

      const result = await determineProviderAndModel(
        undefined, // auto mode
        configuredProviders,
        mockLogger,
      );

      expect(result.provider).toBe("openai");
      expect(result.model).toBe("gpt-4o-mini");
    });

    it("should return null if no providers are available in auto mode", async () => {
      (global.fetch as vi.Mock).mockResolvedValue({ ok: false }); // Ollama is NOT available
      // OPENAI_API_KEY is not set

      const result = await determineProviderAndModel(
        undefined, // auto mode
        configuredProviders,
        mockLogger,
      );

      expect(result.provider).toBeNull();
      expect(result.model).toBe("");
    });
  });
});
