"""Tests for additional LLM providers (OpenAI, Anthropic, OpenVINO)."""

from __future__ import annotations

from collections.abc import AsyncIterator
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from redhat_oracle.core.llm.external import (
    AnthropicLLM,
    OpenAILLM,
    ProviderUnavailableError,
)
from redhat_oracle.core.llm.openvino import OpenVINOLLM, OpenVINOUnavailableError

# --- OpenAI tests ---


class TestOpenAILLM:
    @pytest.mark.asyncio
    async def test_stream_tokens(self) -> None:
        mock_chunk1 = MagicMock()
        mock_chunk1.choices = [MagicMock()]
        mock_chunk1.choices[0].delta.content = "Hello"

        mock_chunk2 = MagicMock()
        mock_chunk2.choices = [MagicMock()]
        mock_chunk2.choices[0].delta.content = " world"

        mock_chunk3 = MagicMock()
        mock_chunk3.choices = [MagicMock()]
        mock_chunk3.choices[0].delta.content = None

        async def fake_chunks() -> AsyncIterator[Any]:
            yield mock_chunk1
            yield mock_chunk2
            yield mock_chunk3

        mock_client = AsyncMock()
        mock_client.chat.completions.create = AsyncMock(return_value=fake_chunks())

        with patch("redhat_oracle.core.llm.external.OpenAILLM.stream") as mock_stream:
            # Directly test the streaming behavior
            llm = OpenAILLM(api_key="sk-test")

            async def fake_stream(
                prompt: str, system: str | None = None
            ) -> AsyncIterator[str]:
                yield "Hello"
                yield " world"

            mock_stream.side_effect = fake_stream
            tokens: list[str] = []
            async for token in llm.stream("test prompt"):
                tokens.append(token)

        assert tokens == ["Hello", " world"]

    @pytest.mark.asyncio
    async def test_generate_concatenates(self) -> None:
        llm = OpenAILLM(api_key="sk-test")

        with patch.object(llm, "stream") as mock_stream:

            async def fake_stream(
                prompt: str, system: str | None = None
            ) -> AsyncIterator[str]:
                yield "Hello"
                yield " world"

            mock_stream.side_effect = fake_stream
            result = await llm.generate("test")

        assert result == "Hello world"

    @pytest.mark.asyncio
    async def test_unavailable_without_key(self) -> None:
        llm = OpenAILLM(api_key="invalid-key")

        # openai SDK raises on invalid key during models.list()
        with patch.object(
            llm,
            "_check_available",
            new_callable=AsyncMock,
            side_effect=ProviderUnavailableError("OpenAI not available"),
        ), pytest.raises(ProviderUnavailableError):
            await llm._check_available()


# --- Anthropic tests ---


class TestAnthropicLLM:
    @pytest.mark.asyncio
    async def test_stream_tokens(self) -> None:
        llm = AnthropicLLM(api_key="sk-ant-test")

        with patch.object(llm, "stream") as mock_stream:

            async def fake_stream(
                prompt: str, system: str | None = None
            ) -> AsyncIterator[str]:
                yield "Hello"
                yield " from"
                yield " Claude"

            mock_stream.side_effect = fake_stream
            tokens: list[str] = []
            async for token in llm.stream("test prompt"):
                tokens.append(token)

        assert tokens == ["Hello", " from", " Claude"]

    @pytest.mark.asyncio
    async def test_generate_concatenates(self) -> None:
        llm = AnthropicLLM(api_key="sk-ant-test")

        with patch.object(llm, "stream") as mock_stream:

            async def fake_stream(
                prompt: str, system: str | None = None
            ) -> AsyncIterator[str]:
                yield "Response"

            mock_stream.side_effect = fake_stream
            result = await llm.generate("test")

        assert result == "Response"

    @pytest.mark.asyncio
    async def test_unavailable_raises(self) -> None:
        llm = AnthropicLLM(api_key="invalid-key")

        with patch.object(
            llm,
            "_check_available",
            new_callable=AsyncMock,
            side_effect=ProviderUnavailableError("Anthropic not available"),
        ), pytest.raises(ProviderUnavailableError):
            await llm._check_available()


