"""
High-value pure logic tests for ConstrainedSearchStrategy.

Tests focus on:
- _calculate_restrictiveness_score and _rank_constraints_by_restrictiveness
- Formatting methods (_format_constraint_analysis, _format_stage_results, etc.)
- Filtering/deduplication logic
- _quick_evidence_check scoring
- _validate_search_results
- _group_similar_candidates

All tests bypass __init__ to avoid LLM/network dependencies.
"""

from unittest.mock import 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.evidence.base_evidence import (
    Evidence,
    EvidenceType,
)
from local_deep_research.advanced_search_system.strategies.constrained_search_strategy import (
    ConstrainedSearchStrategy,
)


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


def _make_strategy(**overrides):
    """Create a ConstrainedSearchStrategy with __init__ bypassed."""
    with patch.object(
        ConstrainedSearchStrategy, "__init__", lambda self, *a, **kw: None
    ):
        strat = ConstrainedSearchStrategy.__new__(ConstrainedSearchStrategy)

    # Defaults that the real __init__ would set
    defaults = dict(
        constraints=[],
        constraint_ranking=[],
        stage_candidates={},
        candidates=[],
        evidence_threshold=0.6,
        candidate_limit=100,
        search_history=[],
    )
    defaults.update(overrides)
    for key, val in defaults.items():
        setattr(strat, key, val)
    return strat


def _make_constraint(
    id_="c1",
    ctype=ConstraintType.PROPERTY,
    value="test value",
    description="Test constraint",
    weight=0.5,
):
    """Create a Constraint with convenient defaults."""
    return Constraint(
        id=id_, type=ctype, value=value, description=description, weight=weight
    )


# ---------------------------------------------------------------------------
# Tests: _calculate_restrictiveness_score
# ---------------------------------------------------------------------------


class TestCalculateRestrictivenessScore:
    """Tests for the _calculate_restrictiveness_score method."""

    def test_statistic_type_gets_10(self):
        """STATISTIC constraint type adds +10 to score."""
        strat = _make_strategy()
        c = _make_constraint(ctype=ConstraintType.STATISTIC, value="abc")
        assert strat._calculate_restrictiveness_score(c) == 10

    def test_event_type_gets_8(self):
        """EVENT constraint type adds +8 to score."""
        strat = _make_strategy()
        c = _make_constraint(ctype=ConstraintType.EVENT, value="abc")
        assert strat._calculate_restrictiveness_score(c) == 8

    def test_location_type_gets_6(self):
        """LOCATION constraint type adds +6 to score."""
        strat = _make_strategy()
        c = _make_constraint(ctype=ConstraintType.LOCATION, value="abc")
        assert strat._calculate_restrictiveness_score(c) == 6

    def test_property_type_gets_4(self):
        """PROPERTY constraint type adds +4 to score."""
        strat = _make_strategy()
        c = _make_constraint(ctype=ConstraintType.PROPERTY, value="abc")
        assert strat._calculate_restrictiveness_score(c) == 4

    def test_value_with_digits_adds_5(self):
        """Presence of digits in value adds +5."""
        strat = _make_strategy()
        c = _make_constraint(ctype=ConstraintType.PROPERTY, value="year 2020")
        # PROPERTY=4, digits=5 => 9
        assert strat._calculate_restrictiveness_score(c) == 9

    def test_long_value_adds_3(self):
        """Value with >3 words adds +3."""
        strat = _make_strategy()
        c = _make_constraint(
            ctype=ConstraintType.PROPERTY,
            value="a very long descriptive phrase",
        )
        # PROPERTY=4, >3 words=3 => 7
        assert strat._calculate_restrictiveness_score(c) == 7

    def test_specific_keyword_adds_2(self):
        """Values containing 'specific', 'exact', 'only', or 'must' add +2."""
        strat = _make_strategy()
        for keyword in ["specific", "exact", "only", "must"]:
            c = _make_constraint(
                ctype=ConstraintType.PROPERTY, value=f"{keyword} item"
            )
            score = strat._calculate_restrictiveness_score(c)
            # PROPERTY=4, keyword=2 => 6
            assert score == 6, f"Keyword '{keyword}' should add +2"

    def test_combined_bonuses_stack(self):
        """All specificity bonuses stack correctly."""
        strat = _make_strategy()
        # STATISTIC(10) + digits(5) + >3 words(3) + keyword(2) = 20
        c = _make_constraint(
            ctype=ConstraintType.STATISTIC,
            value="must be exactly 42 units long",
        )
        assert strat._calculate_restrictiveness_score(c) == 20

    def test_none_value_skips_specificity(self):
        """Constraint with value=None gets only type-based score."""
        strat = _make_strategy()
        c = _make_constraint(ctype=ConstraintType.EVENT, value=None)
        assert strat._calculate_restrictiveness_score(c) == 8

    def test_empty_string_value_skips_specificity(self):
        """Constraint with empty string value gets only type-based score."""
        strat = _make_strategy()
        c = _make_constraint(ctype=ConstraintType.LOCATION, value="")
        assert strat._calculate_restrictiveness_score(c) == 6


