"""Tests for DataFrame row validation with Pydantic."""

from typing import Any

import numpy as np
import pandas as pd
import polars as pl
import pytest
from pydantic import BaseModel, ConfigDict, Field

from daffy.pydantic_types import require_pydantic
from daffy.row_validation import validate_dataframe_rows


class SimpleValidator(BaseModel):
    """Simple test validator."""

    model_config = ConfigDict(str_strip_whitespace=True)

    name: str
    age: int = Field(ge=0, le=120)
    price: float = Field(gt=0)


class TestPandasValidation:
    """Test row validation with pandas DataFrames."""

    def test_all_valid_rows(self) -> None:
        """Test validation passes when all rows are valid."""

        df = pd.DataFrame(
            {
                "name": ["Alice", "Bob", "Charlie"],
                "age": [25, 30, 35],
                "price": [10.5, 20.0, 15.75],
            }
        )

        # Should not raise
        validate_dataframe_rows(df, SimpleValidator)

    def test_batch_validation_with_errors(self) -> None:
        """Test batch validation catches multiple errors efficiently."""

        df = pd.DataFrame(
            {
                "name": ["Alice", "Bob", "Charlie", "David"],
                "age": [25, -5, 35, 150],  # Bob and David invalid
                "price": [10.5, 20.0, -15.0, 30.0],  # Charlie invalid
            }
        )

        with pytest.raises(AssertionError) as exc_info:
            validate_dataframe_rows(df, SimpleValidator)

        message = str(exc_info.value)
        assert "Row validation failed for" in message
        assert "Row 1:" in message  # Bob (index 1)
        assert "age" in message

    def test_nan_handling(self) -> None:
        """Test NaN values are converted to None and fail validation properly."""

        df = pd.DataFrame(
            {
                "name": ["Alice", "Bob"],
                "age": [25, 30],
                "price": [10.5, np.nan],  # NaN should convert to None and fail
            }
        )

        with pytest.raises(AssertionError) as exc_info:
            validate_dataframe_rows(df, SimpleValidator)

        message = str(exc_info.value)
        assert "Row 1:" in message
        assert "price" in message

    def test_nan_handling_disabled(self) -> None:
        """Test that NaN conversion can be disabled."""

        df = pd.DataFrame(
            {
                "name": ["Alice", "Bob"],
                "age": [25, 30],
                "price": [10.5, np.nan],
            }
        )

        # With convert_nans=False, should still fail but differently
        with pytest.raises(AssertionError):
            validate_dataframe_rows(df, SimpleValidator, convert_nans=False)

    def test_non_integer_index(self) -> None:
        """Test validation works with non-integer DataFrame index."""

        df = pd.DataFrame(
            {
                "name": ["Alice", "Bob"],
                "age": [25, -5],  # Bob invalid
                "price": [10.5, 20.0],
            },
            index=["person_a", "person_b"],  # type: ignore[arg-type]
        )

        with pytest.raises(AssertionError) as exc_info:
            validate_dataframe_rows(df, SimpleValidator)

        message = str(exc_info.value)
        assert "Row person_b:" in message  # Uses actual index label

    def test_datetime_index(self) -> None:
        """Test validation works with datetime index."""

        dates = pd.date_range("2025-01-01", periods=2)
        df = pd.DataFrame(
            {
                "name": ["Alice", "Bob"],
                "age": [25, -5],  # Bob invalid
                "price": [10.5, 20.0],
            },
            index=dates,
        )

        with pytest.raises(AssertionError) as exc_info:
            validate_dataframe_rows(df, SimpleValidator)

        message = str(exc_info.value)
        assert "Row 2025-01-02" in message  # Uses date in error

    def test_missing_required_field(self) -> None:
        """Test validation fails when required field is missing."""

        df = pd.DataFrame(
            {
                "name": ["Alice"],
                "age": [25],
                # Missing 'price' column
            }
        )

        with pytest.raises(AssertionError) as exc_info:
            validate_dataframe_rows(df, SimpleValidator)

        message = str(exc_info.value)
        assert "price" in message.lower()

    def test_type_mismatch(self) -> None:
        """Test validation fails with informative error for type mismatches."""

        df = pd.DataFrame(
            {
                "name": ["Alice"],
                "age": ["twenty-five"],  # String instead of int
                "price": [10.5],
            }
        )

        with pytest.raises(AssertionError) as exc_info:
            validate_dataframe_rows(df, SimpleValidator)

        message = str(exc_info.value)
        assert "age" in message
        assert "int" in message.lower() or "integer" in message.lower()

    def test_empty_dataframe(self) -> None:
        """Test validation passes for empty DataFrame."""

        df = pd.DataFrame(columns=["name", "age", "price"])  # type: ignore[arg-type]

        # Should not raise for empty DataFrame
        validate_dataframe_rows(df, SimpleValidator)


