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

Tests cover constraint priorities, type inference, relaxation strategies
for statistical/comparison/temporal/property constraints, impact analysis,
and progressive relaxation logic.
"""

from unittest.mock import MagicMock


from local_deep_research.advanced_search_system.constraint_checking.intelligent_constraint_relaxer import (
    IntelligentConstraintRelaxer,
)


class SimpleConstraint:
    """A plain constraint object with a text value and str representation.

    Uses .value attribute so _create_relaxed_constraint updates it,
    and __str__ returns .value so relaxed text is visible via str().
    """

    def __init__(self, text):
        self.value = text

    def __str__(self):
        return self.value

    def __repr__(self):
        return f"SimpleConstraint({self.value!r})"


def _make_constraint(text, ctype=None, constraint_type_attr=None):
    """Helper to create a mock constraint with optional type attributes."""
    obj = MagicMock()
    obj.__str__ = lambda self: text
    if ctype is not None:
        obj.type = ctype
    else:
        del obj.type
    if constraint_type_attr is not None:
        obj.constraint_type = constraint_type_attr
    else:
        del obj.constraint_type
    return obj


def _make_simple_constraint(text):
    """Create a plain constraint with no type/constraint_type attrs.

    Uses SimpleConstraint so deepcopy and .value assignment work
    correctly inside _create_relaxed_constraint.
    """
    return SimpleConstraint(text)


class TestConstraintPriorities:
    """Tests for the constraint_priorities dictionary."""

    def test_name_pattern_has_highest_priority(self):
        relaxer = IntelligentConstraintRelaxer()
        assert relaxer.constraint_priorities["NAME_PATTERN"] == 10

    def test_comparison_has_lowest_priority(self):
        relaxer = IntelligentConstraintRelaxer()
        assert relaxer.constraint_priorities["COMPARISON"] == 1

    def test_statistic_lower_than_temporal(self):
        relaxer = IntelligentConstraintRelaxer()
        assert (
            relaxer.constraint_priorities["STATISTIC"]
            < relaxer.constraint_priorities["TEMPORAL"]
        )

    def test_all_expected_types_present(self):
        relaxer = IntelligentConstraintRelaxer()
        expected = {
            "NAME_PATTERN",
            "EXISTENCE",
            "LOCATION",
            "TEMPORAL",
            "PROPERTY",
            "EVENT",
            "STATISTIC",
            "COMPARISON",
            "RELATIONSHIP",
        }
        assert set(relaxer.constraint_priorities.keys()) == expected

    def test_min_constraints_default(self):
        relaxer = IntelligentConstraintRelaxer()
        assert relaxer.min_constraints == 2


class TestGetConstraintType:
    """Tests for _get_constraint_type type inference."""

    def test_type_attribute_with_value(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = MagicMock()
        constraint.type.value = "TEMPORAL"
        assert relaxer._get_constraint_type(constraint) == "TEMPORAL"

    def test_type_attribute_without_value(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = MagicMock()
        constraint.type = "LOCATION"
        assert relaxer._get_constraint_type(constraint) == "LOCATION"

    def test_constraint_type_attribute(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = MagicMock()
        del constraint.type
        constraint.constraint_type = "EVENT"
        assert relaxer._get_constraint_type(constraint) == "EVENT"

    def test_infer_name_pattern_from_text(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("entity called FooBar")
        assert relaxer._get_constraint_type(constraint) == "NAME_PATTERN"

    def test_infer_location_from_text(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("located in country X")
        assert relaxer._get_constraint_type(constraint) == "LOCATION"

    def test_infer_temporal_from_text(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("founded in year 1990")
        assert relaxer._get_constraint_type(constraint) == "TEMPORAL"

    def test_infer_statistic_from_text(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("has a number of 500 employees")
        assert relaxer._get_constraint_type(constraint) == "STATISTIC"

    def test_infer_event_from_text(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("this event happened recently")
        assert relaxer._get_constraint_type(constraint) == "EVENT"

    def test_infer_comparison_from_text(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("bigger than the average")
        assert relaxer._get_constraint_type(constraint) == "COMPARISON"

    def test_infer_property_fallback(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("is a multinational conglomerate")
        assert relaxer._get_constraint_type(constraint) == "PROPERTY"


class TestRelaxStatisticalConstraint:
    """Tests for _relax_statistical_constraint."""

    def test_number_gets_range_variations(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("has 100 employees")
        variations = relaxer._relax_statistical_constraint(constraint)
        assert len(variations) > 0
        # Should contain range text
        assert any("between" in str(v) for v in variations)

    def test_approximately_variation_created_when_fits_in_limit(self):
        relaxer = IntelligentConstraintRelaxer()
        # With a single number, 3 range + 1 approx = 4 total, but limit is 3
        # So "approximately" may be cut off. Verify the approx version is generated
        # before truncation by temporarily checking internal logic.
        constraint = _make_simple_constraint("revenue of 500 million")
        # The method creates 3 range + 1 approx per number, then truncates to 3.
        # With 1 number: variations before truncation = 4, so approx is the 4th.
        # Verify at least the range variations are correct.
        variations = relaxer._relax_statistical_constraint(constraint)
        assert len(variations) == 3
        assert all("between" in str(v) for v in variations)

    def test_limited_to_three_variations(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("has 100 employees and 50 offices")
        variations = relaxer._relax_statistical_constraint(constraint)
        assert len(variations) <= 3

    def test_no_numbers_returns_empty(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("has many employees")
        variations = relaxer._relax_statistical_constraint(constraint)
        assert variations == []

    def test_range_boundaries_for_ten_percent(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("population is 1000")
        variations = relaxer._relax_statistical_constraint(constraint)
        # First variation should use 10% tolerance: between 900 and 1100
        first_text = str(variations[0])
        assert "900" in first_text
        assert "1100" in first_text


class TestRelaxComparisonConstraint:
    """Tests for _relax_comparison_constraint."""

    def test_largest_relaxed(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("the largest company")
        variations = relaxer._relax_comparison_constraint(constraint)
        assert any("one of the largest" in str(v) for v in variations)

    def test_exactly_relaxed_to_approximately(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("exactly 5 times bigger")
        variations = relaxer._relax_comparison_constraint(constraint)
        assert any("approximately" in str(v) for v in variations)

    def test_more_than_indicator_splits(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("revenue more than the competitor")
        variations = relaxer._relax_comparison_constraint(constraint)
        # Should have a variation with just the part before "more than"
        assert any(
            "revenue" in str(v) and "competitor" not in str(v)
            for v in variations
        )

    def test_no_comparison_terms_returns_empty(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("a blue widget")
        variations = relaxer._relax_comparison_constraint(constraint)
        assert variations == []

    def test_limited_to_three(self):
        relaxer = IntelligentConstraintRelaxer()
        # Contains many comparison terms
        constraint = _make_simple_constraint(
            "exactly the largest and highest and lowest compared to others"
        )
        variations = relaxer._relax_comparison_constraint(constraint)
        assert len(variations) <= 3


class TestRelaxTemporalConstraint:
    """Tests for _relax_temporal_constraint."""

    def test_year_expanded_to_decade(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("founded in 1985")
        variations = relaxer._relax_temporal_constraint(constraint)
        assert any("1980s" in str(v) for v in variations)

    def test_year_range_created(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("established in 2003")
        variations = relaxer._relax_temporal_constraint(constraint)
        assert any("between" in str(v) for v in variations)

    def test_founded_in_relaxed_to_around(self):
        relaxer = IntelligentConstraintRelaxer()
        # "founded in" without a year so only temporal_relaxations apply
        constraint = _make_simple_constraint("founded in the early era")
        variations = relaxer._relax_temporal_constraint(constraint)
        assert any("around" in str(v) for v in variations)

    def test_no_year_no_temporal_terms_returns_empty(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("a big red ball")
        variations = relaxer._relax_temporal_constraint(constraint)
        assert variations == []

    def test_limited_to_three(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint(
            "founded in 1995 and established in 2000"
        )
        variations = relaxer._relax_temporal_constraint(constraint)
        assert len(variations) <= 3


class TestRelaxPropertyConstraint:
    """Tests for _relax_property_constraint."""

    def test_multinational_generalized(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("a multinational corporation")
        variations = relaxer._relax_property_constraint(constraint)
        assert any("international" in str(v) for v in variations)

    def test_corporation_to_company(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("a large corporation")
        variations = relaxer._relax_property_constraint(constraint)
        assert any("company" in str(v) for v in variations)

    def test_adjectives_removed(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("extremely large building")
        variations = relaxer._relax_property_constraint(constraint)
        assert any("large building" in str(v) for v in variations)

    def test_limited_to_two(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint(
            "a very specialized multinational conglomerate with headquarters"
        )
        variations = relaxer._relax_property_constraint(constraint)
        assert len(variations) <= 2

    def test_no_matching_terms_returns_empty(self):
        relaxer = IntelligentConstraintRelaxer()
        constraint = _make_simple_constraint("a blue widget")
        variations = relaxer._relax_property_constraint(constraint)
        assert variations == []


class TestAnalyzeRelaxationImpact:
    """Tests for analyze_relaxation_impact."""

    def test_counts_correct(self):
        relaxer = IntelligentConstraintRelaxer()
        orig = [_make_simple_constraint("x")] * 5
        relaxed = [_make_simple_constraint("x")] * 3
        result = relaxer.analyze_relaxation_impact(orig, relaxed)
        assert result["original_count"] == 5
        assert result["relaxed_count"] == 3
        assert result["constraints_removed"] == 2

    def test_high_impact_when_name_removed(self):
        relaxer = IntelligentConstraintRelaxer()
        orig = [
            _make_simple_constraint("entity called Foo"),
            _make_simple_constraint("some prop"),
        ]
        relaxed = [_make_simple_constraint("some prop")]
        result = relaxer.analyze_relaxation_impact(orig, relaxed)
        assert result["priority_impact"] == "high"

    def test_medium_impact_when_temporal_removed(self):
        relaxer = IntelligentConstraintRelaxer()
        orig = [
            _make_simple_constraint("founded in year 2000"),
            _make_simple_constraint("is a widget"),
        ]
        # Only keep the property constraint
        relaxed = [_make_simple_constraint("is a widget")]
        result = relaxer.analyze_relaxation_impact(orig, relaxed)
        assert result["priority_impact"] == "medium"

    def test_low_impact_when_only_comparison_removed(self):
        relaxer = IntelligentConstraintRelaxer()
        orig = [
            _make_simple_constraint("bigger than average"),
            _make_simple_constraint("is a widget"),
        ]
        # Both still present in relaxed (same types)
        relaxed = [_make_simple_constraint("is a widget")]
        result = relaxer.analyze_relaxation_impact(orig, relaxed)
        # COMPARISON removed, which is low impact
        assert result["priority_impact"] == "low"

    def test_empty_inputs(self):
        relaxer = IntelligentConstraintRelaxer()
        result = relaxer.analyze_relaxation_impact([], [])
        assert result["original_count"] == 0
        assert result["constraints_removed"] == 0
        assert result["priority_impact"] == "low"


class TestRelaxConstraintsProgressively:
    """Tests for relax_constraints_progressively."""

    def test_sufficient_candidates_returns_original(self):
        relaxer = IntelligentConstraintRelaxer()
        constraints = [
            _make_simple_constraint("a"),
            _make_simple_constraint("b"),
        ]
        candidates = [MagicMock() for _ in range(5)]
        result = relaxer.relax_constraints_progressively(
            constraints, candidates, target_candidates=5
        )
        assert result == [constraints]

    def test_insufficient_candidates_returns_relaxed_sets(self):
        relaxer = IntelligentConstraintRelaxer()
        constraints = [
            _make_simple_constraint("entity called Foo"),
            _make_simple_constraint("located in country X"),
            _make_simple_constraint("bigger than average"),
            _make_simple_constraint("has 100 employees"),
        ]
        result = relaxer.relax_constraints_progressively(
            constraints, [], target_candidates=5
        )
        assert len(result) > 0

    def test_single_constraint_below_min_no_removal(self):
        relaxer = IntelligentConstraintRelaxer()
        constraints = [_make_simple_constraint("a widget")]
        result = relaxer.relax_constraints_progressively(
            constraints, [], target_candidates=5
        )
        # With only 1 constraint, removal would go below min_constraints=2
        # So progressive removal sets should not include sets below min
        for constraint_set in result:
            # All sets should have at least 1 element (the original or variation)
            assert len(constraint_set) >= 1

    def test_deduplication_of_relaxed_sets(self):
        relaxer = IntelligentConstraintRelaxer()
        constraints = [
            _make_simple_constraint("entity called Foo"),
            _make_simple_constraint("located in country X"),
            _make_simple_constraint("is a widget"),
        ]
        result = relaxer.relax_constraints_progressively(
            constraints, [], target_candidates=5
        )
        # Check no duplicate sets
        signatures = [tuple(sorted(str(c) for c in cs)) for cs in result]
        assert len(signatures) == len(set(signatures))

    def test_high_priority_only_set_included(self):
        relaxer = IntelligentConstraintRelaxer()
        # Create constraints with priority >= 7 and < 7
        constraints = [
            _make_simple_constraint("entity called Foo"),  # NAME_PATTERN=10
            _make_simple_constraint("located in country X"),  # LOCATION=8
            _make_simple_constraint("founded in year 2000"),  # TEMPORAL=7
            _make_simple_constraint("bigger than average"),  # COMPARISON=1
        ]
        result = relaxer.relax_constraints_progressively(
            constraints, [], target_candidates=5
        )
        # At least one set should have exactly the high-priority constraints
        set_sizes = [len(cs) for cs in result]
        assert (
            3 in set_sizes
        )  # 3 high-priority constraints (NAME, LOCATION, TEMPORAL)
