"""Unit tests for date/time cleaning variants (Clean.date, Clean.datetime, Clean.time, Clean.duration)."""

from datetime import datetime, date
import polars as pl
import pytest
import zeolite as z
from zeolite.exceptions import CleanConfigurationError


class TestCleanDate:
    """Tests for Clean.date with various date formats."""

    def test_clean_date_basic(self):
        """Test basic date cleaning instantiation and apply."""
        ref = z.ref("value")
        clean = z.Clean.date()
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": ["2023-01-15", "2023-12-31", "2024-06-01"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.dtype == pl.Date
        assert len(result) == 3

    def test_clean_date_handles_all_cases(self):
        """Test date cleaning with various formats, nulls, and invalid values."""
        ref = z.ref("value")
        clean = z.Clean.date(day_first=True)
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "2023-01-15",  # ISO format
                    "15/01/2023",  # Day first format
                    "44927",  # Excel serial
                    "2023-01-15T10:30",  # ISO with time
                    None,  # Null
                    "invalid",  # Invalid
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Check that valid dates are parsed and invalid/null remain as such
        assert result.item(0) is not None  # ISO format should parse
        assert result.item(4) is None  # Null preserved

    def test_clean_date_day_first_false(self):
        """Test date cleaning with month-first parsing."""
        ref = z.ref("value")
        clean = z.Clean.date(day_first=False)
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": ["01/15/2023", "12/31/2023"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.dtype == pl.Date

    def test_clean_date_invalid_output_format(self):
        """Test that invalid output_format raises CleanConfigurationError."""
        with pytest.raises(CleanConfigurationError) as exc_info:
            z.Clean.date(output_format="invalid")

        assert "output_format must be either pl.Date or pl.Datetime" in str(
            exc_info.value
        )

    def test_clean_date_leap_year(self):
        """Test date cleaning with leap year edge cases."""
        ref = z.ref("value")
        clean = z.Clean.date()
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {"value": ["2024-02-29", "2023-02-29", "2020-02-29", "1900-02-29"]}
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Valid leap year dates should parse, invalid should become null
        assert result.item(0) is not None  # 2024-02-29 (leap year)
        assert result.item(1) is None  # 2023-02-29 (not leap year)
        assert result.item(2) is not None  # 2020-02-29 (leap year)
        assert result.item(3) is None  # 1900-02-29 (century not leap year)

    def test_clean_date_edge_case_dates(self, edge_case_dates):
        """Test date cleaning with edge case dates."""
        ref = z.ref("value")
        clean = z.Clean.date()
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": edge_case_dates})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Very old date should work
        assert result.item(0) is not None  # "1900-01-01"
        # Valid leap year should work
        assert result.item(1) is not None  # "2024-02-29"
        # Invalid leap year should fail
        assert result.item(2) is None  # "2023-02-29"
        # Invalid month should fail
        assert result.item(3) is None  # "2024-13-01"
        # Invalid day should fail
        assert result.item(4) is None  # "2024-12-32"

    def test_clean_date_timezone_invalid(self):
        """Test date cleaning with invalid timezone formats."""
        ref = z.ref("value")
        clean = z.Clean.date()
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "2023-01-15T10:30:00Z",  # Valid ISO with timezone
                    "2023-01-15T10:30:00+INVALID",  # Invalid timezone
                    "2023-01-15T10:30:00+25:00",  # Invalid timezone offset
                    "2023-01-15T10:30:00",  # No timezone
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Valid timezone/no timezone should parse, invalid should become null
        assert result.item(0) is not None  # Valid timezone
        assert result.item(1) is None  # Invalid timezone
        assert result.item(2) is None  # Invalid timezone offset
        assert result.item(3) is not None  # No timezone

    def test_clean_date_already_date_type(self):
        """Test date cleaning when input is already Date/Datetime type."""
        ref = z.ref("value")
        clean = z.Clean.date()
        expr = clean.apply(ref)

        # Test with already Date type
        df_date = pl.DataFrame({"value": [date(2023, 1, 15), date(2023, 12, 31)]})
        result_date = df_date.with_columns(expr).select(ref.clean().col).to_series()
        assert result_date.dtype == pl.Date
        assert len(result_date) == 2

        # Test with already Datetime type
        df_datetime = pl.DataFrame({"value": [datetime(2023, 1, 15, 10, 30)]})
        result_datetime = (
            df_datetime.with_columns(expr).select(ref.clean().col).to_series()
        )
        assert result_datetime.dtype == pl.Date or result_datetime.dtype == pl.Datetime

    def test_clean_date_whitespace_variations(self, whitespace_df):
        """Test date cleaning with whitespace edge cases."""
        ref = z.ref("value")
        clean = z.Clean.date()
        expr = clean.apply(ref)

        result = whitespace_df.with_columns(expr).select(ref.clean().col).to_series()

        # Empty strings and whitespace should become null
        assert result.item(0) is None  # ""
        assert result.item(1) is None  # "   "
        assert result.item(2) is None  # "\t"
        assert result.item(3) is None  # "\n"
        assert result.item(4) is None  # "\t\n"
        # Test values with whitespace should be sanitised
        assert result.item(5) is None  # "  test  " - not a date
        assert result.item(6) is None  # "\ttest\t" - not a date
        assert result.item(7) is None  # "\ntest\n" - not a date


