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

import re
from contextlib import AbstractContextManager, nullcontext
from typing import Any

import pytest

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


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.eval({"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.eval({"value": field_value}) is expected

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (10, 20, True),
            (20, 10, False),
            (10, 10, False),
            (5.5, 10.5, True),
        ],
    )
    def test_lt_eval(
        self, field_value: int | float, expr_value: int | float, expected: bool
    ) -> None:
        """Test less-than expression evaluation."""
        field = F("value")
        expr = field < expr_value
        assert expr.eval({"value": field_value}) is expected

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (10, 20, True),
            (20, 10, False),
            (10, 10, True),
            (5.5, 10.5, True),
        ],
    )
    def test_le_eval(
        self, field_value: int | float, expr_value: int | float, expected: bool
    ) -> None:
        """Test less-than-or-equal expression evaluation."""
        field = F("value")
        expr = field <= expr_value
        assert expr.eval({"value": field_value}) is expected

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (20, 10, True),
            (10, 20, False),
            (10, 10, False),
            (10.5, 5.5, True),
        ],
    )
    def test_gt_eval(
        self, field_value: int | float, expr_value: int | float, expected: bool
    ) -> None:
        """Test greater-than expression evaluation."""
        field = F("value")
        expr = field > expr_value
        assert expr.eval({"value": field_value}) is expected

    @pytest.mark.parametrize(
        ("field_value", "expr_value", "expected"),
        [
            (20, 10, True),
            (10, 20, False),
            (10, 10, True),
            (10.5, 5.5, True),
        ],
    )
    def test_ge_eval(
        self, field_value: int | float, expr_value: int | float, expected: bool
    ) -> None:
        """Test greater-than-or-equal expression evaluation."""
        field = F("value")
        expr = field >= expr_value
        assert expr.eval({"value": field_value}) is expected


class TestTypedFieldEval:
    """Test evaluation with typed fields and casting."""

    @pytest.mark.parametrize(
        ("field_value", "field_type", "expr_value", "expected"),
        [
            ("42", int, 42, True),
            ("43", int, 42, False),
            (42, str, "42", True),
            ("3.14", float, 3.14, True),
        ],
    )
    def test_eq_with_casting(
        self,
        field_value: int | str | float,
        field_type: type,
        expr_value: int | str | float,
        expected: bool,
    ) -> None:
        """Test equality with automatic type casting."""
        field = F("value", field_type)
        expr = field == expr_value
        assert expr.eval({"value": field_value}) is expected

    @pytest.mark.parametrize(
        ("field_value", "field_type", "expr_value", "expected"),
        [
            ("10", int, 20, True),
            ("20", int, 10, False),
            ("10", int, 10, False),
        ],
    )
    def test_lt_with_casting(
        self,
        field_value: str,
        field_type: type,
        expr_value: int,
        expected: bool,
    ) -> None:
        """Test less-than with automatic type casting."""
        field = F("value", field_type)
        expr = field < expr_value
        assert expr.eval({"value": field_value}) is expected

    def test_casting_error_propagation(self) -> None:
        """Test that casting errors are properly raised during eval."""
        field = F("value", int)
        expr = field == 42
        with pytest.raises((TypeError, ValueError)):
            expr.eval({"value": "not_a_number"})


class TestRegexEval:
    """Test evaluation of regex expressions."""

    @pytest.mark.parametrize(
        ("field_value", "pattern", "flags", "expected"),
        [
            ("hello world", r"hello", None, True),
            ("hello world", r"goodbye", None, False),
            ("hello world", r"^hello", None, True),
            ("hello world", r"world$", None, True),
            ("hello world", r"^world", None, False),
            ("Hello World", r"hello", None, False),
            ("Hello World", r"hello", re.IGNORECASE, True),
            ("test123", r"\d+", None, True),
            ("test", r"\d+", None, False),
        ],
    )
    def test_regex_eval(
        self, field_value: str, pattern: str, flags: int | None, expected: bool
    ) -> None:
        """Test regex matching expression evaluation."""
        field = F("text")
        expr = field.matches(pattern, flags)
        assert expr.eval({"text": field_value}) is expected


