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

import pytest
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.candidate_exploration.constraint_guided_explorer import (
    ConstraintGuidedExplorer,
)


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,
    )


@pytest.fixture
def explorer():
    """Create a ConstraintGuidedExplorer with mocked parent init."""
    with patch.object(
        ConstraintGuidedExplorer, "__init__", lambda self, *a, **kw: None
    ):
        e = ConstraintGuidedExplorer.__new__(ConstraintGuidedExplorer)
        e.constraint_weight_threshold = 0.7
        e.early_validation = True
        e.max_candidates = 20
        return e


# --- _prioritize_constraints ---


class TestPrioritizeConstraints:
    def test_sort_by_weight_descending(self, explorer):
        constraints = [
            make_constraint(ConstraintType.PROPERTY, "low", weight=0.3),
            make_constraint(ConstraintType.PROPERTY, "high", weight=0.9),
            make_constraint(ConstraintType.PROPERTY, "mid", weight=0.6),
        ]
        result = explorer._prioritize_constraints(constraints)
        assert result[0].value == "high"
        assert result[1].value == "mid"
        assert result[2].value == "low"

    def test_same_weight_sort_by_type_priority(self, explorer):
        constraints = [
            make_constraint(ConstraintType.EXISTENCE, "exists", weight=1.0),
            make_constraint(ConstraintType.NAME_PATTERN, "pattern", weight=1.0),
        ]
        result = explorer._prioritize_constraints(constraints)
        # Both weight 1.0, but NAME_PATTERN has priority 1, EXISTENCE has 8
        # Reverse sort: (1.0, 8) > (1.0, 1) -> EXISTENCE first
        # Wait - sorted reverse=True, so higher tuple first
        assert result[0].type == ConstraintType.EXISTENCE
        assert result[1].type == ConstraintType.NAME_PATTERN

    def test_name_pattern_highest_type_priority(self, explorer):
        constraints = [
            make_constraint(ConstraintType.PROPERTY, "prop", weight=0.5),
            make_constraint(ConstraintType.NAME_PATTERN, "name", weight=0.5),
        ]
        result = explorer._prioritize_constraints(constraints)
        # Same weight, PROPERTY has priority 2, NAME_PATTERN has 1
        # Reverse sort: (0.5, 2) > (0.5, 1)
        assert result[0].type == ConstraintType.PROPERTY

    def test_empty_constraints(self, explorer):
        assert explorer._prioritize_constraints([]) == []

    def test_all_types_sorted(self, explorer):
        constraints = [
            make_constraint(ConstraintType.EXISTENCE, "e", weight=1.0),
            make_constraint(ConstraintType.NAME_PATTERN, "n", weight=1.0),
            make_constraint(ConstraintType.TEMPORAL, "t", weight=1.0),
            make_constraint(ConstraintType.PROPERTY, "p", weight=1.0),
        ]
        result = explorer._prioritize_constraints(constraints)
        # Reverse sort by (weight, type_priority) - all weight 1.0
        # EXISTENCE=8 > TEMPORAL=5 > PROPERTY=2 > NAME_PATTERN=1
        assert result[0].type == ConstraintType.EXISTENCE
        assert result[-1].type == ConstraintType.NAME_PATTERN


# --- _name_pattern_queries ---


class TestNamePatternQueries:
    def test_body_part_with_entity_type(self, explorer):
        c = make_constraint(ConstraintType.NAME_PATTERN, "contains body part")
        queries = explorer._name_pattern_queries(c, "hiking spots", "trail")
        assert len(queries) == 3
        assert all("trail" in q for q in queries)

    def test_body_part_without_entity_type(self, explorer):
        c = make_constraint(ConstraintType.NAME_PATTERN, "contains body part")
        queries = explorer._name_pattern_queries(c, "hiking spots", None)
        assert len(queries) == 3
        assert all("name" in q for q in queries)

    def test_no_body_part_returns_empty(self, explorer):
        c = make_constraint(ConstraintType.NAME_PATTERN, "starts with letter A")
        queries = explorer._name_pattern_queries(c, "hiking spots", None)
        assert queries == []


# --- _property_queries ---