class TestPolarsValidation:
    """Test row validation with polars DataFrames."""

    def test_polars_valid_rows(self) -> None:
        """Test validation passes with valid polars DataFrame."""

        df = pl.DataFrame(
            {
                "name": ["Alice", "Bob"],
                "age": [25, 30],
                "price": [10.5, 20.0],
            }
        )

        validate_dataframe_rows(df, SimpleValidator)

    def test_polars_invalid_rows(self) -> None:
        """Test validation fails with invalid polars DataFrame."""

        df = pl.DataFrame(
            {
                "name": ["Alice", "Bob"],
                "age": [25, -5],  # Bob invalid
                "price": [10.5, 20.0],
            }
        )

        with pytest.raises(AssertionError) as exc_info:
            validate_dataframe_rows(df, SimpleValidator)

        message = str(exc_info.value)
        assert "Row 1:" in message  # Polars uses position
        assert "age" in message

    def test_polars_null_handling(self) -> None:
        """Test Polars null values are handled properly."""

        df = pl.DataFrame(
            {
                "name": ["Alice", "Bob"],
                "age": [25, 30],
                "price": [10.5, None],  # Polars uses None
            }
        )

        with pytest.raises(AssertionError) as exc_info:
            validate_dataframe_rows(df, SimpleValidator)

        message = str(exc_info.value)
        assert "Row 1:" in message


def test_max_errors_limit() -> None:
    """Test that max_errors limits the number of errors shown."""

    df = pd.DataFrame(
        {
            "name": [str(i) for i in range(10)],
            "age": [-1] * 10,  # All invalid
            "price": [10.0] * 10,
        }
    )

    with pytest.raises(AssertionError) as exc_info:
        validate_dataframe_rows(df, SimpleValidator, max_errors=3)

    message = str(exc_info.value)

    # Should show first 3 errors
    assert "Row 0:" in message
    assert "Row 1:" in message
    assert "Row 2:" in message

    # Should indicate more errors exist
    assert "7 more row(s) with errors" in message


def test_invalid_dataframe_type() -> None:
    """Test that non-DataFrame types raise TypeError."""

    with pytest.raises(TypeError, match="Expected DataFrame"):
        validate_dataframe_rows([1, 2, 3], SimpleValidator)  # type: ignore[arg-type]


def test_require_pydantic_error() -> None:
    """Test error message when Pydantic not available."""
    # This test verifies the error message is correct
    # In real isolation testing, Pydantic won't be available

    # In this environment Pydantic is available, so just verify the function exists
    require_pydantic()  # Should not raise


def test_multiple_field_errors() -> None:
    """Test that multiple field errors in same row are reported."""

    df = pd.DataFrame(
        {
            "name": ["Alice", "Bob"],
            "age": [25, 150],  # Too high
            "price": [10.5, -20.0],  # Negative
        }
    )

    with pytest.raises(AssertionError) as exc_info:
        validate_dataframe_rows(df, SimpleValidator)

    message = str(exc_info.value)
    assert "Row 1:" in message
    # Should show both errors for Bob's row
    assert "age" in message
    assert "price" in message


def test_whitespace_stripping() -> None:
    """Test that ConfigDict str_strip_whitespace works."""

    df = pd.DataFrame(
        {
            "name": ["  Alice  ", "  Bob  "],  # Extra whitespace
            "age": [25, 30],
            "price": [10.5, 20.0],
        }
    )

    # Should pass because ConfigDict strips whitespace
    validate_dataframe_rows(df, SimpleValidator)


def test_unknown_dataframe_type() -> None:
    require_pydantic()

    class NotADataFrame:
        pass

    fake_df = NotADataFrame()

    with pytest.raises(TypeError, match="Unknown DataFrame type"):
        from daffy.row_validation import _iterate_dataframe_with_index

        list(_iterate_dataframe_with_index(fake_df))  # type: ignore[arg-type]


def test_iterative_validation_fallback(mocker: Any) -> None:
    require_pydantic()

    df = pd.DataFrame(
        {
            "name": ["Alice", "Bob", "Charlie"],
            "age": [25, -5, 150],  # Two invalid ages
            "price": [10.5, 20.0, 30.0],
        }
    )

    # Mock TypeAdapter to raise TypeError, forcing iterative validation
    from daffy import row_validation

    original_adapter = row_validation.TypeAdapter
    assert original_adapter is not None

    def mock_adapter(*args: Any, **kwargs: Any) -> Any:
        adapter = original_adapter(*args, **kwargs)  # type: ignore[misc]

        def failing_validate(*args: Any, **kwargs: Any) -> Any:
            raise TypeError("Forced fallback to iterative")

        adapter.validate_python = failing_validate
        return adapter

    mocker.patch.object(row_validation, "TypeAdapter", side_effect=mock_adapter)

    with pytest.raises(AssertionError) as exc_info:
        validate_dataframe_rows(df, SimpleValidator, max_errors=5)

    message = str(exc_info.value)
    assert "Row validation failed" in message
    assert "Row 1:" in message or "Row 2:" in message


def test_iterative_validation_fallback_polars(mocker: Any) -> None:
    require_pydantic()

    df = pl.DataFrame(
        {
            "name": ["Alice", "Bob", "Charlie"],
            "age": [25, -5, 150],
            "price": [10.5, 20.0, 30.0],
        }
    )

    from daffy import row_validation

    original_adapter = row_validation.TypeAdapter
    assert original_adapter is not None

    def mock_adapter(*args: Any, **kwargs: Any) -> Any:
        adapter = original_adapter(*args, **kwargs)  # type: ignore[misc]

        def failing_validate(*args: Any, **kwargs: Any) -> Any:
            raise TypeError("Forced fallback to iterative")

        adapter.validate_python = failing_validate
        return adapter

    mocker.patch.object(row_validation, "TypeAdapter", side_effect=mock_adapter)

    with pytest.raises(AssertionError) as exc_info:
        validate_dataframe_rows(df, SimpleValidator, max_errors=5)

    message = str(exc_info.value)
    assert "Row validation failed" in message