# ---------------------------------------------------------------------------
# Tests: _rank_constraints_by_restrictiveness
# ---------------------------------------------------------------------------


class TestRankConstraintsByRestrictiveness:
    """Tests for _rank_constraints_by_restrictiveness method."""

    def test_sorts_by_score_descending(self):
        """Constraints are sorted from highest to lowest score."""
        strat = _make_strategy()
        strat.constraints = [
            _make_constraint(
                id_="p", ctype=ConstraintType.PROPERTY, value="abc"
            ),
            _make_constraint(
                id_="s", ctype=ConstraintType.STATISTIC, value="123"
            ),
            _make_constraint(id_="e", ctype=ConstraintType.EVENT, value="abc"),
        ]
        ranked = strat._rank_constraints_by_restrictiveness()
        # STATISTIC(10)+digits(5)=15, EVENT=8, PROPERTY=4
        assert ranked[0].id == "s"
        assert ranked[1].id == "e"
        assert ranked[2].id == "p"

    def test_empty_constraints_returns_empty(self):
        """Empty constraints list returns empty ranked list."""
        strat = _make_strategy()
        strat.constraints = []
        assert strat._rank_constraints_by_restrictiveness() == []

    def test_single_constraint(self):
        """Single constraint is returned as-is."""
        strat = _make_strategy()
        c = _make_constraint(id_="only")
        strat.constraints = [c]
        ranked = strat._rank_constraints_by_restrictiveness()
        assert len(ranked) == 1
        assert ranked[0].id == "only"

    def test_preserves_all_constraints(self):
        """All constraints appear in ranked output."""
        strat = _make_strategy()
        strat.constraints = [
            _make_constraint(
                id_=str(i), ctype=ConstraintType.PROPERTY, value="x"
            )
            for i in range(5)
        ]
        ranked = strat._rank_constraints_by_restrictiveness()
        assert len(ranked) == 5


# ---------------------------------------------------------------------------
# Tests: Format methods
# ---------------------------------------------------------------------------


class TestFormatMethods:
    """Tests for formatting helper methods."""

    def test_format_constraint_analysis_includes_header(self):
        """_format_constraint_analysis includes query constraint header."""
        strat = _make_strategy()
        c = _make_constraint(description="Height over 100m")
        strat.constraints = [c]
        strat.constraint_ranking = [c]
        result = strat._format_constraint_analysis()
        assert "Query Constraint Analysis" in result

    def test_format_constraint_analysis_includes_description(self):
        """_format_constraint_analysis includes each constraint description."""
        strat = _make_strategy()
        c = _make_constraint(description="Height over 100m")
        strat.constraints = [c]
        strat.constraint_ranking = [c]
        result = strat._format_constraint_analysis()
        assert "Height over 100m" in result

    def test_format_constraint_analysis_includes_score(self):
        """_format_constraint_analysis shows restrictiveness score."""
        strat = _make_strategy()
        c = _make_constraint(ctype=ConstraintType.STATISTIC, value="42")
        strat.constraints = [c]
        strat.constraint_ranking = [c]
        result = strat._format_constraint_analysis()
        assert "15" in result  # STATISTIC(10) + digits(5)

    def test_format_stage_results_with_candidates(self):
        """_format_stage_results shows stage number and candidates."""
        strat = _make_strategy()
        c = _make_constraint(description="Located in Colorado")
        candidates = [
            Candidate(name="Pike's Peak"),
            Candidate(name="Mt Elbert"),
        ]
        result = strat._format_stage_results(0, c, candidates)
        assert "Stage 1" in result
        assert "Located in Colorado" in result
        assert "Pike's Peak" in result
        assert "Mt Elbert" in result

    def test_format_stage_results_empty_candidates(self):
        """_format_stage_results handles empty candidate list."""
        strat = _make_strategy()
        c = _make_constraint()
        result = strat._format_stage_results(0, c, [])
        assert "No candidates found" in result

    def test_format_search_summary_includes_final_count(self):
        """_format_search_summary includes final candidate count."""
        strat = _make_strategy()
        c = _make_constraint()
        strat.constraint_ranking = [c]
        strat.stage_candidates = {0: [Candidate(name="A")]}
        strat.candidates = [Candidate(name="A")]
        result = strat._format_search_summary()
        assert "1 candidates selected" in result

    def test_format_debug_summary_includes_debug_header(self):
        """_format_debug_summary includes Debug Summary header."""
        strat = _make_strategy()
        c = _make_constraint()
        strat.constraints = [c]
        strat.constraint_ranking = [c]
        strat.stage_candidates = {}
        strat.candidates = []
        result = strat._format_debug_summary()
        assert "Debug Summary" in result

    def test_format_evidence_summary_no_candidates(self):
        """_format_evidence_summary works with zero candidates."""
        strat = _make_strategy()
        strat.constraints = [_make_constraint()]
        strat.candidates = []
        result = strat._format_evidence_summary()
        assert "Evidence Gathering Summary" in result


