"""Tests for Pandas expression visitor."""

import re

import pytest

pd = pytest.importorskip("pandas")

from therismos import F
from therismos.expr._expr import FALSE, TRUE, AllExpr, AnyExpr
from therismos.expr.visitors.pandas import PandasExprVisitor


@pytest.fixture()
def visitor() -> PandasExprVisitor:
    """Return a PandasExprVisitor instance."""
    return PandasExprVisitor()


@pytest.fixture()
def df() -> "pd.DataFrame":
    """Return a sample DataFrame for integration tests."""
    return pd.DataFrame(
        {
            "age": [20, 15, 30, 25],
            "name": ["Alice", "Bob", "Charlie", "alice"],
            "score": [85.0, 72.0, 91.0, 60.0],
            "status": ["active", "inactive", "active", "active"],
            "tag": ["python", "java", "python", "rust"],
            "nullable": [1.0, None, 3.0, None],
        }
    )


class TestPandasExprVisitorReturnsCallable:
    """Tests that all visitors return PandasFilter callables."""

    def test_eq_returns_callable(self, visitor: PandasExprVisitor) -> None:
        """Test that Eq returns a callable."""
        expr = F("age") == 20
        result = expr.accept(visitor)
        assert callable(result)

    def test_ne_returns_callable(self, visitor: PandasExprVisitor) -> None:
        """Test that Ne returns a callable."""
        expr = F("age") != 20
        result = expr.accept(visitor)
        assert callable(result)

    def test_lt_returns_callable(self, visitor: PandasExprVisitor) -> None:
        """Test that Lt returns a callable."""
        expr = F("age") < 20
        result = expr.accept(visitor)
        assert callable(result)

    def test_le_returns_callable(self, visitor: PandasExprVisitor) -> None:
        """Test that Le returns a callable."""
        expr = F("age") <= 20
        result = expr.accept(visitor)
        assert callable(result)

    def test_gt_returns_callable(self, visitor: PandasExprVisitor) -> None:
        """Test that Gt returns a callable."""
        expr = F("age") > 20
        result = expr.accept(visitor)
        assert callable(result)

    def test_ge_returns_callable(self, visitor: PandasExprVisitor) -> None:
        """Test that Ge returns a callable."""
        expr = F("age") >= 20
        result = expr.accept(visitor)
        assert callable(result)


class TestPandasExprVisitorComparisons:
    """Tests for comparison expression conversions."""

    @pytest.mark.parametrize(
        "expr,expected_count",
        [
            (F("age") == 20, 1),
            (F("age") != 20, 3),
            (F("age") < 20, 1),
            (F("age") <= 20, 2),
            (F("age") > 20, 2),
            (F("age") >= 20, 3),
        ],
    )
    def test_comparison_filters(
        self,
        visitor: PandasExprVisitor,
        df: "pd.DataFrame",
        expr: object,
        expected_count: int,
    ) -> None:
        """Test that comparison expressions filter correctly."""
        mask = expr.accept(visitor)  # type: ignore[union-attr]
        result = df[mask(df)]
        assert len(result) == expected_count


class TestPandasExprVisitorRegex:
    """Tests for regex expression conversion."""

    def test_regex_without_flags(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test regex match without flags."""
        expr = F("name").matches(r"^Alice$")
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 1
        assert result["name"].iloc[0] == "Alice"

    def test_regex_case_insensitive(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test regex match with IGNORECASE flag."""
        expr = F("name").matches(r"^alice$", flags=re.IGNORECASE)
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 2

    def test_regex_partial_match(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test regex partial string match."""
        expr = F("tag").matches(r"py")
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 2

    def test_regex_no_na(self, visitor: PandasExprVisitor) -> None:
        """Test regex with NA values does not raise (na=False)."""
        df_with_na = pd.DataFrame({"text": ["hello", None, "world"]})
        expr = F("text").matches(r"hello")
        mask = expr.accept(visitor)
        result = df_with_na[mask(df_with_na)]
        assert len(result) == 1


class TestPandasExprVisitorIn:
    """Tests for IN expression conversion."""

    def test_in_filter(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test IN expression filters correctly."""
        expr = F("tag").is_in("python", "java")
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 3

    def test_in_single_value(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test IN expression with a single value."""
        expr = F("status").is_in("inactive")
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 1

    def test_in_no_match(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test IN expression with no matching values."""
        expr = F("tag").is_in("cobol", "fortran")
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 0


class TestPandasExprVisitorIsNull:
    """Tests for IsNull expression conversion."""

    def test_is_null_filter(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test IsNull expression filters null values correctly."""
        expr = F("nullable").is_null()
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 2

    def test_is_not_null_filter(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test IsNull(is_null=False) filters non-null values correctly."""
        expr = F("nullable").is_not_null()
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 2


class TestPandasExprVisitorConstants:
    """Tests for TrueExpr and FalseExpr conversion."""

    def test_true_expr_matches_all(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test TrueExpr matches all rows."""
        mask = TRUE.accept(visitor)
        result = df[mask(df)]
        assert len(result) == len(df)

    def test_false_expr_matches_none(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test FalseExpr matches no rows."""
        mask = FALSE.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 0


class TestPandasExprVisitorLogical:
    """Tests for logical expression conversion (AND, OR, NOT)."""

    def test_all_expr(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test AllExpr (AND) filters correctly."""
        expr = (F("age") > 18) & (F("status") == "active")
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert all(result["age"] > 18)
        assert all(result["status"] == "active")

    def test_any_expr(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test AnyExpr (OR) filters correctly."""
        expr = (F("age") > 25) | (F("status") == "inactive")
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 2

    def test_not_expr(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test NotExpr negates correctly."""
        expr = ~(F("status") == "active")
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 1
        assert result["status"].iloc[0] == "inactive"

    def test_empty_all_expr_matches_all(
        self, visitor: PandasExprVisitor, df: "pd.DataFrame"
    ) -> None:
        """Test empty AllExpr matches all rows."""
        expr = AllExpr()
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == len(df)

    def test_empty_any_expr_matches_none(
        self, visitor: PandasExprVisitor, df: "pd.DataFrame"
    ) -> None:
        """Test empty AnyExpr matches no rows."""
        expr = AnyExpr()
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 0

    def test_nested_logical(self, visitor: PandasExprVisitor, df: "pd.DataFrame") -> None:
        """Test nested logical expressions."""
        expr = ((F("age") >= 20) & (F("age") <= 25)) | (F("name") == "Bob")
        mask = expr.accept(visitor)
        result = df[mask(df)]
        assert len(result) == 3

    def test_closure_captures_correct_values(
        self, visitor: PandasExprVisitor, df: "pd.DataFrame"
    ) -> None:
        """Test that closures capture values correctly (no loop variable issues)."""
        exprs = [F("age") == v for v in [20, 15]]
        masks = [e.accept(visitor) for e in exprs]
        result0 = df[masks[0](df)]
        result1 = df[masks[1](df)]
        assert len(result0) == 1
        assert result0["age"].iloc[0] == 20
        assert len(result1) == 1
        assert result1["age"].iloc[0] == 15
