"""
Pure-logic tests for ConstrainedSearchStrategy.

Tests restrictiveness scoring, constraint ranking, candidate deduplication,
and query generation — 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.strategies.constrained_search_strategy import (
    ConstrainedSearchStrategy,
)


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


def _make_strategy(constraints=None):
    """Build a minimal Mock with the attributes read by pure-logic methods."""
    s = Mock(spec=[])
    s.constraints = constraints or []
    return s


# ---------------------------------------------------------------------------
# _calculate_restrictiveness_score
# ---------------------------------------------------------------------------


class TestCalculateRestrictivenessScore:
    """Verify type-based and value-based scoring components."""

    def _score(self, ctype=ConstraintType.PROPERTY, value="v"):
        c = _constraint("c1", ctype, value)
        return ConstrainedSearchStrategy._calculate_restrictiveness_score(
            Mock(), c
        )

    def test_statistic_highest_type_score(self):
        """STATISTIC type gets 10 points."""
        assert self._score(ConstraintType.STATISTIC) >= 10

    def test_event_type_score(self):
        """EVENT type gets 8 points."""
        assert self._score(ConstraintType.EVENT, "something") == 8

    def test_location_type_score(self):
        """LOCATION type gets 6 points."""
        assert self._score(ConstraintType.LOCATION, "something") == 6

    def test_property_type_score(self):
        """PROPERTY type gets 4 points."""
        assert self._score(ConstraintType.PROPERTY, "something") == 4

    def test_digits_bonus(self):
        """Values containing digits get +5."""
        base = self._score(ConstraintType.PROPERTY, "abc")
        with_digit = self._score(ConstraintType.PROPERTY, "abc 42")
        assert with_digit - base == 5

    def test_long_value_bonus(self):
        """Values with > 3 words get +3."""
        short = self._score(ConstraintType.PROPERTY, "one two three")
        long = self._score(ConstraintType.PROPERTY, "one two three four")
        assert long - short == 3

    def test_specificity_keywords_bonus(self):
        """Values with 'specific', 'exact', 'only', or 'must' get +2."""
        base = self._score(ConstraintType.PROPERTY, "something")
        with_kw = self._score(ConstraintType.PROPERTY, "exact something")
        assert with_kw - base == 2

    def test_combined_bonuses(self):
        """Digits + long value + keyword = +10 total bonus."""
        # PROPERTY(4) + digits(5) + long(3) + keyword(2) = 14
        score = self._score(
            ConstraintType.PROPERTY,
            "must have exactly 100 specific items",
        )
        assert score == 14

    def test_empty_value(self):
        """Empty string value adds no bonus (falsy guard)."""
        score = self._score(ConstraintType.PROPERTY, "")
        assert score == 4  # Type score only


# ---------------------------------------------------------------------------
# _rank_constraints_by_restrictiveness
# ---------------------------------------------------------------------------


class TestRankConstraintsByRestrictiveness:
    """Verify that constraints are sorted most-restrictive-first."""

    def test_type_ordering(self):
        """STATISTIC > EVENT > LOCATION > PROPERTY."""
        s = _make_strategy(
            constraints=[
                _constraint("prop", ConstraintType.PROPERTY, "alpha"),
                _constraint("stat", ConstraintType.STATISTIC, "alpha"),
                _constraint("evt", ConstraintType.EVENT, "alpha"),
                _constraint("loc", ConstraintType.LOCATION, "alpha"),
            ]
        )
        ranked = ConstrainedSearchStrategy._rank_constraints_by_restrictiveness(
            s
        )
        ids = [c.id for c in ranked]
        assert ids == ["stat", "evt", "loc", "prop"]

    def test_bonus_can_reorder(self):
        """A PROPERTY with many bonuses can outrank a plain EVENT."""
        s = _make_strategy(
            constraints=[
                _constraint("evt", ConstraintType.EVENT, "plain"),  # 8
                _constraint(
                    "prop",
                    ConstraintType.PROPERTY,
                    "must have exact 500 specific items",  # 4+5+3+2 = 14
                ),
            ]
        )
        ranked = ConstrainedSearchStrategy._rank_constraints_by_restrictiveness(
            s
        )
        assert ranked[0].id == "prop"

    def test_empty_constraints(self):
        """Empty list returns empty."""
        s = _make_strategy(constraints=[])
        assert (
            ConstrainedSearchStrategy._rank_constraints_by_restrictiveness(s)
            == []
        )


# ---------------------------------------------------------------------------
# _deduplicate_candidates
# ---------------------------------------------------------------------------


class TestDeduplicateCandidates:
    """Verify case-insensitive, strip-based deduplication."""

    def _dedup(self, names):
        candidates = [Candidate(name=n) for n in names]
        return ConstrainedSearchStrategy._deduplicate_candidates(
            Mock(), candidates
        )

    def test_no_duplicates(self):
        """Distinct names all kept."""
        result = self._dedup(["Alice", "Bob"])
        assert len(result) == 2

    def test_case_insensitive(self):
        """'alice' and 'Alice' are duplicates."""
        result = self._dedup(["Alice", "alice"])
        assert len(result) == 1

    def test_strips_whitespace(self):
        """Leading/trailing whitespace is ignored."""
        result = self._dedup(["Alice", " Alice "])
        assert len(result) == 1

    def test_preserves_first(self):
        """First occurrence is kept."""
        result = self._dedup(["Alice", "ALICE"])
        assert result[0].name == "Alice"

    def test_empty_list(self):
        """Empty input returns empty."""
        assert self._dedup([]) == []


# ---------------------------------------------------------------------------
# _generate_additional_queries
# ---------------------------------------------------------------------------


class TestGenerateAdditionalQueries:
    """Verify type-branched query template generation."""

    def _call(self, ctype, value="test data"):
        c = _constraint("c1", ctype, value)
        return ConstrainedSearchStrategy._generate_additional_queries(Mock(), c)

    def test_always_has_reference_queries(self):
        """All types get reference/authoritative/official queries."""
        result = self._call(ConstraintType.PROPERTY)
        assert any("reference" in q for q in result)
        assert any("authoritative" in q for q in result)
        assert any("official" in q for q in result)

    def test_statistic_gets_dataset_queries(self):
        """STATISTIC type gets spreadsheet/dataset/statistical analysis."""
        result = self._call(ConstraintType.STATISTIC)
        assert any("spreadsheet" in q or "dataset" in q for q in result)

    def test_property_gets_characterized_queries(self):
        """PROPERTY type gets 'characterized by' etc."""
        result = self._call(ConstraintType.PROPERTY)
        assert any("characterized by" in q for q in result)

    def test_other_gets_generic(self):
        """Other types get exhaustive/thorough/detailed."""
        result = self._call(ConstraintType.LOCATION)
        assert any("exhaustive" in q or "thorough" in q for q in result)

    def test_value_in_queries(self):
        """All queries contain the constraint value."""
        result = self._call(ConstraintType.PROPERTY, "blue eyes")
        assert all("blue eyes" in q for q in result)


# ---------------------------------------------------------------------------
# _generate_constraint_specific_queries
# ---------------------------------------------------------------------------


class TestGenerateConstraintSpecificQueries:
    """Verify type-specific query expansion with context from other constraints."""

    def _call(self, ctype, value="test data", other_constraints=None):
        c = _constraint("c1", ctype, value)
        s = _make_strategy(constraints=[c] + (other_constraints or []))
        return ConstrainedSearchStrategy._generate_constraint_specific_queries(
            s, c
        )

    def test_statistic_queries(self):
        """STATISTIC produces list/complete/database style queries."""
        result = self._call(ConstraintType.STATISTIC)
        assert any("database" in q for q in result)
        assert any("statistics" in q for q in result)

    def test_event_queries(self):
        """EVENT produces during/timeline style queries."""
        result = self._call(ConstraintType.EVENT)
        assert any("timeline" in q for q in result)

    def test_property_queries(self):
        """PROPERTY produces with/having/characterized by style queries."""
        result = self._call(ConstraintType.PROPERTY)
        assert any("characterized by" in q for q in result)

    def test_generic_queries(self):
        """Other types get generic list/examples queries."""
        result = self._call(ConstraintType.TEMPORAL)
        assert any("list" in q for q in result)

    def test_includes_description(self):
        """Constraint description is included as a query."""
        c = Constraint(
            id="c1",
            type=ConstraintType.PROPERTY,
            description="has blue eyes",
            value="blue eyes",
        )
        s = _make_strategy(constraints=[c])
        result = (
            ConstrainedSearchStrategy._generate_constraint_specific_queries(
                s, c
            )
        )
        assert "has blue eyes" in result

    def test_context_from_other_constraints(self):
        """Context from other constraints is appended in combined queries."""
        other = _constraint("c2", ConstraintType.PROPERTY, "red hair")
        result = self._call(ConstraintType.PROPERTY, "blue eyes", [other])
        assert any("red hair" in q for q in result)
