"""Unit tests for evalkit.judges."""

from __future__ import annotations

import os
import sys
from unittest.mock import MagicMock, patch

import pytest

from evalkit.judges.base import BaseJudge
from evalkit.models import JudgeResponse
from tests.conftest import MockJudge


class TestMockJudge:
    def test_returns_judge_response(self):
        judge = MockJudge()
        result = judge.evaluate("some prompt")
        assert isinstance(result, JudgeResponse)

    def test_response_is_configurable(self):
        judge = MockJudge(score=0.5, reason="Mediocre")
        result = judge.evaluate("prompt")
        assert result.score == 0.5
        assert result.reason == "Mediocre"

    def test_call_count_increments(self):
        judge = MockJudge()
        assert judge.call_count == 0
        judge.evaluate("p1")
        assert judge.call_count == 1
        judge.evaluate("p2")
        assert judge.call_count == 2

    def test_last_prompt_is_recorded(self):
        judge = MockJudge()
        assert judge.last_prompt is None
        judge.evaluate("hello world")
        assert judge.last_prompt == "hello world"

    def test_default_response(self):
        judge = MockJudge()
        result = judge.evaluate("anything")
        assert result.score == 0.8
        assert result.reason == "Good answer"

    def test_is_base_judge_subclass(self):
        assert isinstance(MockJudge(), BaseJudge)


class TestBaseJudgeAsync:
    @pytest.mark.asyncio
    async def test_evaluate_async_runs_evaluate_in_thread(self):
        judge = MockJudge()
        result = await judge.evaluate_async("async prompt")
        assert isinstance(result, JudgeResponse)
        assert judge.call_count == 1
        assert judge.last_prompt == "async prompt"

    @pytest.mark.asyncio
    async def test_evaluate_async_returns_correct_values(self):
        judge = MockJudge(score=1.0, reason="Perfect")
        result = await judge.evaluate_async("test")
        assert result.score == 1.0
        assert result.reason == "Perfect"

    @pytest.mark.asyncio
    async def test_evaluate_async_multiple_calls(self):
        judge = MockJudge()
        await judge.evaluate_async("first")
        await judge.evaluate_async("second")
        assert judge.call_count == 2
        assert judge.last_prompt == "second"


class TestOpenAIJudgeStructuredOutput:
    def test_evaluate_returns_judge_response(self):
        from evalkit.judges.openai import OpenAIJudge

        judge = OpenAIJudge.__new__(OpenAIJudge)
        judge.model = "gpt-4o-mini"
        judge.api_key = "test-key"
        judge.temperature = 0.0
        judge.max_tokens = 1024
        judge.timeout = 60.0

        mock_parsed = JudgeResponse(score=0.9, reason="Well supported")
        mock_message = MagicMock()
        mock_message.parsed = mock_parsed
        mock_choice = MagicMock()
        mock_choice.message = mock_message
        mock_completion = MagicMock()
        mock_completion.choices = [mock_choice]

        with patch("openai.OpenAI") as mock_openai_cls:
            mock_client = MagicMock()
            mock_client.chat.completions.parse.return_value = mock_completion
            mock_openai_cls.return_value = mock_client

            result = judge.evaluate("test prompt")

        assert isinstance(result, JudgeResponse)
        assert result.score == 0.9
        assert result.reason == "Well supported"

    def test_evaluate_passes_response_format(self):
        from evalkit.judges.openai import OpenAIJudge

        judge = OpenAIJudge.__new__(OpenAIJudge)
        judge.model = "gpt-4o-mini"
        judge.api_key = "test-key"
        judge.temperature = 0.0
        judge.max_tokens = 1024
        judge.timeout = 60.0

        mock_parsed = JudgeResponse(score=0.7, reason="ok")
        mock_message = MagicMock()
        mock_message.parsed = mock_parsed
        mock_choice = MagicMock()
        mock_choice.message = mock_message
        mock_completion = MagicMock()
        mock_completion.choices = [mock_choice]

        with patch("openai.OpenAI") as mock_openai_cls:
            mock_client = MagicMock()
            mock_client.chat.completions.parse.return_value = mock_completion
            mock_openai_cls.return_value = mock_client

            judge.evaluate("test")

            call_kwargs = mock_client.chat.completions.parse.call_args[1]
            assert call_kwargs["response_format"] is JudgeResponse


