"""Tests for expression evaluation functionality."""

import pytest

from therismos import (
    FALSE,
    TRUE,
    AllExpr,
    AnyExpr,
    F,
    NotExpr,
    UnwoundValue,
    unwind_data,
)


class TestComparisonEval:
    """Test evaluation of comparison expressions."""

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (42, 42, True),
            (42, 43, False),
            ("hello", "hello", True),
            ("hello", "world", False),
            (3.14, 3.14, True),
            (3.14, 2.71, False),
        ],
    )
    def test_eq_eval(
        self, field_value: int | str | float, expr_value: int | str | float, expected: bool
    ) -> None:
        """Test equality expression evaluation."""
        field = F("value")
        expr = field == expr_value
        assert expr.evaluate(unwind_data({"value": field_value})) is expected

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (42, 42, False),
            (42, 43, True),
            ("hello", "hello", False),
            ("hello", "world", True),
        ],
    )
    def test_ne_eval(self, field_value: int | str, expr_value: int | str, expected: bool) -> None:
        """Test inequality expression evaluation."""
        field = F("value")
        expr = field != expr_value
        assert expr.evaluate(unwind_data({"value": field_value})) is expected

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (42, 43, True),
            (42, 42, False),
            (42, 41, False),
        ],
    )
    def test_lt_eval(self, field_value: int, expr_value: int, expected: bool) -> None:
        """Test less-than expression evaluation."""
        field = F("value")
        expr = field < expr_value
        assert expr.evaluate(unwind_data({"value": field_value})) is expected

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (42, 43, True),
            (42, 42, True),
            (42, 41, False),
        ],
    )
    def test_le_eval(self, field_value: int, expr_value: int, expected: bool) -> None:
        """Test less-than-or-equal expression evaluation."""
        field = F("value")
        expr = field <= expr_value
        assert expr.evaluate(unwind_data({"value": field_value})) is expected

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (42, 41, True),
            (42, 42, False),
            (42, 43, False),
        ],
    )
    def test_gt_eval(self, field_value: int, expr_value: int, expected: bool) -> None:
        """Test greater-than expression evaluation."""
        field = F("value")
        expr = field > expr_value
        assert expr.evaluate(unwind_data({"value": field_value})) is expected

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (42, 41, True),
            (42, 42, True),
            (42, 43, False),
        ],
    )
    def test_ge_eval(self, field_value: int, expr_value: int, expected: bool) -> None:
        """Test greater-than-or-equal expression evaluation."""
        field = F("value")
        expr = field >= expr_value
        assert expr.evaluate(unwind_data({"value": field_value})) is expected


class TestTypedFieldEval:
    """Test typed field evaluation with automatic casting."""

    def test_int_field_casting(self) -> None:
        """Test that int-typed field casts values correctly during eval."""
        field = F("age", int)
        expr = field > 18

        # String value should be cast to int during evaluation
        assert expr.evaluate(unwind_data({"age": "25"})) is True
        assert expr.evaluate(unwind_data({"age": "10"})) is False

    def test_str_field_casting(self) -> None:
        """Test that str-typed field casts values correctly during eval."""
        field = F("value", str)
        expr = field == "42"

        # Int value should be cast to str during evaluation
        assert expr.evaluate(unwind_data({"value": 42})) is True

    def test_float_field_casting(self) -> None:
        """Test that float-typed field casts values correctly during eval."""
        field = F("score", float)
        expr = field > 3.5

        # Int or string should be cast to float during evaluation
        assert expr.evaluate(unwind_data({"score": 4})) is True
        assert expr.evaluate(unwind_data({"score": "4.5"})) is True


class TestRegexEval:
    """Test regex expression evaluation."""

    @pytest.mark.parametrize(
        ("field_value", "pattern", "expected"),
        [
            ("hello world", r"hello", True),
            ("hello world", r"goodbye", False),
            ("test123", r"\d+", True),
            ("test", r"\d+", False),
            ("HELLO", r"hello", False),  # Case sensitive by default
        ],
    )
    def test_regex_eval(self, field_value: str, pattern: str, expected: bool) -> None:
        """Test regex matching evaluation."""
        field = F("text")
        expr = field.matches(pattern)
        assert expr.evaluate(unwind_data({"text": field_value})) is expected


