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

Tests focus on dataclass defaults, constructor defaults, pure logic methods
(score calculations, pattern matching, diversity, filtering) without
requiring LLM or search infrastructure.
"""

from datetime import datetime, UTC
from unittest.mock import MagicMock, patch

import pytest

from local_deep_research.advanced_search_system.strategies.evidence_based_strategy_v2 import (
    EnhancedEvidenceBasedStrategy,
    QueryPattern,
    SourceProfile,
)
from local_deep_research.advanced_search_system.constraints.base_constraint import (
    Constraint,
    ConstraintType,
)
from local_deep_research.advanced_search_system.candidates.base_candidate import (
    Candidate,
)


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


def _make_strategy(**overrides):
    """Create an EnhancedEvidenceBasedStrategy with __init__ bypassed."""
    with patch.object(
        EnhancedEvidenceBasedStrategy, "__init__", lambda self, *a, **kw: None
    ):
        s = EnhancedEvidenceBasedStrategy.__new__(EnhancedEvidenceBasedStrategy)
    # Set common defaults that many methods rely on
    s.model = MagicMock()
    s.search = MagicMock()
    s.candidates = []
    s.constraints = []
    s.constraint_relationships = {}
    s.failed_queries = set()
    s.query_patterns = {}
    s.source_profiles = {}
    s.enable_pattern_learning = True
    s.candidate_limit = 20
    s.evidence_threshold = 0.6
    s.min_candidates_threshold = 10
    s.max_iterations = 20
    s.iteration = 0
    s.progress_callback = None
    for k, v in overrides.items():
        setattr(s, k, v)
    return s


def _make_constraint(
    id="c1",
    ctype=ConstraintType.PROPERTY,
    description="test",
    value="blue eyes",
    weight=1.0,
):
    return Constraint(
        id=id, type=ctype, description=description, value=value, weight=weight
    )


# ===========================================================================
# QueryPattern dataclass
# ===========================================================================
class TestQueryPatternDataclass:
    """Tests for the QueryPattern dataclass defaults and field types."""

    def test_defaults_success_rate(self):
        qp = QueryPattern(pattern="test")
        assert qp.success_rate == 0.0

    def test_defaults_usage_count(self):
        qp = QueryPattern(pattern="test")
        assert qp.usage_count == 0

    def test_defaults_constraint_types_empty_list(self):
        qp = QueryPattern(pattern="test")
        assert qp.constraint_types == []

    def test_constraint_types_list_independence(self):
        """Each instance should get its own list."""
        qp1 = QueryPattern(pattern="a")
        qp2 = QueryPattern(pattern="b")
        qp1.constraint_types.append(ConstraintType.PROPERTY)
        assert qp2.constraint_types == []

    def test_explicit_values(self):
        qp = QueryPattern(
            pattern="{value}",
            success_rate=0.8,
            usage_count=5,
            constraint_types=[ConstraintType.EVENT],
        )
        assert qp.pattern == "{value}"
        assert qp.success_rate == 0.8
        assert qp.usage_count == 5
        assert qp.constraint_types == [ConstraintType.EVENT]


# ===========================================================================
# SourceProfile dataclass
# ===========================================================================
class TestSourceProfileDataclass:
    """Tests for the SourceProfile dataclass defaults and field types."""

    def test_defaults_success_rate(self):
        sp = SourceProfile(source_name="wiki")
        assert sp.success_rate == 0.0

    def test_defaults_usage_count(self):
        sp = SourceProfile(source_name="wiki")
        assert sp.usage_count == 0

    def test_defaults_specialties_empty_list(self):
        sp = SourceProfile(source_name="wiki")
        assert sp.specialties == []

    def test_defaults_last_used_none(self):
        sp = SourceProfile(source_name="wiki")
        assert sp.last_used is None

    def test_specialties_list_independence(self):
        sp1 = SourceProfile(source_name="a")
        sp2 = SourceProfile(source_name="b")
        sp1.specialties.append("characters")
        assert sp2.specialties == []

    def test_explicit_values(self):
        now = datetime.now(UTC)
        sp = SourceProfile(
            source_name="imdb",
            success_rate=0.9,
            usage_count=10,
            specialties=["tv_shows"],
            last_used=now,
        )
        assert sp.source_name == "imdb"
        assert sp.success_rate == 0.9
        assert sp.usage_count == 10
        assert sp.last_used is now


# ===========================================================================
# Constructor defaults (via bypassed __init__)
# ===========================================================================
class TestConstructorDefaults:
    """Verify default attribute values set during real __init__."""

    def test_initialize_patterns_returns_dict(self):
        s = _make_strategy()
        patterns = s._initialize_patterns()
        assert isinstance(patterns, dict)
        assert len(patterns) >= 5

    def test_initialize_patterns_contains_expected_keys(self):
        s = _make_strategy()
        patterns = s._initialize_patterns()
        expected = {
            "property_basic",
            "property_character",
            "event_show",
            "statistic_episodes",
            "cross_constraint",
            "semantic_expansion",
        }
        assert expected.issubset(patterns.keys())

    def test_initialize_sources_returns_dict(self):
        s = _make_strategy()
        sources = s._initialize_sources()
        assert isinstance(sources, dict)
        assert "wikipedia" in sources
        assert "imdb" in sources
        assert "web" in sources

    def test_initialize_sources_profiles_have_specialties(self):
        s = _make_strategy()
        sources = s._initialize_sources()
        assert "characters" in sources["wikipedia"].specialties
        assert "tv_shows" in sources["imdb"].specialties


# ===========================================================================
# _update_pattern_success
# ===========================================================================
class TestUpdatePatternSuccess:
    """Tests for the exponential moving average pattern update."""

    def test_success_increases_rate_from_zero(self):
        s = _make_strategy()
        qp = QueryPattern(pattern="test", success_rate=0.0, usage_count=0)
        s._update_pattern_success(qp, success=True)
        assert qp.usage_count == 1
        # alpha=0.3, new = 0.3*1 + 0.7*0.0 = 0.3
        assert qp.success_rate == pytest.approx(0.3)

    def test_failure_decreases_rate(self):
        s = _make_strategy()
        qp = QueryPattern(pattern="test", success_rate=1.0, usage_count=5)
        s._update_pattern_success(qp, success=False)
        assert qp.usage_count == 6
        # 0.7 * 1.0 = 0.7
        assert qp.success_rate == pytest.approx(0.7)

    def test_repeated_success_converges_to_one(self):
        s = _make_strategy()
        qp = QueryPattern(pattern="test", success_rate=0.0)
        for _ in range(50):
            s._update_pattern_success(qp, success=True)
        assert qp.success_rate > 0.99


# ===========================================================================
# _update_source_profile
# ===========================================================================
class TestUpdateSourceProfile:
    """Tests for source profile update logic."""

    def test_high_confidence_updates_success(self):
        s = _make_strategy()
        profile = SourceProfile(
            source_name="wiki", success_rate=0.0, usage_count=0
        )
        s.source_profiles = {"wiki": profile}
        s._update_source_profile("wiki", confidence=0.8)
        assert profile.usage_count == 1
        assert profile.success_rate == pytest.approx(0.3)  # alpha*1 + 0.7*0
        assert profile.last_used is not None

    def test_low_confidence_does_not_increase(self):
        s = _make_strategy()
        profile = SourceProfile(
            source_name="wiki", success_rate=1.0, usage_count=0
        )
        s.source_profiles = {"wiki": profile}
        s._update_source_profile("wiki", confidence=0.3)  # below threshold
        # 0.3*0 + 0.7*1.0 = 0.7
        assert profile.success_rate == pytest.approx(0.7)

    def test_unknown_source_is_ignored(self):
        s = _make_strategy()
        s.source_profiles = {}
        # Should not raise
        s._update_source_profile("nonexistent", confidence=0.9)


# ===========================================================================
# _find_common_terms
# ===========================================================================
class TestFindCommonTerms:
    """Tests for finding common terms between two constraints."""

    def test_common_terms_found(self):
        s = _make_strategy()
        c1 = _make_constraint(value="blue eyes character")
        c2 = _make_constraint(id="c2", value="character with wings")
        result = s._find_common_terms(c1, c2)
        assert "character" in result

    def test_no_common_terms(self):
        s = _make_strategy()
        c1 = _make_constraint(value="blue eyes")
        c2 = _make_constraint(id="c2", value="red wings")
        result = s._find_common_terms(c1, c2)
        assert result == []

    def test_case_insensitive(self):
        s = _make_strategy()
        c1 = _make_constraint(value="Blue Eyes")
        c2 = _make_constraint(id="c2", value="blue sky")
        result = s._find_common_terms(c1, c2)
        assert "blue" in result


# ===========================================================================
# _is_common_word
# ===========================================================================
class TestIsCommonWord:
    """Tests for common-word filtering."""

    def test_common_words_detected(self):
        s = _make_strategy()
        for word in ["the", "and", "is", "of", "with"]:
            assert s._is_common_word(word) is True

    def test_proper_names_not_common(self):
        s = _make_strategy()
        assert s._is_common_word("Homer") is False
        assert s._is_common_word("SpongeBob") is False

    def test_case_insensitive(self):
        s = _make_strategy()
        assert s._is_common_word("THE") is True
        assert s._is_common_word("And") is True


# ===========================================================================
# _calculate_source_diversity
# ===========================================================================
class TestCalculateSourceDiversity:
    """Tests for the entropy-based source diversity calculation."""

    def test_no_profiles_returns_zero(self):
        s = _make_strategy(source_profiles={})
        assert s._calculate_source_diversity() == 0.0

    def test_no_used_sources_returns_zero(self):
        s = _make_strategy(
            source_profiles={
                "a": SourceProfile(source_name="a", usage_count=0),
                "b": SourceProfile(source_name="b", usage_count=0),
            }
        )
        assert s._calculate_source_diversity() == 0.0

    def test_single_source_returns_one(self):
        """One used source -> entropy=0, max_entropy=0, edge case."""
        s = _make_strategy(
            source_profiles={
                "a": SourceProfile(source_name="a", usage_count=5),
            }
        )
        # log2(1/1) = 0, so max_entropy = 0 -> returns 0.0
        # This is the defined behavior for a single source
        result = s._calculate_source_diversity()
        assert result == 0.0

    def test_uniform_usage_returns_one(self):
        """Uniform usage across N sources should yield diversity ~ 1.0."""
        s = _make_strategy(
            source_profiles={
                "a": SourceProfile(source_name="a", usage_count=10),
                "b": SourceProfile(source_name="b", usage_count=10),
                "c": SourceProfile(source_name="c", usage_count=10),
            }
        )
        assert s._calculate_source_diversity() == pytest.approx(1.0)

    def test_skewed_usage_below_one(self):
        """Heavily skewed usage should yield diversity < 1.0."""
        s = _make_strategy(
            source_profiles={
                "a": SourceProfile(source_name="a", usage_count=100),
                "b": SourceProfile(source_name="b", usage_count=1),
            }
        )
        result = s._calculate_source_diversity()
        assert 0.0 < result < 1.0


# ===========================================================================
# _get_specialized_sources
# ===========================================================================
class TestGetSpecializedSources:
    """Tests for constraint-type to source mapping."""

    def test_property_type(self):
        s = _make_strategy()
        result = s._get_specialized_sources(ConstraintType.PROPERTY)
        assert "fandom" in result
        assert "wikipedia" in result

    def test_event_type(self):
        s = _make_strategy()
        result = s._get_specialized_sources(ConstraintType.EVENT)
        assert "imdb" in result

    def test_unknown_type_returns_web(self):
        s = _make_strategy()
        result = s._get_specialized_sources(ConstraintType.EXISTENCE)
        assert result == ["web"]


# ===========================================================================
# _apply_pattern_to_constraint
# ===========================================================================
class TestApplyPatternToConstraint:
    """Tests for pattern application to constraints."""

    def test_basic_replacement(self):
        s = _make_strategy()
        qp = QueryPattern(pattern="{value} {constraint_type}")
        c = _make_constraint(value="blue eyes", ctype=ConstraintType.PROPERTY)
        result = s._apply_pattern_to_constraint(qp, c)
        assert result == "blue eyes property"

    def test_synonym_placeholders_trigger_llm(self):
        s = _make_strategy()
        s.model.invoke.return_value = MagicMock(content="azure\ncobalt")
        qp = QueryPattern(pattern='"{value}" OR "{synonym1}" OR "{synonym2}"')
        c = _make_constraint(value="blue")
        result = s._apply_pattern_to_constraint(qp, c)
        assert '"blue"' in result
        assert '"azure"' in result


# ===========================================================================
# _build_cross_constraint_query
# ===========================================================================
class TestBuildCrossConstraintQuery:
    """Tests for cross-constraint query building."""

    def test_no_common_terms_uses_and(self):
        s = _make_strategy()
        c1 = _make_constraint(value="blue eyes", ctype=ConstraintType.PROPERTY)
        c2 = _make_constraint(
            id="c2", value="red hair", ctype=ConstraintType.STATISTIC
        )
        result = s._build_cross_constraint_query(c1, c2)
        assert "AND" in result

    def test_common_terms_used_as_base(self):
        s = _make_strategy()
        c1 = _make_constraint(
            value="TV character blue", ctype=ConstraintType.PROPERTY
        )
        c2 = _make_constraint(
            id="c2", value="TV show event", ctype=ConstraintType.EVENT
        )
        result = s._build_cross_constraint_query(c1, c2)
        assert "tv" in result.lower()

    def test_property_event_adds_tv_show_suffix(self):
        s = _make_strategy()
        c1 = _make_constraint(value="blue eyes", ctype=ConstraintType.PROPERTY)
        c2 = _make_constraint(
            id="c2", value="cancelled", ctype=ConstraintType.EVENT
        )
        result = s._build_cross_constraint_query(c1, c2)
        assert "TV show character" in result


# ===========================================================================
# _add_unique_candidates
# ===========================================================================
class TestAddUniqueCandidates:
    """Tests for deduplication and limit logic."""

    def test_deduplicates_by_name_case_insensitive(self):
        s = _make_strategy()
        s.candidates = [Candidate(name="Homer")]
        s._add_unique_candidates(
            [Candidate(name="homer"), Candidate(name="Bart")]
        )
        names = [c.name for c in s.candidates]
        assert len(names) == 2
        assert "Bart" in names

    def test_respects_candidate_limit(self):
        s = _make_strategy(candidate_limit=3)
        s.candidates = []
        s._add_unique_candidates([Candidate(name=f"c{i}") for i in range(10)])
        assert len(s.candidates) == 3


# ===========================================================================
# _generate_semantic_variations
# ===========================================================================
class TestGenerateSemanticVariations:
    """Tests for semantic query variation generation."""

    def test_property_type_variations(self):
        s = _make_strategy()
        c = _make_constraint(value="blue eyes", ctype=ConstraintType.PROPERTY)
        variations = s._generate_semantic_variations(c)
        assert any("entity with blue eyes" in v for v in variations)

    def test_event_type_variations(self):
        s = _make_strategy()
        c = _make_constraint(value="cancelled 2020", ctype=ConstraintType.EVENT)
        variations = s._generate_semantic_variations(c)
        assert any("event" in v for v in variations)

    def test_statistic_type_variations(self):
        s = _make_strategy()
        c = _make_constraint(
            value="100 episodes", ctype=ConstraintType.STATISTIC
        )
        variations = s._generate_semantic_variations(c)
        assert any("number" in v or "count" in v for v in variations)

    def test_max_five_variations(self):
        s = _make_strategy()
        c = _make_constraint(value="test", ctype=ConstraintType.PROPERTY)
        variations = s._generate_semantic_variations(c)
        assert len(variations) <= 5


# ===========================================================================
# _learn_query_pattern
# ===========================================================================
class TestLearnQueryPattern:
    """Tests for pattern learning from query outcomes."""

    def test_learning_disabled_does_nothing(self):
        s = _make_strategy(enable_pattern_learning=False)
        s.query_patterns = {}
        c = _make_constraint()
        s._learn_query_pattern("test query", c, success=True)
        assert len(s.query_patterns) == 0

    def test_learning_enabled_creates_pattern(self):
        s = _make_strategy(enable_pattern_learning=True)
        s.query_patterns = {}
        c = _make_constraint(ctype=ConstraintType.PROPERTY)
        s._learn_query_pattern("test query", c, success=True)
        assert "property_custom" in s.query_patterns

    def test_learning_updates_existing_pattern(self):
        s = _make_strategy(enable_pattern_learning=True)
        qp = QueryPattern(
            pattern="old",
            constraint_types=[ConstraintType.PROPERTY],
            success_rate=0.5,
            usage_count=3,
        )
        s.query_patterns = {"property_custom": qp}
        c = _make_constraint(ctype=ConstraintType.PROPERTY)
        s._learn_query_pattern("new query", c, success=True)
        assert qp.usage_count == 4
        assert qp.success_rate > 0.5


# ===========================================================================
# _generate_relaxed_queries
# ===========================================================================
class TestGenerateRelaxedQueries:
    """Tests for relaxed/fallback query generation."""

    def test_returns_queries_from_top_constraints(self):
        s = _make_strategy()
        s.constraints = [
            _make_constraint(id="c1", value="blue eyes", weight=2.0),
            _make_constraint(id="c2", value="red hair", weight=1.0),
        ]
        queries = s._generate_relaxed_queries()
        assert "blue eyes" in queries
        assert any("property" in q for q in queries)

    def test_empty_constraints_returns_empty(self):
        s = _make_strategy()
        s.constraints = []
        queries = s._generate_relaxed_queries()
        assert queries == []