class TestPropertyQueries:
    def test_returns_three_queries(self, explorer):
        c = make_constraint(ConstraintType.PROPERTY, "scenic view")
        queries = explorer._property_queries(c, "hiking", "trail")
        assert len(queries) == 3

    def test_uses_entity_type_as_base(self, explorer):
        c = make_constraint(ConstraintType.PROPERTY, "scenic")
        queries = explorer._property_queries(c, "hiking", "mountain")
        assert "mountain with scenic" in queries
        assert "mountain that scenic" in queries

    def test_falls_back_to_base_query(self, explorer):
        c = make_constraint(ConstraintType.PROPERTY, "scenic")
        queries = explorer._property_queries(c, "hiking spots", None)
        assert any("hiking spots" in q for q in queries)


# --- _event_queries ---


class TestEventQueries:
    def test_returns_three_queries(self, explorer):
        c = make_constraint(ConstraintType.EVENT, "rockfall 2014")
        queries = explorer._event_queries(c, "canyon", "trail")
        assert len(queries) == 3

    def test_includes_incident_and_accident(self, explorer):
        c = make_constraint(ConstraintType.EVENT, "collapse")
        queries = explorer._event_queries(c, "bridge", None)
        assert any("incident" in q for q in queries)
        assert any("accident" in q for q in queries)


# --- _location_queries ---


class TestLocationQueries:
    def test_returns_three_queries(self, explorer):
        c = make_constraint(ConstraintType.LOCATION, "Colorado")
        queries = explorer._location_queries(c, "hiking", None)
        assert len(queries) == 3

    def test_includes_in_preposition(self, explorer):
        c = make_constraint(ConstraintType.LOCATION, "Arizona")
        queries = explorer._location_queries(c, "trails", None)
        assert "trails in Arizona" in queries


# --- _combine_constraints_query ---


class TestCombineConstraintsQuery:
    def test_two_constraints_combined(self, explorer):
        c1 = make_constraint(ConstraintType.PROPERTY, "scenic")
        c2 = make_constraint(ConstraintType.LOCATION, "Colorado")
        result = explorer._combine_constraints_query("hiking", [c1, c2])
        assert "hiking" in result
        assert "scenic" in result
        assert "Colorado" in result
        assert " AND " in result

    def test_single_constraint_returns_none(self, explorer):
        c1 = make_constraint(ConstraintType.PROPERTY, "scenic")
        result = explorer._combine_constraints_query("hiking", [c1])
        assert result is None

    def test_empty_returns_none(self, explorer):
        result = explorer._combine_constraints_query("hiking", [])
        assert result is None


# --- _quick_name_validation ---


class TestQuickNameValidation:
    def test_body_part_match(self, explorer):
        c = make_constraint(ConstraintType.NAME_PATTERN, "contains body part")
        assert explorer._quick_name_validation("Arm Creek", c) is True

    def test_body_part_no_match(self, explorer):
        c = make_constraint(ConstraintType.NAME_PATTERN, "contains body part")
        assert explorer._quick_name_validation("Silver Creek", c) is False

    def test_non_body_part_constraint_accepts_all(self, explorer):
        c = make_constraint(ConstraintType.NAME_PATTERN, "starts with A")
        assert explorer._quick_name_validation("Anything", c) is True

    def test_body_part_case_insensitive(self, explorer):
        c = make_constraint(
            ConstraintType.NAME_PATTERN, "has Body Part in name"
        )
        assert explorer._quick_name_validation("HEART Mountain", c) is True


# --- _rank_by_constraint_alignment ---


class TestRankByConstraintAlignment:
    def test_scores_name_pattern_constraints(self, explorer):
        explorer._rank_candidates_by_relevance = lambda c, q: c

        c1 = Candidate(name="Arm Creek")
        c2 = Candidate(name="Silver Creek")
        constraints = [
            make_constraint(
                ConstraintType.NAME_PATTERN, "contains body part", weight=2.0
            ),
        ]
        result = explorer._rank_by_constraint_alignment(
            [c1, c2], constraints, "trails"
        )
        assert result[0].name == "Arm Creek"

    def test_non_name_pattern_no_score(self, explorer):
        explorer._rank_candidates_by_relevance = lambda c, q: c

        c1 = Candidate(name="Test")
        constraints = [
            make_constraint(ConstraintType.PROPERTY, "scenic", weight=1.0),
        ]
        result = explorer._rank_by_constraint_alignment(
            [c1], constraints, "trails"
        )
        assert result[0].constraint_alignment_score == 0.0

    def test_empty_candidates(self, explorer):
        explorer._rank_candidates_by_relevance = lambda c, q: c
        result = explorer._rank_by_constraint_alignment([], [], "trails")
        assert result == []
