"""
Pure-logic tests for EnhancedEvidenceBasedStrategy.

Tests algorithmic methods that require no LLM or search calls:
pattern initialization, source diversity scoring, deduplication,
semantic variation generation, and pattern application.
"""

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.evidence_based_strategy_v2 import (
    EnhancedEvidenceBasedStrategy,
    QueryPattern,
    SourceProfile,
)


def _make_strategy(**overrides):
    """Build a minimal Mock with attributes that the pure-logic methods read."""
    s = Mock(spec=[])
    s.candidates = overrides.get("candidates", [])
    s.candidate_limit = overrides.get("candidate_limit", 20)
    s.source_profiles = overrides.get("source_profiles", {})
    return s


# ---------------------------------------------------------------------------
# _initialize_patterns
# ---------------------------------------------------------------------------


class TestInitializePatterns:
    """Verify _initialize_patterns returns correct pattern catalogue."""

    def test_returns_dict(self):
        """Should return a dict of QueryPattern instances."""
        result = EnhancedEvidenceBasedStrategy._initialize_patterns(Mock())
        assert isinstance(result, dict)

    def test_expected_keys(self):
        """All six named patterns should be present."""
        result = EnhancedEvidenceBasedStrategy._initialize_patterns(Mock())
        expected = {
            "property_basic",
            "property_character",
            "event_show",
            "statistic_episodes",
            "cross_constraint",
            "semantic_expansion",
        }
        assert set(result.keys()) == expected

    def test_property_patterns_have_property_type(self):
        """Property patterns should list PROPERTY in constraint_types."""
        result = EnhancedEvidenceBasedStrategy._initialize_patterns(Mock())
        for key in ("property_basic", "property_character"):
            assert ConstraintType.PROPERTY in result[key].constraint_types

    def test_event_pattern_has_event_type(self):
        """Event pattern should list EVENT in constraint_types."""
        result = EnhancedEvidenceBasedStrategy._initialize_patterns(Mock())
        assert ConstraintType.EVENT in result["event_show"].constraint_types

    def test_statistic_pattern_has_statistic_type(self):
        """Statistic pattern should list STATISTIC in constraint_types."""
        result = EnhancedEvidenceBasedStrategy._initialize_patterns(Mock())
        assert (
            ConstraintType.STATISTIC
            in result["statistic_episodes"].constraint_types
        )

    def test_cross_constraint_has_empty_types(self):
        """Cross-constraint pattern is type-agnostic."""
        result = EnhancedEvidenceBasedStrategy._initialize_patterns(Mock())
        assert result["cross_constraint"].constraint_types == []

    def test_patterns_are_querypattern_instances(self):
        """Every value should be a QueryPattern dataclass."""
        result = EnhancedEvidenceBasedStrategy._initialize_patterns(Mock())
        for v in result.values():
            assert isinstance(v, QueryPattern)


# ---------------------------------------------------------------------------
# _initialize_sources
# ---------------------------------------------------------------------------


class TestInitializeSources:
    """Verify _initialize_sources returns correct source catalogue."""

    def test_returns_dict(self):
        """Should return a dict of SourceProfile instances."""
        result = EnhancedEvidenceBasedStrategy._initialize_sources(Mock())
        assert isinstance(result, dict)

    def test_expected_sources(self):
        """All five named sources should be present."""
        result = EnhancedEvidenceBasedStrategy._initialize_sources(Mock())
        expected = {"wikipedia", "imdb", "fandom", "tv_databases", "web"}
        assert set(result.keys()) == expected

    def test_source_names_match_keys(self):
        """Each source_name field should match its dict key."""
        result = EnhancedEvidenceBasedStrategy._initialize_sources(Mock())
        for key, profile in result.items():
            assert profile.source_name == key

    def test_profiles_are_sourceprofile_instances(self):
        """Every value should be a SourceProfile dataclass."""
        result = EnhancedEvidenceBasedStrategy._initialize_sources(Mock())
        for v in result.values():
            assert isinstance(v, SourceProfile)

    def test_wikipedia_specialties(self):
        """Wikipedia should cover characters, properties, general."""
        result = EnhancedEvidenceBasedStrategy._initialize_sources(Mock())
        assert "characters" in result["wikipedia"].specialties
        assert "general" in result["wikipedia"].specialties


# ---------------------------------------------------------------------------
# _calculate_source_diversity  (Shannon entropy)
# ---------------------------------------------------------------------------