class TestAzureOpenAIJudgeStructuredOutput:
    def test_evaluate_returns_judge_response(self):
        from evalkit.judges.openai import AzureOpenAIJudge

        judge = AzureOpenAIJudge.__new__(AzureOpenAIJudge)
        judge.model = "gpt-4o-mini"
        judge.azure_endpoint = "https://test.openai.azure.com"
        judge.api_key = "test-key"
        judge.api_version = "2024-10-21"
        judge.temperature = 0.0
        judge.max_tokens = 1024
        judge.timeout = 60.0

        mock_parsed = JudgeResponse(score=0.88, reason="Azure result")
        mock_message = MagicMock()
        mock_message.parsed = mock_parsed
        mock_choice = MagicMock()
        mock_choice.message = mock_message
        mock_completion = MagicMock()
        mock_completion.choices = [mock_choice]

        with patch("openai.AzureOpenAI") as mock_azure_cls:
            mock_client = MagicMock()
            mock_client.chat.completions.parse.return_value = mock_completion
            mock_azure_cls.return_value = mock_client

            result = judge.evaluate("test prompt")

        assert isinstance(result, JudgeResponse)
        assert result.score == 0.88
        assert result.reason == "Azure result"

    def test_passes_azure_endpoint_and_api_version(self):
        from evalkit.judges.openai import AzureOpenAIJudge

        judge = AzureOpenAIJudge.__new__(AzureOpenAIJudge)
        judge.model = "gpt-4o-mini"
        judge.azure_endpoint = "https://test.openai.azure.com"
        judge.api_key = "test-key"
        judge.api_version = "2024-10-21"
        judge.temperature = 0.0
        judge.max_tokens = 1024
        judge.timeout = 60.0

        mock_parsed = JudgeResponse(score=0.7, reason="ok")
        mock_message = MagicMock()
        mock_message.parsed = mock_parsed
        mock_choice = MagicMock()
        mock_choice.message = mock_message
        mock_completion = MagicMock()
        mock_completion.choices = [mock_choice]

        with patch("openai.AzureOpenAI") as mock_azure_cls:
            mock_client = MagicMock()
            mock_client.chat.completions.parse.return_value = mock_completion
            mock_azure_cls.return_value = mock_client

            judge.evaluate("test")

            init_kwargs = mock_azure_cls.call_args[1]
            assert init_kwargs["azure_endpoint"] == "https://test.openai.azure.com"
            assert init_kwargs["api_version"] == "2024-10-21"
            assert init_kwargs["api_key"] == "test-key"


class TestAnthropicJudgeStructuredOutput:
    def test_evaluate_returns_judge_response(self):
        from evalkit.judges.anthropic import AnthropicJudge

        judge = AnthropicJudge.__new__(AnthropicJudge)
        judge.model = "claude-haiku-4-5-20251001"
        judge.api_key = "test-key"
        judge.max_tokens = 1024
        judge.timeout = 60.0

        mock_parsed = JudgeResponse(score=0.85, reason="Accurate answer")
        mock_response = MagicMock()
        mock_response.parsed_output = mock_parsed

        with patch("anthropic.Anthropic") as mock_anthropic_cls:
            mock_client = MagicMock()
            mock_client.messages.parse.return_value = mock_response
            mock_anthropic_cls.return_value = mock_client

            result = judge.evaluate("test prompt")

        assert isinstance(result, JudgeResponse)
        assert result.score == 0.85
        assert result.reason == "Accurate answer"

    def test_evaluate_passes_output_format(self):
        from evalkit.judges.anthropic import AnthropicJudge

        judge = AnthropicJudge.__new__(AnthropicJudge)
        judge.model = "claude-haiku-4-5-20251001"
        judge.api_key = "test-key"
        judge.max_tokens = 1024
        judge.timeout = 60.0

        mock_parsed = JudgeResponse(score=0.7, reason="ok")
        mock_response = MagicMock()
        mock_response.parsed_output = mock_parsed

        with patch("anthropic.Anthropic") as mock_anthropic_cls:
            mock_client = MagicMock()
            mock_client.messages.parse.return_value = mock_response
            mock_anthropic_cls.return_value = mock_client

            judge.evaluate("test")

            call_kwargs = mock_client.messages.parse.call_args[1]
            assert call_kwargs["output_format"] is JudgeResponse


