"""Unit tests for number cleaning variants (Clean.number, Clean.integer, Clean.decimal)."""

import polars as pl
import pytest

import zeolite as z


class TestCleanNumberFloat:
    """Tests for cleaning data to float"""

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

        df = pl.DataFrame({"value": ["1.5", "2.7", "3.0"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.to_list() == [1.5, 2.7, 3.0]
        assert result.dtype == pl.Float64

    def test_clean_number_handles_all_input_types(self):
        """Test number cleaning with valid, invalid, null, formatted, and edge case values."""
        ref = z.ref("value")
        clean = z.Clean.number()
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "1.5",  # Valid float
                    "1,000",  # Thousand separator
                    "2 500.50",  # Spaces
                    "-1.5",  # Negative
                    "1e5",  # Scientific notation
                    None,  # Null
                    "",  # Empty string
                    "invalid",  # Invalid text
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.to_list() == [1.5, 1000.0, 2500.50, -1.5, 1e5, None, None, None]


class TestCleanNumberInteger:
    """Tests for cleaning data to float"""

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

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

        assert result.to_list() == [1, 2, 3]
        assert result.dtype == pl.Int64

    def test_clean_integer_handles_all_input_types(self):
        """Test integer cleaning with valid, invalid, null, formatted, and edge case values."""
        ref = z.ref("value")
        clean = z.Clean.integer()
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "1",  # Valid integer
                    "1.4",  # Float rounds to 1
                    "2.5",  # Float rounds to 2
                    "3.7",  # Float rounds to 4
                    "1,000",  # Thousand separator
                    " 2,500 ",  # Spaces and separator
                    "-1",  # Negative
                    None,  # Null
                    "",  # Empty string
                    "invalid",  # Invalid text
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.to_list() == [1, 1, 2, 4, 1000, 2500, -1, None, None, None]


class TestCleanNumberDecimal:
    def test_clean_decimal_basic(self):
        """Test basic decimal cleaning instantiation and apply with default 2 decimal places."""
        ref = z.ref("value")
        clean = z.Clean.decimal()
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": ["1.5", "2.75", "3.123"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.dtype == pl.Decimal(None, 2)
        assert float(result.item(0)) == 1.5
        assert float(result.item(1)) == 2.75
        assert float(result.item(2)) == pytest.approx(3.12, abs=0.01)  # Rounded to 2 places

    def test_clean_decimal_custom_decimal_places(self):
        """Test decimal cleaning with custom decimal places configuration."""
        ref = z.ref("value")
        clean = z.Clean.decimal(decimal_places=4)
        expr = clean.apply(ref)

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

        assert result.dtype == pl.Decimal(None, 4)

    def test_clean_decimal_handles_all_input_types(self):
        """Test decimal cleaning with valid, invalid, null, formatted, and edge case values."""
        ref = z.ref("value")
        clean = z.Clean.decimal()
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "1.5",  # Valid decimal
                    "1,000.50",  # Thousand separator
                    " 2,500.75 ",  # Spaces and separator
                    "-1.50",  # Negative
                    None,  # Null
                    "",  # Empty string
                    "invalid",  # Invalid text
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Check valid values were converted
        assert float(result.item(0)) == 1.5
        assert float(result.item(1)) == 1000.50
        assert float(result.item(2)) == 2500.75
        assert float(result.item(3)) == -1.50
        # Check invalid values became null
        assert result.item(4) is None
        assert result.item(5) is None
        assert result.item(6) is None

    def test_clean_number_scientific_notation(self, scientific_notation_df):
        """Test number cleaning with scientific notation edge cases."""
        ref = z.ref("value")
        clean = z.Clean.number()
        expr = clean.apply(ref)

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

        # Check scientific notation parsing
        assert result.item(0) == pytest.approx(1e-5)  # "1e-5"
        assert result.item(1) == pytest.approx(1.5e10, rel=1e-4)  # "1.5E+10"
        assert result.item(2) == pytest.approx(1.0)  # "1e+0"
        assert result.item(3) == pytest.approx(-1e-5)  # "-1e-5"
        assert result.item(4) == pytest.approx(2.5e-3)  # "2.5e-3"
        assert result.item(5) == pytest.approx(1.23e2)  # "1.23e+2"

    def test_clean_number_negative_zero(self):
        """Test number cleaning with negative zero."""
        ref = z.ref("value")
        clean = z.Clean.number()
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": ["-0", "0", "+0"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # All should be treated as zero
        assert result.to_list() == [0.0, 0.0, 0.0]

    def test_clean_number_edge_case_strings(self, edge_case_numbers):
        """Test number cleaning with edge case number strings."""
        ref = z.ref("value")
        clean = z.Clean.number()
        expr = clean.apply(ref)

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

        # "0" and "-0" should be 0.0
        assert result.item(0) == 0.0
        assert result.item(1) == 0.0
        # Scientific notation should parse correctly
        assert result.item(2) == pytest.approx(1e-10)
        assert result.item(3) == pytest.approx(1.5e20, rel=1e-4)
        assert result.item(4) == pytest.approx(1.0)
        assert result.item(5) == pytest.approx(-1e-5)
        # "inf" and "NaN" should be parsed as special float values
        assert result.item(6) == float("inf")  # "inf"
        assert result.item(7) is None  # "NaN" - becomes null
        assert result.item(8) is None  # "nan" - becomes null

    def test_clean_integer_scientific_notation(self, scientific_notation_df):
        """Test integer cleaning with scientific notation."""
        ref = z.ref("value")
        clean = z.Clean.integer()
        expr = clean.apply(ref)

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

        # Scientific notation should be parsed and rounded to integers
        assert result.item(0) == 0  # 1e-5 rounds to 0
        assert result.item(1) == 15000000000  # 1.5E+10
        assert result.item(2) == 1  # 1e+0
        assert result.item(3) == 0  # -1e-5 rounds to 0
        assert result.item(4) == 0  # 2.5e-3 rounds to 0
        assert result.item(5) == 123  # 1.23e+2

    def test_clean_decimal_scientific_notation(self, scientific_notation_df):
        """Test decimal cleaning with scientific notation."""
        ref = z.ref("value")
        clean = z.Clean.decimal()
        expr = clean.apply(ref)

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

        # Scientific notation should be parsed correctly with decimal precision
        # Some values may be too small/large for decimal precision and become null
        if result.item(0) is not None:
            assert float(result.item(0)) == pytest.approx(1e-5)
        if result.item(1) is not None:
            assert float(result.item(1)) == pytest.approx(1.5e10, rel=1e-4)
        if result.item(2) is not None:
            assert float(result.item(2)) == pytest.approx(1.0)
        if result.item(3) is not None:
            assert float(result.item(3)) == pytest.approx(-1e-5)
        if result.item(4) is not None:
            assert float(result.item(4)) == pytest.approx(2.5e-3)
        if result.item(5) is not None:
            assert float(result.item(5)) == pytest.approx(1.23e2)

    def test_clean_number_whitespace_variations(self, whitespace_df):
        """Test number cleaning with whitespace edge cases."""
        ref = z.ref("value")
        clean = z.Clean.number()
        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 number
        assert result.item(6) is None  # "\ttest\t" - not a number
        assert result.item(7) is None  # "\ntest\n" - not a number
