"""
Coverage tests for DualConfidenceChecker.

Tests cover missing branches in:
- _should_early_reject: high negative returns True; low positive returns True; both ok False
- should_reject_candidate_from_averages: high negative, low positive, both ok
- should_reject_candidate: empty evidence list; high negative; low positive; both ok
- _log_constraint_result_detailed: symbol selection (good/medium/bad score)
- _llm_prescreen_candidate: no original_query short-circuits; quality>=50 accepts;
  quality<50 rejects; parse failure accepts; exception accepts
"""

from unittest.mock import MagicMock, Mock, patch

from local_deep_research.advanced_search_system.candidates.base_candidate import (
    Candidate,
)
from local_deep_research.advanced_search_system.constraints.base_constraint import (
    Constraint,
    ConstraintType,
)
from local_deep_research.advanced_search_system.constraint_checking.dual_confidence_checker import (
    DualConfidenceChecker,
)
from local_deep_research.advanced_search_system.constraint_checking.evidence_analyzer import (
    ConstraintEvidence,
)

MODULE = "local_deep_research.advanced_search_system.constraint_checking.dual_confidence_checker"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_checker(
    negative_threshold=0.25,
    positive_threshold=0.4,
    uncertainty_threshold=0.6,
    max_reevaluations=2,
):
    model = MagicMock()
    with patch(
        f"{MODULE}.EvidenceAnalyzer",
        return_value=MagicMock(),
    ):
        checker = DualConfidenceChecker(
            model,
            negative_threshold=negative_threshold,
            positive_threshold=positive_threshold,
            uncertainty_threshold=uncertainty_threshold,
            max_reevaluations=max_reevaluations,
        )
    return checker


def _constraint(value="test_constraint"):
    return Constraint(
        id="c1",
        type=ConstraintType.PROPERTY,
        description="A test constraint",
        value=value,
        weight=1.0,
    )


def _candidate(name="TestCandidate"):
    return Candidate(name=name)


def _evidence(positive=0.5, negative=0.1, uncertainty=0.4):
    ev = ConstraintEvidence(
        positive_confidence=positive,
        negative_confidence=negative,
        uncertainty=uncertainty,
        evidence_text="some evidence",
        source="test_source",
    )
    return ev


# ---------------------------------------------------------------------------
# _should_early_reject
# ---------------------------------------------------------------------------


class TestShouldEarlyReject:
    """_should_early_reject returns True only when thresholds are breached."""

    def test_high_negative_returns_true(self):
        checker = _make_checker(negative_threshold=0.25)
        assert (
            checker._should_early_reject(avg_positive=0.6, avg_negative=0.5)
            is True
        )

    def test_low_positive_returns_true(self):
        checker = _make_checker(positive_threshold=0.4)
        assert (
            checker._should_early_reject(avg_positive=0.1, avg_negative=0.1)
            is True
        )

    def test_both_ok_returns_false(self):
        checker = _make_checker(negative_threshold=0.25, positive_threshold=0.4)
        assert (
            checker._should_early_reject(avg_positive=0.8, avg_negative=0.1)
            is False
        )


# ---------------------------------------------------------------------------
# should_reject_candidate_from_averages
# ---------------------------------------------------------------------------


class TestShouldRejectCandidateFromAverages:
    """should_reject_candidate_from_averages covers all three branches."""

    def test_high_negative_rejects(self):
        checker = _make_checker(negative_threshold=0.25)
        reject, reason = checker.should_reject_candidate_from_averages(
            _candidate(), _constraint(), avg_positive=0.8, avg_negative=0.5
        )
        assert reject is True
        assert "negative" in reason.lower()

    def test_low_positive_rejects(self):
        checker = _make_checker(positive_threshold=0.4)
        reject, reason = checker.should_reject_candidate_from_averages(
            _candidate(), _constraint(), avg_positive=0.1, avg_negative=0.1
        )
        assert reject is True
        assert "positive" in reason.lower()

    def test_both_within_thresholds_accepts(self):
        checker = _make_checker(negative_threshold=0.25, positive_threshold=0.4)
        reject, reason = checker.should_reject_candidate_from_averages(
            _candidate(), _constraint(), avg_positive=0.8, avg_negative=0.1
        )
        assert reject is False
        assert reason == ""


# ---------------------------------------------------------------------------
# should_reject_candidate (evidence list variant)
# ---------------------------------------------------------------------------


