"""Tests for Pandas sort specification visitor."""

import pytest

pd = pytest.importorskip("pandas")

from therismos.sorting import SortCriterion, SortOrder, SortSpec
from therismos.sorting.visitors.pandas import PandasSortSpec, PandasSortSpecVisitor


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


@pytest.fixture()
def df() -> "pd.DataFrame":
    """Return a sample DataFrame for integration tests."""
    return pd.DataFrame(
        {
            "age": [25, 30, 20, 25],
            "name": ["Bob", "Alice", "Charlie", "Alice"],
            "score": [85, 91, 72, 60],
        }
    )


class TestPandasSortSpecVisitorBasic:
    """Tests for basic PandasSortSpecVisitor functionality."""

    def test_single_ascending_criterion(self, visitor: PandasSortSpecVisitor) -> None:
        """Test Pandas sort spec for single ascending criterion."""
        spec = SortSpec([SortCriterion("age", SortOrder.ASCENDING)])
        result = spec.accept(visitor)
        assert result.by == ("age",)
        assert result.ascending == (True,)

    def test_single_descending_criterion(self, visitor: PandasSortSpecVisitor) -> None:
        """Test Pandas sort spec for single descending criterion."""
        spec = SortSpec([SortCriterion("name", SortOrder.DESCENDING)])
        result = spec.accept(visitor)
        assert result.by == ("name",)
        assert result.ascending == (False,)

    def test_multiple_criteria(self, visitor: PandasSortSpecVisitor) -> None:
        """Test Pandas sort spec for multiple criteria."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.DESCENDING),
            ]
        )
        result = spec.accept(visitor)
        assert result.by == ("age", "name")
        assert result.ascending == (True, False)

    def test_empty_spec(self, visitor: PandasSortSpecVisitor) -> None:
        """Test Pandas sort spec for empty spec."""
        spec = SortSpec()
        result = spec.accept(visitor)
        assert result.by == ()
        assert result.ascending == ()

    def test_result_is_pandas_sort_spec(self, visitor: PandasSortSpecVisitor) -> None:
        """Test that result is a PandasSortSpec."""
        spec = SortSpec([SortCriterion("x", SortOrder.ASCENDING)])
        result = spec.accept(visitor)
        assert isinstance(result, PandasSortSpec)


class TestPandasSortSpecVisitorNoneOrder:
    """Tests for handling SortOrder.NONE."""

    def test_single_none_criterion_skipped(self, visitor: PandasSortSpecVisitor) -> None:
        """Test that NONE order is skipped."""
        spec = SortSpec([SortCriterion("age", SortOrder.NONE)])
        result = spec.accept(visitor)
        assert result.by == ()
        assert result.ascending == ()

    def test_none_mixed_with_other_orders(self, visitor: PandasSortSpecVisitor) -> None:
        """Test that NONE orders are skipped while others are preserved."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.NONE),
                SortCriterion("score", SortOrder.DESCENDING),
            ]
        )
        result = spec.accept(visitor)
        assert result.by == ("age", "score")
        assert result.ascending == (True, False)
        assert "name" not in result.by

    def test_all_none_orders(self, visitor: PandasSortSpecVisitor) -> None:
        """Test spec with all NONE orders produces empty result."""
        spec = SortSpec(
            [
                SortCriterion("a", SortOrder.NONE),
                SortCriterion("b", SortOrder.NONE),
            ]
        )
        result = spec.accept(visitor)
        assert result.by == ()
        assert result.ascending == ()


@pytest.mark.parametrize(
    "criteria,expected_by,expected_ascending",
    [
        (
            [SortCriterion("x", SortOrder.ASCENDING)],
            ("x",),
            (True,),
        ),
        (
            [SortCriterion("y", SortOrder.DESCENDING)],
            ("y",),
            (False,),
        ),
        (
            [SortCriterion("a", SortOrder.ASCENDING), SortCriterion("b", SortOrder.DESCENDING)],
            ("a", "b"),
            (True, False),
        ),
        (
            [SortCriterion("a", SortOrder.ASCENDING), SortCriterion("b", SortOrder.NONE)],
            ("a",),
            (True,),
        ),
        ([], (), ()),
    ],
)
def test_pandas_sort_visitor_parametrized(
    criteria: list[SortCriterion],
    expected_by: tuple[str, ...],
    expected_ascending: tuple[bool, ...],
) -> None:
    """Parametrized test for Pandas sort visitor with various specs."""
    spec = SortSpec(criteria)
    visitor = PandasSortSpecVisitor()
    result = spec.accept(visitor)
    assert result.by == expected_by
    assert result.ascending == expected_ascending


class TestPandasSortSpecVisitorIntegration:
    """Integration tests for Pandas sort visitor with actual DataFrames."""

    def test_sort_ascending(self, visitor: PandasSortSpecVisitor, df: "pd.DataFrame") -> None:
        """Test ascending sort on DataFrame."""
        spec = SortSpec([SortCriterion("age", SortOrder.ASCENDING)])
        sort = spec.accept(visitor)
        result = df.sort_values(by=list(sort.by), ascending=list(sort.ascending))
        ages = result["age"].tolist()
        assert ages == sorted(ages)

    def test_sort_descending(self, visitor: PandasSortSpecVisitor, df: "pd.DataFrame") -> None:
        """Test descending sort on DataFrame."""
        spec = SortSpec([SortCriterion("score", SortOrder.DESCENDING)])
        sort = spec.accept(visitor)
        result = df.sort_values(by=list(sort.by), ascending=list(sort.ascending))
        scores = result["score"].tolist()
        assert scores == sorted(scores, reverse=True)

    def test_multi_column_sort(self, visitor: PandasSortSpecVisitor, df: "pd.DataFrame") -> None:
        """Test multi-column sort on DataFrame."""
        spec = SortSpec(
            [
                SortCriterion("age", SortOrder.ASCENDING),
                SortCriterion("name", SortOrder.ASCENDING),
            ]
        )
        sort = spec.accept(visitor)
        result = df.sort_values(by=list(sort.by), ascending=list(sort.ascending))
        assert result["age"].iloc[0] == 20

    def test_ascending_polarity_opposite_to_polars(self, visitor: PandasSortSpecVisitor) -> None:
        """Test that Pandas ascending uses opposite polarity to Polars descending."""
        spec = SortSpec([SortCriterion("field", SortOrder.ASCENDING)])
        result = spec.accept(visitor)
        assert result.ascending == (True,)

        spec2 = SortSpec([SortCriterion("field", SortOrder.DESCENDING)])
        result2 = spec2.accept(visitor)
        assert result2.ascending == (False,)