class TestInEval:
    """Test evaluation of IN expressions."""

    @pytest.mark.parametrize(
        ("field_value", "allowed_values", "expected"),
        [
            (1, (1, 2, 3), True),
            (4, (1, 2, 3), False),
            ("apple", ("apple", "banana", "orange"), True),
            ("grape", ("apple", "banana", "orange"), False),
            (2.5, (1.5, 2.5, 3.5), True),
            (4.5, (1.5, 2.5, 3.5), False),
        ],
    )
    def test_in_eval(
        self,
        field_value: int | str | float,
        allowed_values: tuple[int | str | float, ...],
        expected: bool,
    ) -> None:
        """Test IN expression evaluation."""
        field = F("value")
        expr = field.is_in(*allowed_values)
        assert expr.eval({"value": field_value}) is expected

    @pytest.mark.parametrize(
        ("field_value", "field_type", "allowed_values", "expected"),
        [
            ("1", int, (1, 2, 3), True),
            ("4", int, (1, 2, 3), False),
            (1, str, ("1", "2", "3"), True),
        ],
    )
    def test_in_eval_with_casting(
        self,
        field_value: str | int,
        field_type: type[int] | type[str],
        allowed_values: tuple[int | str, ...],
        expected: bool,
    ) -> None:
        """Test IN expression evaluation with type casting."""
        field = F("value", field_type)
        expr = field.is_in(*allowed_values)
        assert expr.eval({"value": field_value}) is expected

    def test_is_one_of_eval(self) -> None:
        """Test is_one_of method for IN expression."""
        field = F("status")
        expr = field.is_one_of(["active", "pending", "approved"])
        assert expr.eval({"status": "active"}) is True
        assert expr.eval({"status": "rejected"}) is False


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

    @pytest.mark.parametrize(
        ("field_value", "is_null", "expected"),
        [
            (None, True, True),
            ("value", True, False),
            (0, True, False),
            ("", True, False),
            (None, False, False),
            ("value", False, True),
            (0, False, True),
        ],
    )
    def test_is_null_eval(
        self, field_value: str | int | None, is_null: bool, expected: bool
    ) -> None:
        """Test null-checking expression evaluation."""
        field = F("value")
        expr = field.is_null() if is_null else field.is_not_null()
        assert expr.eval({"value": field_value}) is expected


class TestConstantEval:
    """Test evaluation of constant expressions."""

    def test_true_expr_eval(self) -> None:
        """Test TRUE constant always evaluates to True."""
        assert TRUE.eval({}) is True
        assert TRUE.eval({"any": "data"}) is True

    def test_false_expr_eval(self) -> None:
        """Test FALSE constant always evaluates to False."""
        assert FALSE.eval({}) is False
        assert FALSE.eval({"any": "data"}) is False


class TestCompoundEval:
    """Test evaluation of compound expressions."""

    @pytest.mark.parametrize(
        ("age", "status", "expected"),
        [
            (25, "active", True),
            (15, "active", False),
            (25, "inactive", False),
            (15, "inactive", False),
        ],
    )
    def test_all_expr_eval(self, age: int, status: str, expected: bool) -> None:
        """Test AND expression evaluation."""
        age_field = F("age")
        status_field = F("status")
        expr = AllExpr(age_field > 18, status_field == "active")
        assert expr.eval({"age": age, "status": status}) is expected

    @pytest.mark.parametrize(
        ("role", "status", "expected"),
        [
            ("admin", "inactive", True),
            ("user", "active", True),
            ("admin", "active", True),
            ("user", "inactive", False),
        ],
    )
    def test_any_expr_eval(self, role: str, status: str, expected: bool) -> None:
        """Test OR expression evaluation."""
        role_field = F("role")
        status_field = F("status")
        expr = AnyExpr(role_field == "admin", status_field == "active")
        assert expr.eval({"role": role, "status": status}) is expected

    @pytest.mark.parametrize(
        ("age", "expected"),
        [
            (15, True),
            (18, False),
            (25, False),
        ],
    )
    def test_not_expr_eval(self, age: int, expected: bool) -> None:
        """Test NOT expression evaluation."""
        age_field = F("age")
        expr = NotExpr(age_field >= 18)
        assert expr.eval({"age": age}) is expected

    def test_nested_compound_eval(self) -> None:
        """Test nested compound expression evaluation."""
        age = F("age")
        status = F("status")
        role = F("role")

        # ((age > 18 AND status == "active") OR role == "admin")
        expr = AnyExpr(
            AllExpr(age > 18, status == "active"),
            role == "admin",
        )

        # Admin with any age and status
        assert expr.eval({"age": 15, "status": "inactive", "role": "admin"}) is True

        # Adult with active status
        assert expr.eval({"age": 25, "status": "active", "role": "user"}) is True

        # Minor with active status (not admin)
        assert expr.eval({"age": 15, "status": "active", "role": "user"}) is False

        # Adult with inactive status (not admin)
        assert expr.eval({"age": 25, "status": "inactive", "role": "user"}) is False

    def test_complex_nested_eval(self) -> None:
        """Test complex nested expression evaluation."""
        age = F("age", int)
        status = F("status")
        role = F("role")

        # NOT((age >= 18 AND status == "active") OR role == "admin")
        expr = ~((age >= 18) & (status == "active") | (role == "admin"))

        # Should be False for admin
        assert expr.eval({"age": 15, "status": "inactive", "role": "admin"}) is False

        # Should be False for adult with active status
        assert expr.eval({"age": 25, "status": "active", "role": "user"}) is False

        # Should be True for minor with inactive status
        assert expr.eval({"age": 15, "status": "inactive", "role": "user"}) is True