# ---------------------------------------------------------------------------
# Tests: _deduplicate_candidates
# ---------------------------------------------------------------------------


class TestDeduplicateCandidates:
    """Tests for candidate deduplication."""

    def test_removes_case_insensitive_duplicates(self):
        """Candidates with same name (case-insensitive) are deduplicated."""
        strat = _make_strategy()
        candidates = [
            Candidate(name="Alpha"),
            Candidate(name="alpha"),
            Candidate(name="ALPHA"),
        ]
        result = strat._deduplicate_candidates(candidates)
        assert len(result) == 1
        assert result[0].name == "Alpha"  # first seen wins

    def test_keeps_distinct_candidates(self):
        """Distinct candidates are all kept."""
        strat = _make_strategy()
        candidates = [
            Candidate(name="Alpha"),
            Candidate(name="Beta"),
            Candidate(name="Gamma"),
        ]
        result = strat._deduplicate_candidates(candidates)
        assert len(result) == 3

    def test_empty_list(self):
        """Empty input returns empty output."""
        strat = _make_strategy()
        assert strat._deduplicate_candidates([]) == []


# ---------------------------------------------------------------------------
# Tests: _group_similar_candidates
# ---------------------------------------------------------------------------


class TestGroupSimilarCandidates:
    """Tests for candidate grouping logic."""

    def test_ai_model_group(self):
        """Candidates with AI model keywords go to 'AI Models' group."""
        strat = _make_strategy()
        candidates = [Candidate(name="GPT-4"), Candidate(name="Claude 3")]
        grouped = strat._group_similar_candidates(candidates)
        assert "AI Models" in grouped
        assert len(grouped["AI Models"]) == 2

    def test_numeric_items_group(self):
        """Candidates with digits go to 'Numeric Items' group."""
        strat = _make_strategy()
        candidates = [Candidate(name="Highway 66")]
        grouped = strat._group_similar_candidates(candidates)
        assert "Numeric Items" in grouped

    def test_default_grouping_uses_first_word(self):
        """Candidates without keyword matches use first-word grouping."""
        strat = _make_strategy()
        candidates = [Candidate(name="Evergreen Forest")]
        grouped = strat._group_similar_candidates(candidates)
        assert "Evergreen Items" in grouped

    def test_empty_candidates(self):
        """Empty candidate list returns empty dict."""
        strat = _make_strategy()
        assert strat._group_similar_candidates([]) == {}


# ---------------------------------------------------------------------------
# Tests: _validate_search_results
# ---------------------------------------------------------------------------


