"""Unit tests for enum cleaning variant (Clean.enum)."""

import polars as pl
import pytest
import zeolite as z
from zeolite.exceptions import CleanConfigurationError


class TestCleanEnum:
    """Tests for Clean.enum with mapping and sanitisation."""

    def test_clean_enum_basic(self):
        """Test basic enum cleaning instantiation and apply."""
        ref = z.ref("value")
        clean = z.Clean.enum(enum_map={"m": "Male", "f": "Female"})
        expr = clean.apply(ref)

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

        assert result.to_list() == ["Male", "Female", "Male"]
        assert isinstance(result.dtype, pl.Enum)

    def test_clean_enum_handles_all_cases(self):
        """Test enum cleaning with sanitisation, nulls, invalid values, and edge cases."""
        ref = z.ref("value")
        clean = z.Clean.enum(
            enum_map={"male": "Male", "female": "Female"}, sanitise="full"
        )
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "male",  # Exact match
                    "MALE",  # Case variation (sanitised)
                    " Female ",  # Whitespace (sanitised)
                    "Fe-male",  # Hyphen (sanitised to "fe_male")
                    None,  # Null -> __NO_DATA__
                    "unknown",  # Invalid -> __INVALID__
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.to_list() == [
            "Male",
            "Male",
            "Female",
            z.INVALID_DATA,
            z.NO_DATA,
            z.INVALID_DATA,
        ]

    def test_clean_enum_custom_null_and_invalid_values(self):
        """Test enum cleaning with custom null_value and invalid_value."""
        ref = z.ref("value")
        clean = z.Clean.enum(
            enum_map={"m": "Male", "f": "Female"},
            null_value="Not Specified",
            invalid_value="Other",
        )
        expr = clean.apply(ref)

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

        assert result.to_list() == ["Male", "Not Specified", "Other"]

    def test_clean_enum_requires_dict(self):
        """Test that enum_map must be a dictionary."""
        with pytest.raises(CleanConfigurationError) as exc_info:
            z.Clean.enum(enum_map=["m", "f"])

        assert "enum_map must be a dictionary" in str(exc_info.value)

    def test_clean_enum_duplicate_sanitised_keys_error(self):
        """Test that duplicate sanitised keys raise an error."""
        with pytest.raises(CleanConfigurationError) as exc_info:
            z.Clean.enum(enum_map={"M": "Male", "m": "Female"}, sanitise="lowercase")

        assert "Duplicate sanitised keys" in str(exc_info.value)

    def test_clean_enum_empty_map(self):
        """Test enum cleaning with empty enum map."""
        ref = z.ref("value")
        clean = z.Clean.enum(enum_map={})
        expr = clean.apply(ref)

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

        # All values should become null/invalid since map is empty
        assert all(val in [z.INVALID_DATA, z.NO_DATA] for val in result.to_list())

    def test_clean_enum_special_characters(self):
        """Test enum cleaning with special characters in values."""
        ref = z.ref("value")
        clean = z.Clean.enum(
            enum_map={
                "test_value": "Test",
                "another\nvalue": "Another",
                "tab\tvalue": "Tab",
            },
            sanitise="full",
        )
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {"value": ["test_value", "another\nvalue", "tab\tvalue", "unknown"]}
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Special characters should be handled by sanitisation
        assert result.to_list() == ["Test", "Another", "Tab", z.INVALID_DATA]

    def test_clean_enum_case_variations(self):
        """Test enum cleaning with case sensitivity edge cases."""
        ref = z.ref("value")
        clean = z.Clean.enum(
            enum_map={"male": "M", "female": "F"}, sanitise="lowercase"
        )
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": ["Male", "FEMALE", "Female", "MALE", "unknown"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Case variations should be handled by lowercase sanitisation
        assert result.to_list() == ["M", "F", "F", "M", z.INVALID_DATA]

    def test_clean_enum_whitespace_variations(self, whitespace_df):
        """Test enum cleaning with whitespace edge cases."""
        ref = z.ref("value")
        clean = z.Clean.enum(
            enum_map={"test": "Test", "value": "Value"}, sanitise="trim"
        )
        expr = clean.apply(ref)

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

        # Empty strings and whitespace should become INVALID_DATA
        assert result.item(0) == z.INVALID_DATA  # ""
        assert result.item(1) == z.INVALID_DATA  # "   "
        assert result.item(2) == z.INVALID_DATA  # "\t"
        assert result.item(3) == z.INVALID_DATA  # "\n"
        assert result.item(4) == z.INVALID_DATA  # "\t\n"
        # Test values with whitespace should be trimmed and processed
        assert result.item(5) == "Test"  # "  test  " -> "test" -> "Test"
        assert result.item(6) == "Test"  # "\ttest\t" -> "test" -> "Test"
        assert result.item(7) == "Test"  # "\ntest\n" -> "test" -> "Test"

    def test_clean_enum_null_string_variations(self, edge_case_strings):
        """Test enum cleaning with various null string representations."""
        ref = z.ref("value")
        clean = z.Clean.enum(enum_map={"male": "M", "female": "F"})
        expr = clean.apply(ref)

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

        # Null-like strings should be treated as invalid values
        assert result.item(0) == z.INVALID_DATA  # ""
        assert result.item(1) == z.INVALID_DATA  # "   "
        assert result.item(2) == z.INVALID_DATA  # "\t"
        assert result.item(3) == z.INVALID_DATA  # "\n"
        assert result.item(4) == z.INVALID_DATA  # "\t\n"
        assert result.item(5) == z.INVALID_DATA  # "null"
        assert result.item(6) == z.INVALID_DATA  # "NULL"
        assert result.item(7) == z.INVALID_DATA  # "None"
        assert result.item(8) == z.INVALID_DATA  # "none"

    def test_clean_enum_mixed_input_types(self, mixed_type_df):
        """Test enum cleaning with mixed input data types."""
        ref = z.ref("string_col")
        clean = z.Clean.enum(
            enum_map={
                "yes": "Yes",
                "no": "No",
                "true": "Yes",
                "false": "No",
                "1": "Yes",
                "0": "No",
            }
        )
        expr = clean.apply(ref)

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

        # Mixed types should be converted to strings and processed
        assert result.to_list() == ["Yes", "Yes", "Yes", z.NO_DATA, "No", "No", "Yes"]
