"""High-value tests for constraint_analyzer.py.

Covers pure logic in ConstraintAnalyzer:
- _parse_constraint_type(): all 8 valid types, case insensitivity, unknown defaults
- _parse_weight(): int, float, string number, string with annotation, empty/no-number
- extract_constraints(): mocked model, single/multiple constraints, missing fields, IDs
"""

from unittest.mock import MagicMock

from local_deep_research.advanced_search_system.constraints.base_constraint import (
    ConstraintType,
)
from local_deep_research.advanced_search_system.constraints.constraint_analyzer import (
    ConstraintAnalyzer,
)


def _make_analyzer():
    """Create a ConstraintAnalyzer with a MagicMock model, bypassing __init__."""
    analyzer = ConstraintAnalyzer.__new__(ConstraintAnalyzer)
    analyzer.model = MagicMock()
    return analyzer


# ---------------------------------------------------------------------------
# _parse_constraint_type tests
# ---------------------------------------------------------------------------


class TestParseConstraintType:
    """Tests for _parse_constraint_type covering all 8 types and edge cases."""

    def test_type_property(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("property")
            == ConstraintType.PROPERTY
        )

    def test_type_name_pattern(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("name_pattern")
            == ConstraintType.NAME_PATTERN
        )

    def test_type_event(self):
        analyzer = _make_analyzer()
        assert analyzer._parse_constraint_type("event") == ConstraintType.EVENT

    def test_type_statistic(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("statistic")
            == ConstraintType.STATISTIC
        )

    def test_type_temporal(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("temporal")
            == ConstraintType.TEMPORAL
        )

    def test_type_location(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("location")
            == ConstraintType.LOCATION
        )

    def test_type_comparison(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("comparison")
            == ConstraintType.COMPARISON
        )

    def test_type_existence(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("existence")
            == ConstraintType.EXISTENCE
        )

    def test_case_insensitive_upper(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("PROPERTY")
            == ConstraintType.PROPERTY
        )

    def test_case_insensitive_mixed(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("Temporal")
            == ConstraintType.TEMPORAL
        )

    def test_case_insensitive_name_pattern_mixed(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("Name_Pattern")
            == ConstraintType.NAME_PATTERN
        )

    def test_unknown_type_defaults_to_property(self):
        analyzer = _make_analyzer()
        assert (
            analyzer._parse_constraint_type("something_else")
            == ConstraintType.PROPERTY
        )

    def test_empty_string_defaults_to_property(self):
        analyzer = _make_analyzer()
        assert analyzer._parse_constraint_type("") == ConstraintType.PROPERTY


# ---------------------------------------------------------------------------
# _parse_weight tests
# ---------------------------------------------------------------------------


class TestParseWeight:
    """Tests for _parse_weight covering numeric, string, and fallback inputs."""

    def test_integer_input(self):
        analyzer = _make_analyzer()
        assert analyzer._parse_weight(1) == 1.0

    def test_float_input(self):
        analyzer = _make_analyzer()
        assert analyzer._parse_weight(0.75) == 0.75

    def test_string_plain_number(self):
        """String containing just a number like '0.8'."""
        analyzer = _make_analyzer()
        assert analyzer._parse_weight("0.8") == 0.8

    def test_string_with_annotation(self):
        """String like '0.8 (high importance)' extracts 0.8."""
        analyzer = _make_analyzer()
        assert analyzer._parse_weight("0.8 (high importance)") == 0.8

    def test_string_no_number_returns_default(self):
        """String with no digits at all returns default 1.0."""
        analyzer = _make_analyzer()
        assert analyzer._parse_weight("high") == 1.0

    def test_empty_string_returns_default(self):
        """Empty string returns default 1.0."""
        analyzer = _make_analyzer()
        assert analyzer._parse_weight("") == 1.0

    def test_none_returns_default(self):
        """None value returns default 1.0."""
        analyzer = _make_analyzer()
        assert analyzer._parse_weight(None) == 1.0


# ---------------------------------------------------------------------------
# extract_constraints tests (mocked model)
# ---------------------------------------------------------------------------