class TestValidateSearchResults:
    """Tests for search result validation."""

    def test_rejects_none_results(self):
        """None results are rejected."""
        strat = _make_strategy()
        c = _make_constraint()
        assert strat._validate_search_results(None, c) is False

    def test_rejects_empty_content(self):
        """Empty content string is rejected."""
        strat = _make_strategy()
        c = _make_constraint()
        results = {"current_knowledge": "", "search_results": []}
        assert strat._validate_search_results(results, c) is False

    def test_rejects_short_content(self):
        """Content under 50 chars is rejected."""
        strat = _make_strategy()
        c = _make_constraint()
        results = {"current_knowledge": "Too short", "search_results": []}
        assert strat._validate_search_results(results, c) is False

    def test_rejects_no_results_found_message(self):
        """Content containing 'No results found' is rejected."""
        strat = _make_strategy()
        c = _make_constraint()
        results = {
            "current_knowledge": "No results found for this query. " * 5,
            "search_results": [],
        }
        assert strat._validate_search_results(results, c) is False

    def test_accepts_valid_property_content(self):
        """Valid content matching property constraint terms is accepted."""
        strat = _make_strategy()
        c = _make_constraint(
            ctype=ConstraintType.PROPERTY,
            value="mountain hiking trail overview",
        )
        results = {
            "current_knowledge": (
                "This mountain hiking trail overview covers several popular "
                "destinations. The trail system includes many routes for "
                "experienced hikers and beginners alike."
            ),
            "search_results": [{"title": "Trails", "snippet": "hiking info"}],
        }
        assert strat._validate_search_results(results, c) is True

    def test_accepts_statistic_with_matching_terms(self):
        """STATISTIC constraint with matching terms in content is accepted."""
        strat = _make_strategy()
        c = _make_constraint(
            ctype=ConstraintType.STATISTIC,
            value="population density growth",
        )
        results = {
            "current_knowledge": (
                "The population of the region saw significant density "
                "increases and growth over the past decade due to urban "
                "expansion and migration patterns."
            ),
            "search_results": [{"title": "Stats", "snippet": "data"}],
        }
        assert strat._validate_search_results(results, c) is True

    def test_rejects_statistic_with_no_matching_terms(self):
        """STATISTIC constraint rejected when content has no matching terms."""
        strat = _make_strategy()
        c = _make_constraint(
            ctype=ConstraintType.STATISTIC,
            value="revenue quarterly earnings",
        )
        results = {
            "current_knowledge": (
                "The weather forecast for tomorrow shows sunny skies "
                "with temperatures reaching thirty degrees in most areas."
            ),
            "search_results": [{"title": "Weather", "snippet": "sun"}],
        }
        assert strat._validate_search_results(results, c) is False

    def test_accepts_statistic_non_tv_domain(self):
        """STATISTIC constraint works for non-TV domains (no TV coupling)."""
        strat = _make_strategy()
        c = _make_constraint(
            ctype=ConstraintType.STATISTIC,
            value="4000 meter elevation",
        )
        results = {
            "current_knowledge": (
                "The mountain peak reaches an elevation of over four "
                "thousand meter above sea level, making it one of the "
                "tallest in the range."
            ),
            "search_results": [{"title": "Peak", "snippet": "altitude"}],
        }
        assert strat._validate_search_results(results, c) is True

    def test_statistic_none_value_skips_term_check(self):
        """STATISTIC with None value skips term check, falls to quality."""
        strat = _make_strategy()
        c = _make_constraint(
            ctype=ConstraintType.STATISTIC,
            value=None,
        )
        results = {
            "current_knowledge": (
                "Sufficient content that passes the length check easily "
                "and does not contain error or no results messages."
            ),
            "search_results": [{"title": "Info", "snippet": "data"}],
        }
        assert strat._validate_search_results(results, c) is True

    def test_statistic_numeric_value_no_crash(self):
        """STATISTIC with numeric value does not crash."""
        strat = _make_strategy()
        c = _make_constraint(
            ctype=ConstraintType.STATISTIC,
            value=42,
        )
        results = {
            "current_knowledge": (
                "This content is long enough and contains the number 42 "
                "somewhere in the text for validation purposes."
            ),
            "search_results": [],
        }
        # str(42) = "42", len == 2, filtered by len > 2,
        # so constraint_terms is empty and validation passes.
        assert strat._validate_search_results(results, c) is True

    def test_none_value_non_statistic_no_crash(self):
        """PROPERTY with None value does not crash."""
        strat = _make_strategy()
        c = _make_constraint(
            ctype=ConstraintType.PROPERTY,
            value=None,
        )
        results = {
            "current_knowledge": (
                "Sufficient content that passes the length check easily "
                "and does not contain error or no results messages."
            ),
            "search_results": [{"title": "Info", "snippet": "data"}],
        }
        assert strat._validate_search_results(results, c) is True

    def test_short_numeric_value_passes_validation(self):
        """PROPERTY with short numeric value '42' passes (no terms > 2 chars)."""
        strat = _make_strategy()
        c = _make_constraint(
            ctype=ConstraintType.PROPERTY,
            value="42",
        )
        results = {
            "current_knowledge": (
                "This content is long enough to pass the minimum length "
                "check and has valid search results attached to it."
            ),
            "search_results": [{"title": "Info", "snippet": "data"}],
        }
        assert strat._validate_search_results(results, c) is True