class TestInEval:
    """Test IN expression evaluation."""

    def test_in_with_values(self) -> None:
        """Test IN expression with multiple values using is_in."""
        field = F("status")
        expr = field.is_in("active", "pending", "review")

        assert expr.evaluate(unwind_data({"status": "active"})) is True
        assert expr.evaluate(unwind_data({"status": "inactive"})) is False

    def test_in_with_iterable(self) -> None:
        """Test IN expression with iterable using is_one_of."""
        field = F("status")
        allowed = ["active", "pending", "review"]
        expr = field.is_one_of(allowed)

        assert expr.evaluate(unwind_data({"status": "pending"})) is True
        assert expr.evaluate(unwind_data({"status": "closed"})) is False

    def test_in_with_typed_field(self) -> None:
        """Test IN expression with type casting."""
        field = F("count", int)
        expr = field.is_in("1", "2", "3")  # String values

        # Field value is int, but IN values are strings - should cast correctly
        assert expr.evaluate(unwind_data({"count": 2})) is True
        assert expr.evaluate(unwind_data({"count": 5})) is False


class TestIsNullEval:
    """Test null-checking evaluation."""

    def test_is_null(self) -> None:
        """Test is_null expression."""
        field = F("value")
        expr = field.is_null()

        assert expr.evaluate(unwind_data({"value": None})) is True
        assert expr.evaluate(unwind_data({"value": "something"})) is False

    def test_is_not_null(self) -> None:
        """Test is_not_null expression."""
        field = F("value")
        expr = field.is_not_null()

        assert expr.evaluate(unwind_data({"value": "something"})) is True
        assert expr.evaluate(unwind_data({"value": None})) is False


class TestConstantEval:
    """Test constant expression evaluation."""

    def test_true_expr_always_true(self) -> None:
        """Test that TRUE constant always evaluates to True."""
        assert TRUE.evaluate(unwind_data({})) is True
        assert TRUE.evaluate(unwind_data({"any": "data"})) is True

    def test_false_expr_always_false(self) -> None:
        """Test that FALSE constant always evaluates to False."""
        assert FALSE.evaluate(unwind_data({})) is False
        assert FALSE.evaluate(unwind_data({"any": "data"})) is False


class TestCompoundEval:
    """Test compound expression evaluation."""

    def test_all_expr_all_true(self) -> None:
        """Test AllExpr returns True when all subexpressions are True."""
        age_expr = F("age") > 18
        status_expr = F("status") == "active"
        expr = AllExpr(age_expr, status_expr)

        assert expr.evaluate(unwind_data({"age": 25, "status": "active"})) is True

    def test_all_expr_one_false(self) -> None:
        """Test AllExpr returns False when any subexpression is False."""
        age_expr = F("age") > 18
        status_expr = F("status") == "active"
        expr = AllExpr(age_expr, status_expr)

        assert expr.evaluate(unwind_data({"age": 15, "status": "active"})) is False

    def test_any_expr_one_true(self) -> None:
        """Test AnyExpr returns True when at least one subexpression is True."""
        admin_expr = F("role") == "admin"
        owner_expr = F("role") == "owner"
        expr = AnyExpr(admin_expr, owner_expr)

        assert expr.evaluate(unwind_data({"role": "admin"})) is True
        assert expr.evaluate(unwind_data({"role": "owner"})) is True
        assert expr.evaluate(unwind_data({"role": "user"})) is False

    def test_not_expr(self) -> None:
        """Test NotExpr negates subexpression."""
        age_expr = F("age") > 18
        expr = NotExpr(age_expr)

        assert expr.evaluate(unwind_data({"age": 15})) is True
        assert expr.evaluate(unwind_data({"age": 25})) is False

    def test_nested_compound(self) -> None:
        """Test nested compound expressions."""
        # (age > 18) AND ((role == "admin") OR (role == "owner"))
        age_expr = F("age") > 18
        admin_expr = F("role") == "admin"
        owner_expr = F("role") == "owner"
        role_expr = AnyExpr(admin_expr, owner_expr)
        expr = AllExpr(age_expr, role_expr)

        assert expr.evaluate(unwind_data({"age": 25, "role": "admin"})) is True
        assert expr.evaluate(unwind_data({"age": 25, "role": "owner"})) is True
        assert expr.evaluate(unwind_data({"age": 25, "role": "user"})) is False
        assert expr.evaluate(unwind_data({"age": 15, "role": "admin"})) is False


