"""Unit tests for string cleaning variants (Clean.string, Clean.sanitised_string)."""

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


class TestCleanString:
    """Tests for Clean.string with various sanitisation levels."""

    def test_clean_string_basic_no_sanitise(self):
        """Test basic string cleaning instantiation and apply without sanitisation."""
        ref = z.ref("value")
        clean = z.Clean.string()
        expr = clean.apply(ref)

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

        assert result.to_list() == ["Hello", "World", "Test"]
        assert result.dtype == pl.String

    def test_clean_string_all_sanitise_levels(self):
        """Test string cleaning with all sanitisation levels and edge cases."""
        ref = z.ref("value")

        # Test trim
        clean_trim = z.Clean.string(sanitise="trim")
        df = pl.DataFrame({"value": ["  Hello  ", " World ", "Test"]})
        result = (
            df.with_columns(clean_trim.apply(ref)).select(ref.clean().col).to_series()
        )
        assert result.to_list() == ["Hello", "World", "Test"]

        # Test lowercase
        clean_lower = z.Clean.string(sanitise="lowercase")
        df = pl.DataFrame({"value": ["  HELLO  ", "WoRlD", " TEST "]})
        result = (
            df.with_columns(clean_lower.apply(ref)).select(ref.clean().col).to_series()
        )
        assert result.to_list() == ["hello", "world", "test"]

        # Test full sanitise
        clean_full = z.Clean.string(sanitise="full")
        df = pl.DataFrame({"value": ["  Hello World  ", "Test-Case", "māori", None]})
        result = (
            df.with_columns(clean_full.apply(ref)).select(ref.clean().col).to_series()
        )
        assert result.to_list() == ["hello_world", "test_case", "maori", None]

    def test_clean_string_invalid_sanitise_level_raises_error(self):
        """Test that invalid sanitise level raises CleanConfigurationError."""
        with pytest.raises(CleanConfigurationError) as exc_info:
            z.Clean.string(sanitise="invalid")

        assert "sanitise must be one of" in str(exc_info.value)

    def test_clean_string_mixed_input_types(self, mixed_type_df):
        """Test string cleaning with mixed input data types."""
        ref = z.ref("mixed_col")
        clean = z.Clean.string(sanitise=None)
        expr = clean.apply(ref)

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

        # Mixed types should be converted to strings
        assert result.to_list() == ["123", "456", None, "abc", "7.89", "", "   "]

    def test_clean_string_empty_vs_null(self, edge_case_strings):
        """Test string cleaning with empty strings vs null handling."""
        ref = z.ref("value")
        clean = z.Clean.string(sanitise=None)
        expr = clean.apply(ref)

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

        # Empty strings should be preserved, whitespace preserved, null-like strings preserved
        assert result.item(0) == ""  # ""
        assert result.item(1) == "   "  # "   "
        assert result.item(2) == "\t"  # "\t"
        assert result.item(3) == "\n"  # "\n"
        assert result.item(4) == "\t\n"  # "\t\n"
        assert result.item(5) == "null"  # "null"
        assert result.item(6) == "NULL"  # "NULL"
        assert result.item(7) == "None"  # "None"
        assert result.item(8) == "none"  # "none"

    def test_clean_string_control_characters(self):
        """Test string cleaning with control characters."""
        ref = z.ref("value")
        clean = z.Clean.string(sanitise="full")
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {"value": ["hello\x00world", "test\x01data", "line\nbreak", "tab\there"]}
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Control characters should be replaced with underscores in full sanitise
        assert result.to_list() == [
            "hello_world",
            "test_data",
            "line_break",
            "tab_here",
        ]

    def test_clean_string_unicode_precomposed_characters(self):
        """Test string cleaning with precomposed Unicode characters.

        Note: This implementation handles precomposed accented characters (e.g., 'é', 'ü')
        but does not handle decomposed combining characters (e.g., 'e\u0301').
        For full Unicode normalization support, use unicodedata.normalize() preprocessing.
        """
        ref = z.ref("value")
        clean = z.Clean.string(sanitise="full")
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "café",
                    "Müller",
                    "façade",
                    "naïve",
                    "Łódź",
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Precomposed accented characters should be normalized to ASCII equivalents
        assert result.to_list() == [
            "cafe",
            "muller",
            "facade",
            "naive",
            "lodz",
        ]

    def test_clean_string_whitespace_variations(self, whitespace_df):
        """Test string cleaning with various whitespace edge cases."""
        ref = z.ref("value")
        clean = z.Clean.string(sanitise="trim")
        expr = clean.apply(ref)

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

        # Trim should remove leading/trailing whitespace but preserve internal
        assert result.item(0) == ""  # "" -> ""
        assert result.item(1) == ""  # "   " -> ""
        assert result.item(2) == ""  # "\t" -> ""
        assert result.item(3) == ""  # "\n" -> ""
        assert result.item(4) == ""  # "\t\n" -> ""
        assert result.item(5) == "test"  # "  test  " -> "test"
        assert result.item(6) == "test"  # "\ttest\t" -> "test"
        assert result.item(7) == "test"  # "\ntest\n" -> "test"