class TestEmptyCompoundEval:
    """Test evaluation of empty compound expressions."""

    def test_empty_all_expr_eval(self) -> None:
        """Test empty AND expression evaluates to True (vacuous truth)."""
        expr = AllExpr()
        assert expr.eval({}) is True

    def test_empty_any_expr_eval(self) -> None:
        """Test empty OR expression evaluates to False."""
        expr = AnyExpr()
        assert expr.eval({}) is False


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

    def test_missing_field_raises_key_error(self) -> None:
        """Test that missing field raises KeyError."""
        field = F("missing")
        expr = field == 42
        with pytest.raises(KeyError):
            expr.eval({"other": 10})

    @pytest.mark.parametrize(
        ("field_type", "field_value", "expr_value", "expectation"),
        [
            (int, "not_a_number", 42, pytest.raises((TypeError, ValueError))),
            (int, 42, 42, nullcontext()),
            (float, "3.14", 3.14, nullcontext()),
            (str, 42, "42", nullcontext()),
        ],
    )
    def test_casting_errors(
        self,
        field_type: type[int] | type[float] | type[str],
        field_value: str | int | float,
        expr_value: int | float | str,
        expectation: AbstractContextManager[Any],
    ) -> None:
        """Test that invalid casts raise appropriate errors."""
        field = F("value", field_type)
        expr = field == expr_value
        with expectation:
            expr.eval({"value": field_value})


class TestRealWorldScenarios:
    """Test evaluation in real-world scenarios."""

    def test_user_eligibility_check(self) -> None:
        """Test complex user eligibility criteria."""
        age = F("age", int)
        country = F("country")
        verified = F("verified")
        subscription = F("subscription")

        # Eligible if: (age >= 18 AND country in allowed list) AND (verified OR has subscription)
        expr = AllExpr(
            age >= 18,
            country.is_one_of(["US", "UK", "CA"]),
            AnyExpr(
                verified == True,  # noqa: E712
                subscription.is_in("premium", "enterprise"),
            ),
        )

        # Eligible: adult, allowed country, verified
        assert (
            expr.eval(
                {
                    "age": 25,
                    "country": "US",
                    "verified": True,
                    "subscription": "free",
                }
            )
            is True
        )

        # Eligible: adult, allowed country, premium subscription
        assert (
            expr.eval(
                {
                    "age": 30,
                    "country": "UK",
                    "verified": False,
                    "subscription": "premium",
                }
            )
            is True
        )

        # Not eligible: minor
        assert (
            expr.eval(
                {
                    "age": 16,
                    "country": "US",
                    "verified": True,
                    "subscription": "premium",
                }
            )
            is False
        )

        # Not eligible: wrong country
        assert (
            expr.eval(
                {
                    "age": 25,
                    "country": "FR",
                    "verified": True,
                    "subscription": "premium",
                }
            )
            is False
        )

        # Not eligible: not verified and no subscription
        assert (
            expr.eval(
                {
                    "age": 25,
                    "country": "US",
                    "verified": False,
                    "subscription": "free",
                }
            )
            is False
        )

    def test_content_filtering(self) -> None:
        """Test content filtering criteria."""
        title = F("title")
        tags = F("tags")
        rating = F("rating", float)
        status = F("status")

        # Match: (title contains "python" OR "data" in tags) AND rating >= 4.0 AND status is active
        expr = AllExpr(
            AnyExpr(
                title.matches(r"python", re.IGNORECASE),
                tags.matches(r"\bdata\b"),
            ),
            rating >= 4.0,
            status == "active",
        )

        # Should match: python in title, high rating, active
        assert (
            expr.eval(
                {
                    "title": "Learning Python",
                    "tags": "programming,tutorial",
                    "rating": 4.5,
                    "status": "active",
                }
            )
            is True
        )

        # Should match: data in tags, high rating, active
        assert (
            expr.eval(
                {
                    "title": "Advanced Analytics",
                    "tags": "data,analysis",
                    "rating": 4.8,
                    "status": "active",
                }
            )
            is True
        )

        # Should not match: low rating
        assert (
            expr.eval(
                {
                    "title": "Learning Python",
                    "tags": "programming",
                    "rating": 3.5,
                    "status": "active",
                }
            )
            is False
        )

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