class TestEmptyCompoundEval:
    """Test edge cases with empty compound expressions."""

    def test_all_expr_empty(self) -> None:
        """Test that AllExpr with no subexpressions returns True."""
        expr = AllExpr()
        assert expr.evaluate(unwind_data({})) is True

    def test_any_expr_empty(self) -> None:
        """Test that AnyExpr with no subexpressions returns False."""
        expr = AnyExpr()
        assert expr.evaluate(unwind_data({})) is False


class TestEvalErrors:
    """Test error handling during evaluation."""

    def test_missing_field_raises_key_error(self) -> None:
        """Test that accessing a missing field raises KeyError."""
        expr = F("nonexistent") == 42
        with pytest.raises(KeyError, match="nonexistent"):
            expr.evaluate(unwind_data({"other_field": 100}))

    def test_type_casting_error(self) -> None:
        """Test that invalid type casting raises appropriate error."""
        field = F("age", int)
        expr = field > 18

        # "not_a_number" cannot be cast to int
        with pytest.raises((TypeError, ValueError)):
            expr.evaluate(unwind_data({"age": "not_a_number"}))


class TestRealWorldScenarios:
    """Test real-world usage scenarios."""

    def test_user_authorization_check(self) -> None:
        """Test a realistic user authorization scenario."""
        # User must be active AND (admin OR owner)
        active_expr = F("status") == "active"
        admin_expr = F("role") == "admin"
        owner_expr = F("is_owner") == True  # noqa: E712
        auth_expr = active_expr & (admin_expr | owner_expr)

        # Active admin
        data = {"status": "active", "role": "admin", "is_owner": False}
        assert auth_expr.evaluate(unwind_data(data)) is True

        # Active owner
        assert (
            auth_expr.evaluate(unwind_data({"status": "active", "role": "user", "is_owner": True}))
            is True
        )

        # Active regular user
        assert (
            auth_expr.evaluate(unwind_data({"status": "active", "role": "user", "is_owner": False}))
            is False
        )

        # Inactive admin
        data = {"status": "inactive", "role": "admin", "is_owner": False}
        assert auth_expr.evaluate(unwind_data(data)) is False

    def test_content_filtering(self) -> None:
        """Test a realistic content filtering scenario."""
        # Content must:
        # - Have "python" in title (case-insensitive)
        # - Have "programming" tag
        # - Rating >= 4.0
        # - Status is "active"

        title_expr = F("title").matches(r"(?i)python")
        tag_expr = F("tags") == "programming"
        rating_expr = F("rating", float) >= 4.0
        status_expr = F("status") == "active"

        expr = AllExpr(title_expr, tag_expr, rating_expr, status_expr)

        # Matches all criteria
        assert (
            expr.evaluate(
                unwind_data(
                    {
                        "title": "Learning Python",
                        "tags": "programming",
                        "rating": 4.5,
                        "status": "active",
                    }
                )
            )
            is True
        )

        # Title doesn't match
        assert (
            expr.evaluate(
                unwind_data(
                    {
                        "title": "Learning Java",
                        "tags": "programming",
                        "rating": 4.5,
                        "status": "active",
                    }
                )
            )
            is False
        )

        # Rating too low
        assert (
            expr.evaluate(
                unwind_data(
                    {
                        "title": "Learning Python",
                        "tags": "programming",
                        "rating": 3.5,
                        "status": "active",
                    }
                )
            )
            is False
        )

        # Should not match: inactive status
        assert (
            expr.evaluate(
                unwind_data(
                    {
                        "title": "Learning Python",
                        "tags": "programming",
                        "rating": 4.5,
                        "status": "inactive",
                    }
                )
            )
            is False
        )