class TestExtractConstraints:
    """Tests for extract_constraints with a mocked model."""

    @staticmethod
    def _mock_response(analyzer, content):
        """Configure the analyzer's model to return the given content string."""
        analyzer.model.invoke.return_value = MagicMock(content=content)

    def test_single_constraint_parsed(self):
        analyzer = _make_analyzer()
        self._mock_response(
            analyzer,
            (
                "CONSTRAINT_1:\n"
                "Type: property\n"
                "Description: Must be a red fruit\n"
                "Value: red fruit\n"
                "Weight: 0.9\n"
            ),
        )
        constraints = analyzer.extract_constraints("What red fruit is this?")

        assert len(constraints) == 1
        assert constraints[0].type == ConstraintType.PROPERTY
        assert constraints[0].description == "Must be a red fruit"
        assert constraints[0].value == "red fruit"
        assert constraints[0].weight == 0.9
        assert constraints[0].id == "c1"

    def test_multiple_constraints_parsed(self):
        analyzer = _make_analyzer()
        self._mock_response(
            analyzer,
            (
                "CONSTRAINT_1:\n"
                "Type: temporal\n"
                "Description: Happened in 2020\n"
                "Value: 2020\n"
                "Weight: 0.8\n"
                "\n"
                "CONSTRAINT_2:\n"
                "Type: location\n"
                "Description: In Europe\n"
                "Value: Europe\n"
                "Weight: 0.7\n"
            ),
        )
        constraints = analyzer.extract_constraints("Where and when?")

        assert len(constraints) == 2
        assert constraints[0].type == ConstraintType.TEMPORAL
        assert constraints[1].type == ConstraintType.LOCATION

    def test_last_constraint_without_trailing_marker(self):
        """The last constraint has no CONSTRAINT_ marker after it; must be captured."""
        analyzer = _make_analyzer()
        self._mock_response(
            analyzer,
            (
                "CONSTRAINT_1:\n"
                "Type: event\n"
                "Description: First\n"
                "Value: first\n"
                "\n"
                "CONSTRAINT_2:\n"
                "Type: existence\n"
                "Description: Second and last\n"
                "Value: exists\n"
                "Weight: 0.5\n"
            ),
        )
        constraints = analyzer.extract_constraints("test query")

        assert len(constraints) == 2
        assert constraints[1].id == "c2"
        assert constraints[1].description == "Second and last"

    def test_missing_description_skips_constraint(self):
        """A constraint block missing 'description' should be skipped."""
        analyzer = _make_analyzer()
        self._mock_response(
            analyzer,
            ("CONSTRAINT_1:\nType: property\nValue: some value\nWeight: 0.8\n"),
        )
        constraints = analyzer.extract_constraints("test")

        assert len(constraints) == 0

    def test_missing_value_skips_constraint(self):
        """A constraint block missing 'value' should be skipped."""
        analyzer = _make_analyzer()
        self._mock_response(
            analyzer,
            (
                "CONSTRAINT_1:\n"
                "Type: property\n"
                "Description: Something\n"
                "Weight: 0.8\n"
            ),
        )
        constraints = analyzer.extract_constraints("test")

        assert len(constraints) == 0

    def test_ids_are_sequential(self):
        analyzer = _make_analyzer()
        self._mock_response(
            analyzer,
            (
                "CONSTRAINT_1:\n"
                "Type: property\n"
                "Description: A\n"
                "Value: a\n"
                "\n"
                "CONSTRAINT_2:\n"
                "Type: event\n"
                "Description: B\n"
                "Value: b\n"
                "\n"
                "CONSTRAINT_3:\n"
                "Type: comparison\n"
                "Description: C\n"
                "Value: c\n"
            ),
        )
        constraints = analyzer.extract_constraints("test")

        assert [c.id for c in constraints] == ["c1", "c2", "c3"]

    def test_empty_model_response_returns_no_constraints(self):
        analyzer = _make_analyzer()
        self._mock_response(analyzer, "")
        constraints = analyzer.extract_constraints("anything")

        assert constraints == []

    def test_invalid_block_between_valid_ones_skipped(self):
        """Invalid block (missing required fields) between valid blocks is skipped."""
        analyzer = _make_analyzer()
        self._mock_response(
            analyzer,
            (
                "CONSTRAINT_1:\n"
                "Type: property\n"
                "Description: Valid first\n"
                "Value: v1\n"
                "\n"
                "CONSTRAINT_2:\n"
                "Type: statistic\n"
                "\n"
                "CONSTRAINT_3:\n"
                "Type: existence\n"
                "Description: Valid third\n"
                "Value: v3\n"
            ),
        )
        constraints = analyzer.extract_constraints("test")

        assert len(constraints) == 2
        assert constraints[0].id == "c1"
        assert constraints[1].id == "c2"
        assert constraints[1].description == "Valid third"

    def test_default_weight_when_omitted(self):
        """Weight defaults to 1.0 when not specified in the constraint block."""
        analyzer = _make_analyzer()
        self._mock_response(
            analyzer,
            (
                "CONSTRAINT_1:\n"
                "Type: property\n"
                "Description: No weight\n"
                "Value: val\n"
            ),
        )
        constraints = analyzer.extract_constraints("test")

        assert len(constraints) == 1
        assert constraints[0].weight == 1.0