# --- OpenVINO tests ---


class TestOpenVINOLLM:
    @pytest.mark.asyncio
    async def test_check_available_without_package(self) -> None:
        llm = OpenVINOLLM()

        with (
            patch.dict("sys.modules", {"optimum.intel": None}),
            patch(
                "builtins.__import__",
                side_effect=ImportError("No module named 'optimum.intel'"),
            ),pytest.raises(OpenVINOUnavailableError)
        ):
            await llm._check_available()

    @pytest.mark.asyncio
    async def test_generate_uses_model(self) -> None:
        llm = OpenVINOLLM()

        # Mock tokenizer and model
        mock_tokenizer = MagicMock()
        mock_tokenizer.return_value = {"input_ids": MagicMock(shape=(1, 5))}
        mock_tokenizer.decode.return_value = "Generated response"

        import torch

        mock_model = MagicMock()
        mock_model.generate.return_value = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8]])

        llm._model = mock_model
        llm._tokenizer = mock_tokenizer

        result = await llm.generate("test prompt")
        assert result == "Generated response"

    @pytest.mark.asyncio
    async def test_stream_yields_words(self) -> None:
        llm = OpenVINOLLM()

        with patch.object(llm, "generate", new_callable=AsyncMock) as mock_gen:
            mock_gen.return_value = "word1 word2 word3"
            tokens: list[str] = []
            async for token in llm.stream("test"):
                tokens.append(token)

        assert "".join(tokens) == "word1 word2 word3"

    @pytest.mark.asyncio
    async def test_system_prompt_prepended(self) -> None:
        llm = OpenVINOLLM()

        mock_tokenizer = MagicMock()
        mock_tokenizer.return_value = {"input_ids": MagicMock(shape=(1, 10))}
        mock_tokenizer.decode.return_value = "answer"

        import torch

        mock_model = MagicMock()
        mock_model.generate.return_value = torch.tensor(
            [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]]
        )

        llm._model = mock_model
        llm._tokenizer = mock_tokenizer

        await llm.generate("question", system="You are helpful")

        call_args = mock_tokenizer.call_args[0][0]
        assert "You are helpful" in call_args
        assert "question" in call_args


# --- CLI provider selection tests ---


class TestCLIProviderSelection:
    def test_openai_provider_needs_key(self) -> None:
        from click.testing import CliRunner

        from redhat_oracle.cli.app import main
        from redhat_oracle.core.config import OracleConfig

        runner = CliRunner()
        with patch("redhat_oracle.cli.app.load_config") as mock_config:
            mock_config.return_value = OracleConfig()  # no API key set
            result = runner.invoke(
                main, ["chat", "--provider", "openai"], input="exit\n"
            )

        assert result.exit_code == 1
        assert "API key not configured" in result.output

    def test_anthropic_provider_needs_key(self) -> None:
        from click.testing import CliRunner

        from redhat_oracle.cli.app import main
        from redhat_oracle.core.config import OracleConfig

        runner = CliRunner()
        with patch("redhat_oracle.cli.app.load_config") as mock_config:
            mock_config.return_value = OracleConfig()
            result = runner.invoke(
                main, ["chat", "--provider", "anthropic"], input="exit\n"
            )

        assert result.exit_code == 1
        assert "API key not configured" in result.output

    def test_unknown_provider_rejected(self) -> None:
        from click.testing import CliRunner

        from redhat_oracle.cli.app import main
        from redhat_oracle.core.config import OracleConfig

        runner = CliRunner()
        with patch("redhat_oracle.cli.app.load_config") as mock_config:
            mock_config.return_value = OracleConfig()
            result = runner.invoke(
                main, ["chat", "--provider", "fakemodel"], input="exit\n"
            )

        assert result.exit_code == 1
        assert "Unknown provider" in result.output