class TestCleanDatetime:
    """Tests for Clean.datetime."""

    def test_clean_datetime_basic(self):
        """Test basic datetime cleaning instantiation and apply."""
        ref = z.ref("value")
        clean = z.Clean.datetime()
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": ["2023-01-15T10:30:00", "2023-12-31T23:59:59"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.dtype == pl.Datetime


class TestCleanTime:
    """Tests for Clean.time."""

    def test_clean_time_basic(self):
        """Test basic time cleaning instantiation and apply."""
        ref = z.ref("value")
        clean = z.Clean.time()
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": ["10:30:45", "14:45:00", "23:59:21"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.dtype == pl.Time

    def test_clean_time_custom_format(self):
        """Test time cleaning with custom format."""
        ref = z.ref("value")
        clean = z.Clean.time(time_format="%H:%M")
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": ["10:30", "14:45"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.dtype == pl.Time

    def test_clean_time_edge_cases(self):
        """Test time cleaning with edge cases."""
        ref = z.ref("value")
        clean = z.Clean.time()
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {"value": ["23:59:59", "24:00:00", "00:00:00", "12:30:45", "invalid"]}
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Valid times should parse, invalid should become null
        assert result.item(0) is not None  # "23:59:59"
        assert result.item(1) is None  # "24:00:00" - invalid time
        assert result.item(2) is not None  # "00:00:00"
        assert result.item(3) is not None  # "12:30:45"
        assert result.item(4) is None  # "invalid"


class TestCleanDuration:
    """Tests for Clean.duration."""

    def test_clean_duration_basic(self):
        """Test basic duration cleaning instantiation and apply."""
        ref = z.ref("value")
        clean = z.Clean.duration(input_unit="days")
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": [1, 7, 30]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.dtype == pl.Duration

    def test_clean_duration_various_units(self):
        """Test duration cleaning with different input units."""
        ref = z.ref("value")

        # Test hours
        clean_hours = z.Clean.duration(input_unit="hours")
        df = pl.DataFrame({"value": [1, 24, 48]})
        result = (
            df.with_columns(clean_hours.apply(ref)).select(ref.clean().col).to_series()
        )
        assert result.dtype == pl.Duration

        # Test minutes
        clean_minutes = z.Clean.duration(input_unit="minutes")
        result = (
            df.with_columns(clean_minutes.apply(ref))
            .select(ref.clean().col)
            .to_series()
        )
        assert result.dtype == pl.Duration