class TestCalculateSourceDiversity:
    """Verify Shannon-entropy source diversity calculation."""

    def _call(self, profiles):
        s = _make_strategy(source_profiles=profiles)
        return EnhancedEvidenceBasedStrategy._calculate_source_diversity(s)

    def test_empty_profiles(self):
        """No profiles at all -> 0.0."""
        assert self._call({}) == 0.0

    def test_no_usage(self):
        """Profiles exist but none used -> 0.0."""
        profiles = {
            "a": SourceProfile(source_name="a", usage_count=0),
            "b": SourceProfile(source_name="b", usage_count=0),
        }
        assert self._call(profiles) == 0.0

    def test_single_source_used(self):
        """Only one source used -> 0.0 (log2(1) = 0)."""
        profiles = {
            "a": SourceProfile(source_name="a", usage_count=5),
            "b": SourceProfile(source_name="b", usage_count=0),
        }
        # Only one source in used_sources -> max_entropy = -log2(1/1) = 0
        # Division by zero guard returns 0.0
        assert self._call(profiles) == 0.0

    def test_uniform_two_sources(self):
        """Two sources used equally -> diversity = 1.0."""
        profiles = {
            "a": SourceProfile(source_name="a", usage_count=10),
            "b": SourceProfile(source_name="b", usage_count=10),
        }
        assert self._call(profiles) == 1.0

    def test_uniform_three_sources(self):
        """Three sources used equally -> diversity = 1.0."""
        profiles = {
            "a": SourceProfile(source_name="a", usage_count=5),
            "b": SourceProfile(source_name="b", usage_count=5),
            "c": SourceProfile(source_name="c", usage_count=5),
        }
        assert abs(self._call(profiles) - 1.0) < 1e-9

    def test_skewed_distribution(self):
        """Skewed usage -> 0 < diversity < 1."""
        profiles = {
            "a": SourceProfile(source_name="a", usage_count=90),
            "b": SourceProfile(source_name="b", usage_count=10),
        }
        diversity = self._call(profiles)
        assert 0.0 < diversity < 1.0

    def test_heavily_skewed(self):
        """Very skewed usage -> close to 0 but positive."""
        profiles = {
            "a": SourceProfile(source_name="a", usage_count=999),
            "b": SourceProfile(source_name="b", usage_count=1),
        }
        diversity = self._call(profiles)
        assert 0.0 < diversity < 0.2


# ---------------------------------------------------------------------------
# _find_common_terms
# ---------------------------------------------------------------------------


class TestFindCommonTerms:
    """Verify set-intersection of constraint value words."""

    def _call(self, val1, val2):
        c1 = Constraint(
            id="c1", type=ConstraintType.PROPERTY, description="", value=val1
        )
        c2 = Constraint(
            id="c2", type=ConstraintType.PROPERTY, description="", value=val2
        )
        return EnhancedEvidenceBasedStrategy._find_common_terms(Mock(), c1, c2)

    def test_overlapping_terms(self):
        """Shared words should be returned."""
        result = self._call("red blue green", "blue green yellow")
        assert set(result) == {"blue", "green"}

    def test_no_overlap(self):
        """No shared words -> empty list."""
        assert self._call("cat dog", "fish bird") == []

    def test_case_insensitive(self):
        """Matching should be case-insensitive (lowered)."""
        result = self._call("Alpha Beta", "alpha gamma")
        assert "alpha" in result

    def test_empty_values(self):
        """Empty constraint values -> empty result."""
        # split() on empty string returns [] -> empty set intersection
        assert self._call("", "") == []


# ---------------------------------------------------------------------------
# _is_common_word
# ---------------------------------------------------------------------------


class TestIsCommonWord:
    """Verify stopword detection."""

    def _call(self, name):
        return EnhancedEvidenceBasedStrategy._is_common_word(Mock(), name)

    def test_common_word(self):
        """'the' is a common word."""
        assert self._call("the") is True

    def test_proper_name(self):
        """A proper name should not be flagged."""
        assert self._call("Elizabeth") is False

    def test_case_insensitive(self):
        """'THE' should still be detected."""
        assert self._call("THE") is True

    def test_preposition(self):
        """'from' is in the common word set."""
        assert self._call("from") is True

    def test_uncommon_short_word(self):
        """'ax' is short but not in the set."""
        assert self._call("ax") is False


# ---------------------------------------------------------------------------
# _add_unique_candidates
# ---------------------------------------------------------------------------