class TestOllamaJudge:
    def test_evaluate_calls_ollama_client_chat(self, monkeypatch):
        from unittest.mock import MagicMock, patch

        from evalkit.judges.ollama import OllamaJudge

        mock_client_instance = MagicMock()
        mock_response = MagicMock()
        mock_response.message.content = '{"score": 0.85, "reason": "Good answer"}'
        mock_client_instance.chat.return_value = mock_response

        with patch("ollama.Client", return_value=mock_client_instance) as mock_client_cls:
            judge = OllamaJudge(base_url="http://myhost:11434")
            result = judge.evaluate("test prompt")

        mock_client_cls.assert_called_once_with(host="http://myhost:11434", timeout=300)
        call_kwargs = mock_client_instance.chat.call_args
        assert call_kwargs.kwargs["model"] == "gpt-oss"
        assert call_kwargs.kwargs["format"] == JudgeResponse.model_json_schema()
        assert result.score == 0.85
        assert result.reason == "Good answer"

    def test_evaluate_clamps_score_above_1(self):
        from unittest.mock import MagicMock, patch

        from evalkit.judges.ollama import OllamaJudge

        mock_client_instance = MagicMock()
        mock_response = MagicMock()
        mock_response.message.content = '{"score": 1.5, "reason": "overflow"}'
        mock_client_instance.chat.return_value = mock_response

        with patch("ollama.Client", return_value=mock_client_instance):
            judge = OllamaJudge()
            result = judge.evaluate("test")

        assert result.score == 1.0

    def test_evaluate_clamps_score_below_0(self):
        from unittest.mock import MagicMock, patch

        from evalkit.judges.ollama import OllamaJudge

        mock_client_instance = MagicMock()
        mock_response = MagicMock()
        mock_response.message.content = '{"score": -0.3, "reason": "underflow"}'
        mock_client_instance.chat.return_value = mock_response

        with patch("ollama.Client", return_value=mock_client_instance):
            judge = OllamaJudge()
            result = judge.evaluate("test")

        assert result.score == 0.0

    def test_evaluate_wraps_response_error(self):
        from unittest.mock import MagicMock, patch

        from evalkit.judges.ollama import OllamaJudge

        mock_client_instance = MagicMock()
        ResponseError = type("ResponseError", (Exception,), {})
        mock_client_instance.chat.side_effect = ResponseError("model not found")

        with patch("ollama.Client", return_value=mock_client_instance), patch("ollama.ResponseError", ResponseError):
            judge = OllamaJudge()
            with pytest.raises(ConnectionError, match="model not found"):
                judge.evaluate("test")

    def test_evaluate_wraps_connect_error(self):
        from unittest.mock import MagicMock, patch

        from evalkit.judges.ollama import OllamaJudge

        mock_client_instance = MagicMock()
        error = type("ConnectError", (Exception,), {})("connection refused")
        mock_client_instance.chat.side_effect = error

        with patch("ollama.Client", return_value=mock_client_instance):
            judge = OllamaJudge()
            with pytest.raises(ConnectionError, match="Could not connect"):
                judge.evaluate("test")

    def test_custom_parameters_passed_through(self):
        from unittest.mock import MagicMock, patch

        from evalkit.judges.ollama import OllamaJudge

        mock_client_instance = MagicMock()
        mock_response = MagicMock()
        mock_response.message.content = '{"score": 0.5, "reason": "ok"}'
        mock_client_instance.chat.return_value = mock_response

        with patch("ollama.Client", return_value=mock_client_instance) as mock_client_cls:
            judge = OllamaJudge(
                model="gpt-oss", base_url="http://custom:1234", temperature=0.5, max_tokens=512, timeout=60
            )
            judge.evaluate("test")

        mock_client_cls.assert_called_once_with(host="http://custom:1234", timeout=60)
        call_kwargs = mock_client_instance.chat.call_args.kwargs
        assert call_kwargs["model"] == "gpt-oss"
        assert call_kwargs["options"]["temperature"] == 0.5
        assert call_kwargs["options"]["num_predict"] == 512