# ---------------------------------------------------------------------------
# Tests: _quick_evidence_check scoring
# ---------------------------------------------------------------------------


class TestQuickEvidenceCheck:
    """Tests for the _quick_evidence_check scoring logic."""

    def test_returns_evidence_object(self):
        """Returns an Evidence instance."""
        strat = _make_strategy()
        candidate = Candidate(name="Test Entity")
        constraint = _make_constraint()
        results = {"current_knowledge": "Some content", "search_results": []}
        evidence = strat._quick_evidence_check(results, candidate, constraint)
        assert isinstance(evidence, Evidence)

    def test_base_confidence_when_nothing_matches(self):
        """When no components match, confidence falls back to Evidence type base (INFERENCE=0.5).

        The _quick_evidence_check computes 0.0 but Evidence.__post_init__
        replaces 0.0 with the EvidenceType.INFERENCE base_confidence of 0.5.
        """
        strat = _make_strategy()
        candidate = Candidate(name="Nonexistent Entity")
        constraint = _make_constraint(value="irrelevant zzzz")
        results = {
            "current_knowledge": "Completely unrelated topic about weather patterns.",
            "search_results": [],
        }
        evidence = strat._quick_evidence_check(results, candidate, constraint)
        # All scoring components are zero
        assert evidence.metadata["name_presence"] == 0.0
        assert evidence.metadata["constraint_presence"] == 0.0
        assert evidence.metadata["co_occurrence"] == 0.0
        assert evidence.metadata["context_quality"] == 0.0
        # But Evidence post_init sets INFERENCE base_confidence
        assert evidence.confidence == EvidenceType.INFERENCE.base_confidence

    def test_higher_confidence_with_co_occurrence(self):
        """Confidence is higher when both candidate and constraint value co-occur."""
        strat = _make_strategy()
        candidate = Candidate(name="Mount Elbert")
        constraint = _make_constraint(value="14000 feet")
        # Both terms present and close together
        content = (
            "Mount Elbert rises to 14000 feet above sea level in Colorado."
        )
        results = {"current_knowledge": content, "search_results": []}
        evidence = strat._quick_evidence_check(results, candidate, constraint)
        assert evidence.confidence > 0.5

    def test_statistic_type_multiplier(self):
        """STATISTIC constraint type applies 1.1x multiplier to confidence."""
        strat = _make_strategy()
        candidate = Candidate(name="Entity A")
        content = (
            "Entity A has exactly the value abc as expected in this context."
        )

        c_stat = _make_constraint(ctype=ConstraintType.STATISTIC, value="abc")
        c_prop = _make_constraint(ctype=ConstraintType.PROPERTY, value="abc")

        results = {"current_knowledge": content, "search_results": []}
        ev_stat = strat._quick_evidence_check(results, candidate, c_stat)
        ev_prop = strat._quick_evidence_check(results, candidate, c_prop)
        # STATISTIC multiplier (1.1) > PROPERTY multiplier (0.95)
        assert ev_stat.confidence > ev_prop.confidence

    def test_confidence_capped_at_one(self):
        """Confidence never exceeds 1.0."""
        strat = _make_strategy()
        candidate = Candidate(name="x")
        constraint = _make_constraint(ctype=ConstraintType.STATISTIC, value="x")
        # Lots of repetition to push score up
        content = ("x " * 500).strip()
        results = {
            "current_knowledge": content,
            "search_results": [
                {"title": "x", "snippet": "x"} for _ in range(20)
            ],
        }
        evidence = strat._quick_evidence_check(results, candidate, constraint)
        assert evidence.confidence <= 1.0

    def test_none_constraint_value_no_crash(self):
        """None constraint value does not crash _quick_evidence_check."""
        strat = _make_strategy()
        candidate = Candidate(name="Test Entity")
        constraint = _make_constraint(value=None)
        results = {
            "current_knowledge": "Some content about various topics.",
            "search_results": [],
        }
        evidence = strat._quick_evidence_check(results, candidate, constraint)
        assert isinstance(evidence, Evidence)