class TestCleanSanitisedString:
    """Tests for Clean.sanitised_string with full sanitisation."""

    def test_clean_sanitised_string_basic(self):
        """Test CleanSanitisedStringColumn instantiation and apply."""
        ref = z.ref("value")
        clean = z.Clean.sanitised_string(sanitise="full")
        expr = clean.apply(ref)

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

        assert result.to_list() == ["hello_world", "test_case"]

    def test_clean_sanitised_string_handles_all_cases(self):
        """Test full sanitise with special chars, unicode, underscores, and nulls."""
        ref = z.ref("value")
        clean = z.Clean.sanitised_string(sanitise="full")
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "hello@world",  # Special chars
                    "test___case",  # Multiple underscores
                    "_hello_",  # Leading/trailing underscores
                    "māori",  # Unicode macrons
                    None,  # Null
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.to_list() == ["hello_world", "test_case", "hello", "maori", None]

    def test_clean_sanitised_string_custom_join_char(self):
        """Test full sanitise with custom join character."""
        ref = z.ref("value")
        clean = z.Clean.sanitised_string(sanitise="full", sanitise_join_char="-")
        expr = clean.apply(ref)

        df = pl.DataFrame({"value": ["hello world", "test case"]})
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        assert result.to_list() == ["hello-world", "test-case"]

    def test_clean_sanitised_string_invalid_sanitise_level_raises_error(self):
        """Test that CleanSanitisedStringColumn with invalid level raises error."""
        with pytest.raises(CleanConfigurationError) as exc_info:
            z.Clean.sanitised_string(sanitise="invalid")

        assert "sanitise must be one of" in str(exc_info.value)

    def test_clean_sanitised_string_special_characters(self):
        """Test sanitised string with special character edge cases."""
        ref = z.ref("value")
        clean = z.Clean.sanitised_string(sanitise="full")
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "hello@world",
                    "test#case",
                    "item$list",
                    "name%value",
                    "file&data",
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # Special characters should be replaced with underscores
        assert result.to_list() == [
            "hello_world",
            "test_case",
            "item_list",
            "name_value",
            "file_data",
        ]

    def test_clean_sanitised_string_extended_accents(self):
        """Test sanitised string with extended European accented characters."""
        ref = z.ref("value")
        clean = z.Clean.sanitised_string(sanitise="full")
        expr = clean.apply(ref)

        df = pl.DataFrame(
            {
                "value": [
                    "café",  # French - é
                    "façade",  # French - ç
                    "niño",  # Spanish - ñ
                    "Zürich",  # German - ü
                    "Łódź",  # Polish - ł, ó, ź
                    "København",  # Danish - ø
                    "São Paulo",  # Portuguese - ã
                    "Václav",  # Czech - á
                ]
            }
        )
        result = df.with_columns(expr).select(ref.clean().col).to_series()

        # All accented characters should be replaced with ASCII equivalents
        assert result.to_list() == [
            "cafe",
            "facade",
            "nino",
            "zurich",
            "lodz",
            "kobenhavn",
            "sao_paulo",
            "vaclav",
        ]
