"""High-value tests for ParallelConstrainedStrategy pure logic methods."""

import pytest
from unittest.mock import patch

from local_deep_research.advanced_search_system.constraints.base_constraint import (
    Constraint,
    ConstraintType,
)
from local_deep_research.advanced_search_system.strategies.parallel_constrained_strategy import (
    ParallelConstrainedStrategy,
    SearchCombination,
)


def make_constraint(ctype, value, weight=1.0, cid=None):
    return Constraint(
        id=cid or f"c_{value[:8]}",
        type=ctype,
        description=value,
        value=value,
        weight=weight,
    )


# --- SearchCombination dataclass ---


class TestSearchCombination:
    def test_hash_by_query(self):
        c1 = SearchCombination(constraints=[], query="foo", priority=1)
        c2 = SearchCombination(constraints=[], query="foo", priority=2)
        assert hash(c1) == hash(c2)

    def test_different_query_different_hash(self):
        c1 = SearchCombination(constraints=[], query="foo", priority=1)
        c2 = SearchCombination(constraints=[], query="bar", priority=1)
        assert hash(c1) != hash(c2)

    def test_fields(self):
        constraints = [make_constraint(ConstraintType.PROPERTY, "test")]
        combo = SearchCombination(
            constraints=constraints, query="test query", priority=5
        )
        assert combo.constraints == constraints
        assert combo.query == "test query"
        assert combo.priority == 5

    def test_same_hash_for_same_query(self):
        c1 = SearchCombination(constraints=[], query="foo", priority=1)
        c2 = SearchCombination(constraints=[], query="foo", priority=2)
        # Same hash but no __eq__ override, so set uses identity
        assert hash(c1) == hash(c2)


# --- _classify_constraints ---


@pytest.fixture
def strategy():
    """Create a ParallelConstrainedStrategy with mocked parent init."""
    with patch.object(
        ParallelConstrainedStrategy, "__init__", lambda self, *a, **kw: None
    ):
        s = ParallelConstrainedStrategy.__new__(ParallelConstrainedStrategy)
        s.parallel_workers = 100
        s.min_results_threshold = 10
        return s


class TestClassifyConstraints:
    def test_temporal_is_hard(self, strategy):
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.TEMPORAL, "in 2020"),
        ]
        strategy._classify_constraints()
        assert len(strategy.hard_constraints) == 1
        assert len(strategy.soft_constraints) == 0

    def test_statistic_is_hard(self, strategy):
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.STATISTIC, "84.5x ratio"),
        ]
        strategy._classify_constraints()
        assert len(strategy.hard_constraints) == 1

    def test_property_is_soft(self, strategy):
        """PROPERTY constraints are always soft (no keyword matching)."""
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.PROPERTY, "female character"),
        ]
        strategy._classify_constraints()
        assert len(strategy.soft_constraints) == 1
        assert len(strategy.hard_constraints) == 0

    def test_property_without_keyword_is_soft(self, strategy):
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.PROPERTY, "has blue eyes"),
        ]
        strategy._classify_constraints()
        assert len(strategy.soft_constraints) == 1
        assert len(strategy.hard_constraints) == 0

    def test_mixed_types_hard_and_soft(self, strategy):
        """TEMPORAL → hard, PROPERTY → soft."""
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.TEMPORAL, "in 2020"),
            make_constraint(ConstraintType.PROPERTY, "has blue eyes"),
        ]
        strategy._classify_constraints()
        assert len(strategy.hard_constraints) == 1
        assert len(strategy.soft_constraints) == 1


# --- _build_query ---


class TestBuildQuery:
    def test_single_constraint(self, strategy):
        strategy.entity_type = None
        c = make_constraint(ConstraintType.PROPERTY, "blue eyes")
        result = strategy._build_query([c])
        assert '"blue eyes"' in result

    def test_multi_word_quoted(self, strategy):
        strategy.entity_type = None
        c = make_constraint(ConstraintType.PROPERTY, "has blue eyes")
        result = strategy._build_query([c])
        assert '"has blue eyes"' in result

    def test_single_word_not_quoted(self, strategy):
        strategy.entity_type = None
        c = make_constraint(ConstraintType.PROPERTY, "Colorado")
        result = strategy._build_query([c])
        assert result == "Colorado"

    def test_and_joining(self, strategy):
        strategy.entity_type = None
        c1 = make_constraint(ConstraintType.PROPERTY, "Colorado")
        c2 = make_constraint(ConstraintType.PROPERTY, "mountain")
        result = strategy._build_query([c1, c2])
        assert " AND " in result

    def test_entity_type_prefix(self, strategy):
        strategy.entity_type = "TV show"
        c = make_constraint(ConstraintType.PROPERTY, "comedy")
        result = strategy._build_query([c])
        assert '"TV show"' in result
        assert "comedy" in result

    def test_entity_type_unknown_skipped(self, strategy):
        strategy.entity_type = "unknown entity"
        c = make_constraint(ConstraintType.PROPERTY, "comedy")
        result = strategy._build_query([c])
        assert "unknown" not in result