class TestUnwindingUtility:
    """Test the unwind_data utility function."""

    def test_unwind_single_value(self) -> None:
        """Test unwinding a single non-list value."""
        result = unwind_data({"a": 5})
        assert result == [UnwoundValue(field_name="a", field_value=5)]

    def test_unwind_flat_list(self) -> None:
        """Test unwinding a flat list."""
        result = unwind_data({"a": [1, 2, 3]})
        assert result == [
            UnwoundValue(field_name="a", field_value=1),
            UnwoundValue(field_name="a", field_value=2),
            UnwoundValue(field_name="a", field_value=3),
        ]

    def test_unwind_nested_list(self) -> None:
        """Test unwinding a nested list (2 levels)."""
        result = unwind_data({"a": [[1, 2], [3, 4]]})
        assert result == [
            UnwoundValue(field_name="a", field_value=1),
            UnwoundValue(field_name="a", field_value=2),
            UnwoundValue(field_name="a", field_value=3),
            UnwoundValue(field_name="a", field_value=4),
        ]

    def test_unwind_deeply_nested(self) -> None:
        """Test unwinding a deeply nested list (3+ levels)."""
        result = unwind_data({"a": [[[1, 2], [3]], [[4]]]})
        assert result == [
            UnwoundValue(field_name="a", field_value=1),
            UnwoundValue(field_name="a", field_value=2),
            UnwoundValue(field_name="a", field_value=3),
            UnwoundValue(field_name="a", field_value=4),
        ]

    def test_unwind_empty_list(self) -> None:
        """Test unwinding an empty list preserves it as a single entry."""
        result = unwind_data({"a": []})
        assert result == [UnwoundValue(field_name="a", field_value=[])]

    def test_unwind_multiple_fields(self) -> None:
        """Test unwinding multiple fields."""
        result = unwind_data({"a": [1, 2], "b": "x"})
        assert len(result) == 3
        assert UnwoundValue(field_name="a", field_value=1) in result
        assert UnwoundValue(field_name="a", field_value=2) in result
        assert UnwoundValue(field_name="b", field_value="x") in result

    def test_unwind_mixed_nested(self) -> None:
        """Test unwinding with mixed nesting levels."""
        result = unwind_data({"a": [[1, 2], 3], "b": 4})
        assert len(result) == 4
        assert UnwoundValue(field_name="a", field_value=1) in result
        assert UnwoundValue(field_name="a", field_value=2) in result
        assert UnwoundValue(field_name="a", field_value=3) in result
        assert UnwoundValue(field_name="b", field_value=4) in result


class TestNestedFieldEvaluation:
    """Test evaluation with nested field values."""

    def test_comparison_with_nested_values(self) -> None:
        """Test comparison operators with nested list values."""
        expr = F("scores") > 5

        # Any value > 5
        data = unwind_data({"scores": [[1, 2], [6, 7]]})
        assert expr.evaluate(data) is True

        # No value > 5
        data = unwind_data({"scores": [[1, 2], [3, 4]]})
        assert expr.evaluate(data) is False

    def test_equality_with_nested_values(self) -> None:
        """Test equality with nested list values."""
        expr = F("tags") == "python"

        # Contains "python"
        data = unwind_data({"tags": [["java", "rust"], ["python", "go"]]})
        assert expr.evaluate(data) is True

        # Does not contain "python"
        data = unwind_data({"tags": [["java", "rust"], ["go", "c++"]]})
        assert expr.evaluate(data) is False

    def test_inequality_with_nested_values(self) -> None:
        """Test inequality (none semantics) with nested list values."""
        expr = F("tags") != "python"

        # None equal "python"
        data = unwind_data({"tags": [["java", "rust"], ["go", "c++"]]})
        assert expr.evaluate(data) is True

        # Some equal "python"
        data = unwind_data({"tags": [["java", "python"], ["go"]]})
        assert expr.evaluate(data) is False

    def test_compound_with_nested_values(self) -> None:
        """Test compound expressions with nested field values."""
        expr = (F("age") > 18) & (F("tags") == "python")

        data = unwind_data({"age": [[20, 25], [30]], "tags": [["python", "java"], ["rust"]]})
        # age: any > 18, tags: any == "python"
        assert expr.evaluate(data) is True

        data = unwind_data({"age": [[10, 15], [12]], "tags": [["python", "java"], ["rust"]]})
        # age: no value > 18
        assert expr.evaluate(data) is False
