"""Tests for the AI quality judge module.

Tests cover:
- Prompt formatting
- JSON response parsing (valid, markdown-wrapped, malformed)
- Graceful fallback when no AI provider available
- Result dataclass structure
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from orbit.analyzer.ai_quality_judge import (
    AIJudgeResult,
    _format_stats_for_judge,
    check_grade,
)


class TestFormatStats:
    def test_basic_formatting(self):
        result = _format_stats_for_judge(
            divergence=0.161, n_clipping=2, policy_fit=0.88,
            consistency=0.793, elevated_cvs=3,
        )
        assert "0.161" in result
        assert "2 joints" in result
        assert "0.88" in result
        assert "3/8" in result
        assert "0.793" in result

    def test_zero_values(self):
        result = _format_stats_for_judge(0.0, 0, 0.0, 0.0, 0)
        assert "0.000" in result
        assert "0 joints" in result
        assert "0/8" in result

    def test_custom_total_cvs(self):
        result = _format_stats_for_judge(0.1, 1, 0.9, 0.8, 2, total_cvs=10)
        assert "2/10" in result


def _no_provider():
    """Return a ProviderInfo indicating no provider available."""
    from orbit.llm_utils import ProviderInfo
    return ProviderInfo("none", "", None, None, False, "No AI provider available")


class TestCheckGradeNoProvider:
    """Fallback when no AI provider is available."""

    @patch("orbit.analyzer.ai_quality_judge.detect_provider", return_value=_no_provider())
    def test_no_provider_keeps_grade(self, mock_detect):
        result = check_grade(0.20, 3, 0.85, 0.70, 4)
        assert isinstance(result, AIJudgeResult)
        assert result.keep_grade is True
        assert result.error == "No AI provider configured"
        assert result.model_used == "none"

    @patch("orbit.analyzer.ai_quality_judge.detect_provider", return_value=_no_provider())
    def test_returns_reasoning(self, mock_detect):
        result = check_grade(0.1, 0, 0.9, 0.8, 1)
        assert "No AI provider" in result.reasoning


class TestCheckGradeMockedAPI:
    """Test with mocked call_llm responses."""

    @patch("orbit.analyzer.ai_quality_judge.call_llm")
    @patch("orbit.analyzer.ai_quality_judge.detect_provider")
    def test_keep_a_response(self, mock_detect, mock_call):
        from orbit.llm_utils import ProviderInfo
        mock_detect.return_value = ProviderInfo(
            "gemini", "gemini-2.5-flash", None, "fake-key", True, "Gemini"
        )
        mock_call.return_value = '{"keep_A": true, "reasoning": "Low divergence, clean data."}'
        result = check_grade(0.09, 0, 0.92, 0.85, 1)
        assert result.keep_grade is True
        assert "Low divergence" in result.reasoning
        assert result.error is None

    @patch("orbit.analyzer.ai_quality_judge.call_llm")
    @patch("orbit.analyzer.ai_quality_judge.detect_provider")
    def test_downgrade_response(self, mock_detect, mock_call):
        from orbit.llm_utils import ProviderInfo
        mock_detect.return_value = ProviderInfo(
            "gemini", "gemini-2.5-flash", None, "fake-key", True, "Gemini"
        )
        mock_call.return_value = '{"keep_A": false, "reasoning": "Moderate divergence plus clipping."}'
        result = check_grade(0.16, 2, 0.88, 0.79, 3)
        assert result.keep_grade is False
        assert "divergence" in result.reasoning.lower()

    @patch("orbit.analyzer.ai_quality_judge.call_llm")
    @patch("orbit.analyzer.ai_quality_judge.detect_provider")
    def test_markdown_wrapped_json(self, mock_detect, mock_call):
        from orbit.llm_utils import ProviderInfo
        mock_detect.return_value = ProviderInfo(
            "ollama", "gemma4", "http://localhost:11434/v1", "ollama", False, "Ollama"
        )
        mock_call.return_value = '```json\n{"keep_A": false, "reasoning": "Issues found."}\n```'
        result = check_grade(0.18, 3, 0.85, 0.70, 4)
        assert result.keep_grade is False

    @patch("orbit.analyzer.ai_quality_judge.call_llm")
    @patch("orbit.analyzer.ai_quality_judge.detect_provider")
    def test_malformed_json_keeps_grade(self, mock_detect, mock_call):
        from orbit.llm_utils import ProviderInfo
        mock_detect.return_value = ProviderInfo(
            "gemini", "gemini-2.5-flash", None, "fake-key", True, "Gemini"
        )
        mock_call.return_value = "I think the grade should be B because the data has issues."
        result = check_grade(0.16, 2, 0.88, 0.79, 3)
        assert result.keep_grade is True
        assert result.error is not None
        assert "parse" in result.error.lower()

    @patch("orbit.analyzer.ai_quality_judge.call_llm")
    @patch("orbit.analyzer.ai_quality_judge.detect_provider")
    def test_empty_response_keeps_grade(self, mock_detect, mock_call):
        from orbit.llm_utils import ProviderInfo
        mock_detect.return_value = ProviderInfo(
            "gemini", "gemini-2.5-flash", None, "fake-key", True, "Gemini"
        )
        mock_call.return_value = None
        result = check_grade(0.16, 2, 0.88, 0.79, 3)
        assert result.keep_grade is True
        assert result.error is not None

    @patch("orbit.analyzer.ai_quality_judge.call_llm", side_effect=RuntimeError("API down"))
    @patch("orbit.analyzer.ai_quality_judge.detect_provider")
    def test_api_error_keeps_grade(self, mock_detect, mock_call):
        from orbit.llm_utils import ProviderInfo
        mock_detect.return_value = ProviderInfo(
            "gemini", "gemini-2.5-flash", None, "fake-key", True, "Gemini"
        )
        result = check_grade(0.16, 2, 0.88, 0.79, 3)
        assert result.keep_grade is True
        assert result.error is not None
        assert "API down" in result.error

    @patch("orbit.analyzer.ai_quality_judge.call_llm")
    @patch("orbit.analyzer.ai_quality_judge.detect_provider")
    def test_missing_fields_default_to_keep(self, mock_detect, mock_call):
        from orbit.llm_utils import ProviderInfo
        mock_detect.return_value = ProviderInfo(
            "gemini", "gemini-2.5-flash", None, "fake-key", True, "Gemini"
        )
        mock_call.return_value = '{"reasoning": "looks fine"}'
        result = check_grade(0.10, 0, 0.90, 0.85, 1)
        assert result.keep_grade is True


class TestAIJudgeResult:
    def test_dataclass_fields(self):
        r = AIJudgeResult(keep_grade=True, reasoning="test", model_used="gemini")
        assert r.keep_grade is True
        assert r.reasoning == "test"
        assert r.model_used == "gemini"
        assert r.error is None

    def test_dataclass_with_error(self):
        r = AIJudgeResult(
            keep_grade=True, reasoning="fallback", model_used="none", error="no key"
        )
        assert r.error == "no key"