class TestAddUniqueCandidates:
    """Verify deduplication and limit enforcement."""

    def test_adds_new_candidates(self):
        """New candidates are appended."""
        s = _make_strategy(candidates=[], candidate_limit=10)
        new = [Candidate(name="Alice"), Candidate(name="Bob")]
        EnhancedEvidenceBasedStrategy._add_unique_candidates(s, new)
        names = [c.name for c in s.candidates]
        assert names == ["Alice", "Bob"]

    def test_deduplicates_by_name(self):
        """Duplicate names (case-insensitive) are skipped."""
        s = _make_strategy(
            candidates=[Candidate(name="Alice")],
            candidate_limit=10,
        )
        new = [Candidate(name="alice"), Candidate(name="Bob")]
        EnhancedEvidenceBasedStrategy._add_unique_candidates(s, new)
        names = [c.name for c in s.candidates]
        assert names == ["Alice", "Bob"]

    def test_enforces_limit(self):
        """Total candidates should not exceed candidate_limit."""
        s = _make_strategy(candidates=[], candidate_limit=3)
        new = [Candidate(name=f"C{i}") for i in range(5)]
        EnhancedEvidenceBasedStrategy._add_unique_candidates(s, new)
        assert len(s.candidates) == 3

    def test_empty_input(self):
        """Empty new-candidates list doesn't modify existing."""
        existing = [Candidate(name="Alice")]
        s = _make_strategy(candidates=existing, candidate_limit=10)
        EnhancedEvidenceBasedStrategy._add_unique_candidates(s, [])
        assert len(s.candidates) == 1


# ---------------------------------------------------------------------------
# _generate_semantic_variations
# ---------------------------------------------------------------------------


class TestGenerateSemanticVariations:
    """Verify query variation generation by constraint type."""

    def _call(self, ctype, value="test value"):
        c = Constraint(id="c1", type=ctype, description="desc", value=value)
        return EnhancedEvidenceBasedStrategy._generate_semantic_variations(
            Mock(), c
        )

    def test_property_variations(self):
        """PROPERTY type should produce 'entity with …' style variations."""
        result = self._call(ConstraintType.PROPERTY)
        assert any("entity with" in v for v in result)

    def test_event_variations(self):
        """EVENT type should produce 'when …' style variation."""
        result = self._call(ConstraintType.EVENT)
        assert any("when" in v for v in result)

    def test_statistic_variations(self):
        """STATISTIC type should produce 'number …' style variation."""
        result = self._call(ConstraintType.STATISTIC)
        assert any("number" in v for v in result)

    def test_unknown_type_generic(self):
        """Non-standard type falls through to generic variations."""
        result = self._call(ConstraintType.LOCATION)
        assert any(
            "specific" in v or "about" in v or "regarding" in v for v in result
        )

    def test_capped_at_five(self):
        """No more than 5 variations returned."""
        result = self._call(ConstraintType.PROPERTY)
        assert len(result) <= 5

    def test_variations_contain_value(self):
        """All variations should incorporate the constraint value."""
        result = self._call(ConstraintType.PROPERTY, "blue eyes")
        assert all("blue eyes" in v for v in result)


# ---------------------------------------------------------------------------
# _apply_pattern_to_constraint
# ---------------------------------------------------------------------------


class TestApplyPatternToConstraint:
    """Verify placeholder substitution in query patterns."""

    def _call(self, pattern_str, value, ctype=ConstraintType.PROPERTY):
        pattern = QueryPattern(pattern=pattern_str, constraint_types=[])
        constraint = Constraint(
            id="c1", type=ctype, description="", value=value
        )
        return EnhancedEvidenceBasedStrategy._apply_pattern_to_constraint(
            Mock(), pattern, constraint
        )

    def test_value_substitution(self):
        """'{value}' placeholder is replaced."""
        result = self._call("{value} query", "blue eyes")
        assert result == "blue eyes query"

    def test_constraint_type_substitution(self):
        """'{constraint_type}' placeholder is replaced with enum value."""
        result = self._call(
            "{value} {constraint_type}", "test", ConstraintType.EVENT
        )
        assert result == "test event"

    def test_no_placeholders(self):
        """Pattern without placeholders is returned unchanged."""
        result = self._call("static query", "ignored")
        assert result == "static query"

    def test_both_placeholders(self):
        """Both {value} and {constraint_type} replaced in one pass."""
        result = self._call(
            "find {value} type {constraint_type}", "X", ConstraintType.STATISTIC
        )
        assert result == "find X type statistic"
