"""
Pure-logic tests for EvidenceBasedStrategy.

Tests _has_sufficient_answer (threshold + critical constraint checks) and
_get_distinctive_constraints (priority-indexed sort with naming guarantee)
— no LLM or search calls.
"""

from unittest.mock import Mock

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.evidence.base_evidence import (
    Evidence,
    EvidenceType,
)
from local_deep_research.advanced_search_system.strategies.evidence_based_strategy import (
    EvidenceBasedStrategy,
)


def _constraint(id, ctype=ConstraintType.PROPERTY, weight=1.0, value="v"):
    return Constraint(
        id=id,
        type=ctype,
        description="",
        value=value,
        weight=weight,
    )


def _evidence(confidence=0.8):
    return Evidence(
        claim="test",
        type=EvidenceType.DIRECT_STATEMENT,
        source="test",
        confidence=confidence,
    )


def _make_strategy(
    candidates=None,
    constraints=None,
    confidence_threshold=0.85,
    evidence_threshold=0.6,
):
    """Build a minimal Mock with the attributes used by pure-logic methods."""
    s = Mock(spec=[])
    s.candidates = candidates or []
    s.constraints = constraints or []
    s.confidence_threshold = confidence_threshold
    s.evidence_threshold = evidence_threshold
    return s


# ---------------------------------------------------------------------------
# _has_sufficient_answer
# ---------------------------------------------------------------------------


class TestHasSufficientAnswer:
    """Verify sufficient-answer determination: score threshold + critical evidence."""

    def test_no_candidates(self):
        """No candidates -> False."""
        s = _make_strategy(candidates=[])
        assert EvidenceBasedStrategy._has_sufficient_answer(s) is False

    def test_top_candidate_below_threshold(self):
        """Top candidate score below confidence_threshold -> False."""
        c = Candidate(name="X", score=0.5)
        s = _make_strategy(candidates=[c])
        assert EvidenceBasedStrategy._has_sufficient_answer(s) is False

    def test_passes_with_all_critical_evidence(self):
        """High score + all critical constraints have strong evidence -> True."""
        c = Candidate(name="X", score=0.9)
        con = _constraint("c1", weight=0.9)
        c.add_evidence("c1", _evidence(confidence=0.7))
        s = _make_strategy(
            candidates=[c],
            constraints=[con],
            confidence_threshold=0.85,
            evidence_threshold=0.6,
        )
        assert EvidenceBasedStrategy._has_sufficient_answer(s) is True

    def test_fails_with_weak_critical_evidence(self):
        """High score but critical constraint evidence below threshold -> False."""
        c = Candidate(name="X", score=0.9)
        con = _constraint("c1", weight=0.9)
        c.add_evidence("c1", _evidence(confidence=0.3))  # Below 0.6
        s = _make_strategy(
            candidates=[c],
            constraints=[con],
            confidence_threshold=0.85,
            evidence_threshold=0.6,
        )
        assert EvidenceBasedStrategy._has_sufficient_answer(s) is False

    def test_fails_with_missing_critical_evidence(self):
        """High score but critical constraint has no evidence -> False."""
        c = Candidate(name="X", score=0.9)
        con = _constraint("c1", weight=0.9)
        s = _make_strategy(
            candidates=[c],
            constraints=[con],
            confidence_threshold=0.85,
            evidence_threshold=0.6,
        )
        assert EvidenceBasedStrategy._has_sufficient_answer(s) is False

    def test_non_critical_constraints_ignored(self):
        """Constraints with weight < 0.8 don't block sufficiency."""
        c = Candidate(name="X", score=0.9)
        critical = _constraint("c1", weight=0.9)
        non_critical = _constraint("c2", weight=0.5)
        c.add_evidence("c1", _evidence(confidence=0.8))
        # c2 has no evidence but weight < 0.8
        s = _make_strategy(
            candidates=[c],
            constraints=[critical, non_critical],
        )
        assert EvidenceBasedStrategy._has_sufficient_answer(s) is True

    def test_multiple_critical_all_needed(self):
        """All critical constraints must have sufficient evidence."""
        c = Candidate(name="X", score=0.9)
        c1 = _constraint("c1", weight=0.9)
        c2 = _constraint("c2", weight=0.85)
        c.add_evidence("c1", _evidence(confidence=0.8))
        # c2 missing evidence
        s = _make_strategy(candidates=[c], constraints=[c1, c2])
        assert EvidenceBasedStrategy._has_sufficient_answer(s) is False


# ---------------------------------------------------------------------------
# _get_distinctive_constraints
# ---------------------------------------------------------------------------


class TestGetDistinctiveConstraints:
    """Verify priority-sorted constraint selection with naming guarantee."""

    def test_priority_order(self):
        """NAME_PATTERN > LOCATION > EVENT > STATISTIC > PROPERTY > TEMPORAL."""
        constraints = [
            _constraint("prop", ConstraintType.PROPERTY),
            _constraint("name", ConstraintType.NAME_PATTERN),
            _constraint("loc", ConstraintType.LOCATION),
            _constraint("evt", ConstraintType.EVENT),
        ]
        s = _make_strategy(constraints=constraints)
        result = EvidenceBasedStrategy._get_distinctive_constraints(s)
        ids = [c.id for c in result]
        # Top 3 by priority: NAME_PATTERN, LOCATION, EVENT
        assert ids == ["name", "loc", "evt"]

    def test_returns_max_three(self):
        """At most 3 constraints returned."""
        constraints = [
            _constraint(f"c{i}", ConstraintType.PROPERTY) for i in range(5)
        ]
        s = _make_strategy(constraints=constraints)
        result = EvidenceBasedStrategy._get_distinctive_constraints(s)
        assert len(result) == 3

    def test_naming_guarantee(self):
        """If top 3 lack NAME_PATTERN/LOCATION, one is swapped in."""
        constraints = [
            _constraint("stat", ConstraintType.STATISTIC),
            _constraint("evt", ConstraintType.EVENT),
            _constraint("prop", ConstraintType.PROPERTY),
            _constraint("loc", ConstraintType.LOCATION),  # 4th by priority
        ]
        s = _make_strategy(constraints=constraints)
        result = EvidenceBasedStrategy._get_distinctive_constraints(s)
        # Location should be swapped in for the least important (property)
        ids = [c.id for c in result]
        assert "loc" in ids

    def test_naming_already_present(self):
        """No swap needed if NAME_PATTERN already in top 3."""
        constraints = [
            _constraint("name", ConstraintType.NAME_PATTERN),
            _constraint("stat", ConstraintType.STATISTIC),
            _constraint("evt", ConstraintType.EVENT),
            _constraint("prop", ConstraintType.PROPERTY),
        ]
        s = _make_strategy(constraints=constraints)
        result = EvidenceBasedStrategy._get_distinctive_constraints(s)
        ids = [c.id for c in result]
        assert ids == ["name", "evt", "stat"]

    def test_weight_tiebreaker(self):
        """Within same type, higher weight comes first."""
        constraints = [
            _constraint("p1", ConstraintType.PROPERTY, weight=0.5),
            _constraint("p2", ConstraintType.PROPERTY, weight=0.9),
            _constraint("p3", ConstraintType.PROPERTY, weight=0.7),
        ]
        s = _make_strategy(constraints=constraints)
        result = EvidenceBasedStrategy._get_distinctive_constraints(s)
        ids = [c.id for c in result]
        assert ids == ["p2", "p3", "p1"]