class TestShouldRejectCandidateFromEvidenceList:
    """should_reject_candidate with ConstraintEvidence list."""

    def test_empty_evidence_returns_false(self):
        checker = _make_checker()
        reject, reason = checker.should_reject_candidate(
            _candidate(), _constraint(), dual_evidence=[]
        )
        assert reject is False

    def test_high_negative_evidence_rejects(self):
        checker = _make_checker(negative_threshold=0.25)
        evidence = [_evidence(positive=0.9, negative=0.8)]
        reject, reason = checker.should_reject_candidate(
            _candidate(), _constraint(), dual_evidence=evidence
        )
        assert reject is True

    def test_low_positive_evidence_rejects(self):
        checker = _make_checker(positive_threshold=0.4)
        evidence = [_evidence(positive=0.1, negative=0.1)]
        reject, reason = checker.should_reject_candidate(
            _candidate(), _constraint(), dual_evidence=evidence
        )
        assert reject is True

    def test_good_evidence_accepts(self):
        checker = _make_checker(negative_threshold=0.25, positive_threshold=0.4)
        evidence = [_evidence(positive=0.9, negative=0.05)]
        reject, reason = checker.should_reject_candidate(
            _candidate(), _constraint(), dual_evidence=evidence
        )
        assert reject is False


# ---------------------------------------------------------------------------
# _log_constraint_result_detailed
# ---------------------------------------------------------------------------


class TestLogConstraintResultDetailed:
    """_log_constraint_result_detailed selects correct symbol."""

    def test_high_score_logs_checkmark(self, caplog):
        checker = _make_checker()
        with patch(f"{MODULE}.logger") as mock_logger:
            checker._log_constraint_result_detailed(
                _candidate("X"),
                _constraint(),
                score=0.9,
                positive=0.9,
                negative=0.05,
                uncertainty=0.05,
            )
            assert mock_logger.info.called
            call_str = mock_logger.info.call_args[0][0]
            assert "✓" in call_str

    def test_medium_score_logs_circle(self):
        checker = _make_checker()
        with patch(f"{MODULE}.logger") as mock_logger:
            checker._log_constraint_result_detailed(
                _candidate("X"),
                _constraint(),
                score=0.6,
                positive=0.6,
                negative=0.1,
                uncertainty=0.3,
            )
            call_str = mock_logger.info.call_args[0][0]
            assert "○" in call_str

    def test_low_score_logs_cross(self):
        checker = _make_checker()
        with patch(f"{MODULE}.logger") as mock_logger:
            checker._log_constraint_result_detailed(
                _candidate("X"),
                _constraint(),
                score=0.3,
                positive=0.3,
                negative=0.4,
                uncertainty=0.3,
            )
            call_str = mock_logger.info.call_args[0][0]
            assert "✗" in call_str

    def test_reevaluation_indicator_included(self):
        checker = _make_checker()
        with patch(f"{MODULE}.logger") as mock_logger:
            checker._log_constraint_result_detailed(
                _candidate("X"),
                _constraint(),
                score=0.7,
                positive=0.7,
                negative=0.1,
                uncertainty=0.2,
                reevaluation_count=1,
            )
            call_str = mock_logger.info.call_args[0][0]
            assert "[R1]" in call_str


# ---------------------------------------------------------------------------
# _llm_prescreen_candidate
# ---------------------------------------------------------------------------


class TestLlmPrescreenCandidate:
    """_llm_prescreen_candidate covers all branches."""

    def test_no_original_query_accepts_without_model_call(self):
        checker = _make_checker()
        result = checker._llm_prescreen_candidate(
            _candidate(), [], original_query=None
        )
        assert result["should_reject"] is False
        checker.model.invoke.assert_not_called()

    def test_quality_above_50_accepts(self):
        checker = _make_checker()
        checker.model.invoke.return_value = Mock(content="75")
        result = checker._llm_prescreen_candidate(
            _candidate(), [], original_query="What is the answer?"
        )
        assert result["should_reject"] is False

    def test_quality_below_50_rejects(self):
        checker = _make_checker()
        checker.model.invoke.return_value = Mock(content="20")
        result = checker._llm_prescreen_candidate(
            _candidate(), [], original_query="What is the answer?"
        )
        assert result["should_reject"] is True
        assert "20%" in result["reason"]

    def test_parse_failure_accepts(self):
        checker = _make_checker()
        checker.model.invoke.return_value = Mock(content="no number here")
        result = checker._llm_prescreen_candidate(
            _candidate(), [], original_query="What is the answer?"
        )
        assert result["should_reject"] is False

    def test_exception_accepts(self):
        checker = _make_checker()
        checker.model.invoke.side_effect = RuntimeError("network error")
        result = checker._llm_prescreen_candidate(
            _candidate(), [], original_query="What is X?"
        )
        assert result["should_reject"] is False