# --- _generate_query_variations ---


class TestGenerateQueryVariations:
    def test_statistic_variations(self, strategy):
        c = make_constraint(ConstraintType.STATISTIC, "84.5x ratio")
        variations = strategy._generate_query_variations(c)
        assert "84.5x ratio" in variations
        assert any("list" in v for v in variations)
        assert any("complete" in v for v in variations)
        assert len(variations) <= 3

    def test_property_variations(self, strategy):
        c = make_constraint(ConstraintType.PROPERTY, "blue eyes")
        variations = strategy._generate_query_variations(c)
        assert any("with" in v for v in variations)
        assert any("featuring" in v for v in variations)
        assert len(variations) <= 3

    def test_other_type_base_only(self, strategy):
        c = make_constraint(ConstraintType.TEMPORAL, "in 2020")
        variations = strategy._generate_query_variations(c)
        assert variations == ["in 2020"]

    def test_limit_to_three(self, strategy):
        c = make_constraint(ConstraintType.STATISTIC, "test stat")
        variations = strategy._generate_query_variations(c)
        assert len(variations) <= 3


# --- _create_strict_combinations ---


class TestCreateStrictCombinations:
    def test_top_two_combination(self, strategy):
        strategy.entity_type = None
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.PROPERTY, "A"),
            make_constraint(ConstraintType.PROPERTY, "B"),
        ]
        combos = strategy._create_strict_combinations()
        assert len(combos) >= 1
        assert len(combos[0].constraints) == 2

    def test_temporal_property_combination(self, strategy):
        strategy.entity_type = None
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.EVENT, "fall in 2014"),
            make_constraint(ConstraintType.PROPERTY, "scenic view"),
        ]
        combos = strategy._create_strict_combinations()
        assert any(len(c.constraints) == 2 for c in combos)

    def test_limit_five(self, strategy):
        strategy.entity_type = None
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.PROPERTY, f"prop{i}")
            for i in range(10)
        ]
        combos = strategy._create_strict_combinations()
        assert len(combos) <= 5

    def test_single_constraint_no_crash(self, strategy):
        strategy.entity_type = None
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.PROPERTY, "solo"),
        ]
        combos = strategy._create_strict_combinations()
        assert isinstance(combos, list)


# --- _create_relaxed_combinations ---


class TestCreateRelaxedCombinations:
    def test_single_constraints_from_top_three(self, strategy):
        strategy.entity_type = None
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.PROPERTY, f"c{i}") for i in range(5)
        ]
        combos = strategy._create_relaxed_combinations()
        single_combos = [c for c in combos if len(c.constraints) == 1]
        assert len(single_combos) == 3

    def test_weaker_group_included(self, strategy):
        strategy.entity_type = None
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.PROPERTY, f"c{i}") for i in range(6)
        ]
        combos = strategy._create_relaxed_combinations()
        multi = [c for c in combos if len(c.constraints) > 1]
        assert len(multi) >= 1


# --- _create_individual_combinations ---


class TestCreateIndividualCombinations:
    def test_two_variations_per_constraint(self, strategy):
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.STATISTIC, "stat1"),
        ]
        combos = strategy._create_individual_combinations()
        assert len(combos) == 2

    def test_max_five_constraints(self, strategy):
        strategy.constraint_ranking = [
            make_constraint(ConstraintType.PROPERTY, f"c{i}") for i in range(10)
        ]
        combos = strategy._create_individual_combinations()
        # 5 constraints * 2 variations = 10 max (but PROPERTY only gives 1 base)
        # Actually PROPERTY gives: base + with + featuring + known for = 4 -> [:2] = 2
        assert len(combos) <= 10