class TestOpenAIJudgeImportGuard:
    def test_raises_import_error_when_openai_not_installed(self):
        original = sys.modules.get("openai", None)
        sys.modules["openai"] = None  # type: ignore[assignment]
        try:
            import importlib

            import evalkit.judges.openai as oj_module

            importlib.reload(oj_module)
            with pytest.raises(ImportError, match="pip install evalkit-llm\\[openai\\]"):
                oj_module.OpenAIJudge()
        finally:
            if original is None:
                sys.modules.pop("openai", None)
            else:
                sys.modules["openai"] = original

    def test_import_error_message_contains_install_hint(self):
        original = sys.modules.get("openai", None)
        sys.modules["openai"] = None  # type: ignore[assignment]
        try:
            import importlib

            import evalkit.judges.openai as oj_module

            importlib.reload(oj_module)
            try:
                oj_module.OpenAIJudge()
            except ImportError as exc:
                assert "evalkit-llm[openai]" in str(exc)
        finally:
            if original is None:
                sys.modules.pop("openai", None)
            else:
                sys.modules["openai"] = original


class TestAnthropicJudgeImportGuard:
    def test_raises_import_error_when_anthropic_not_installed(self):
        original = sys.modules.get("anthropic", None)
        sys.modules["anthropic"] = None  # type: ignore[assignment]
        try:
            import importlib

            import evalkit.judges.anthropic as aj_module

            importlib.reload(aj_module)
            with pytest.raises(ImportError, match="pip install evalkit-llm\\[anthropic\\]"):
                aj_module.AnthropicJudge()
        finally:
            if original is None:
                sys.modules.pop("anthropic", None)
            else:
                sys.modules["anthropic"] = original

    def test_import_error_message_contains_install_hint(self):
        original = sys.modules.get("anthropic", None)
        sys.modules["anthropic"] = None  # type: ignore[assignment]
        try:
            import importlib

            import evalkit.judges.anthropic as aj_module

            importlib.reload(aj_module)
            try:
                aj_module.AnthropicJudge()
            except ImportError as exc:
                assert "evalkit-llm[anthropic]" in str(exc)
        finally:
            if original is None:
                sys.modules.pop("anthropic", None)
            else:
                sys.modules["anthropic"] = original


class TestOllamaJudgeImportGuard:
    def test_raises_import_error_when_ollama_not_installed(self):
        original = sys.modules.get("ollama", None)
        sys.modules["ollama"] = None  # type: ignore[assignment]
        try:
            import importlib

            import evalkit.judges.ollama as ol_module

            importlib.reload(ol_module)
            with pytest.raises(ImportError, match="pip install evalkit-llm\\[ollama\\]"):
                ol_module.OllamaJudge()
        finally:
            if original is None:
                sys.modules.pop("ollama", None)
            else:
                sys.modules["ollama"] = original

    def test_import_error_message_contains_install_hint(self):
        original = sys.modules.get("ollama", None)
        sys.modules["ollama"] = None  # type: ignore[assignment]
        try:
            import importlib

            import evalkit.judges.ollama as ol_module

            importlib.reload(ol_module)
            try:
                ol_module.OllamaJudge()
            except ImportError as exc:
                assert "evalkit-llm[ollama]" in str(exc)
        finally:
            if original is None:
                sys.modules.pop("ollama", None)
            else:
                sys.modules["ollama"] = original


class TestAzureOpenAIJudgeValidation:
    def test_raises_without_endpoint(self):
        from evalkit.judges.openai import AzureOpenAIJudge

        with patch.dict(os.environ, {}, clear=True):
            with pytest.raises(ValueError, match="azure_endpoint is required"):
                AzureOpenAIJudge(model="gpt-4o-mini", api_key="test-key")

    def test_reads_endpoint_from_env(self):
        from evalkit.judges.openai import AzureOpenAIJudge

        with patch.dict(os.environ, {"AZURE_OPENAI_ENDPOINT": "https://test.openai.azure.com"}):
            judge = AzureOpenAIJudge(model="gpt-4o-mini", api_key="test-key")
            assert judge.azure_endpoint == "https://test.openai.azure.com"

    def test_explicit_endpoint_overrides_env(self):
        from evalkit.judges.openai import AzureOpenAIJudge

        with patch.dict(os.environ, {"AZURE_OPENAI_ENDPOINT": "https://env.openai.azure.com"}):
            judge = AzureOpenAIJudge(
                model="gpt-4o-mini",
                api_key="test-key",
                azure_endpoint="https://explicit.openai.azure.com",
            )
            assert judge.azure_endpoint == "https://explicit.openai.azure.com"
