import pandas as pd
import pytest

from xfintech.serde.common.dataformat import DataFormat
from xfintech.serde.common.error import (
    DeserialiserFailedError,
    DeserialiserInputTypeError,
)
from xfintech.serde.deserialiser.pandas import PandasDeserialiser
from xfintech.serde.serialiser.pandas import PandasSerialiser

# ==================== Class Structure Tests ====================


def test_pandasdeserialiser_has_deserialise_method():
    """Test PandasDeserialiser has deserialise method"""
    assert hasattr(PandasDeserialiser, "deserialise")
    assert callable(PandasDeserialiser.deserialise)


def test_pandasdeserialiser_has_from_parquet_method():
    """Test PandasDeserialiser has from_parquet method"""
    assert hasattr(PandasDeserialiser, "from_parquet")
    assert callable(PandasDeserialiser.from_parquet)


def test_pandasdeserialiser_has_from_csv_method():
    """Test PandasDeserialiser has from_csv method"""
    assert hasattr(PandasDeserialiser, "from_csv")
    assert callable(PandasDeserialiser.from_csv)


def test_pandasdeserialiser_has_from_json_method():
    """Test PandasDeserialiser has from_json method"""
    assert hasattr(PandasDeserialiser, "from_json")
    assert callable(PandasDeserialiser.from_json)


def test_pandasdeserialiser_methods_are_static():
    """Test all methods are static methods"""
    assert isinstance(PandasDeserialiser.__dict__["deserialise"], staticmethod)
    assert isinstance(PandasDeserialiser.__dict__["from_parquet"], staticmethod)
    assert isinstance(PandasDeserialiser.__dict__["from_csv"], staticmethod)
    assert isinstance(PandasDeserialiser.__dict__["from_json"], staticmethod)


# ==================== deserialise() Method Tests ====================


def test_deserialise_with_parquet_format_enum():
    """Test deserialise with PARQUET DataFormat enum"""
    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.deserialise(bytes_data, DataFormat.PARQUET)
    assert isinstance(result, pd.DataFrame)
    assert result.equals(df)


def test_deserialise_with_parquet_format_string():
    """Test deserialise with 'parquet' string"""
    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.deserialise(bytes_data, "parquet")
    assert isinstance(result, pd.DataFrame)
    assert result.equals(df)


def test_deserialise_with_csv_format_enum():
    """Test deserialise with CSV DataFormat enum"""
    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.to_csv(df, index=False)
    result = PandasDeserialiser.deserialise(bytes_data, DataFormat.CSV)
    assert isinstance(result, pd.DataFrame)
    assert list(result.columns) == ["a", "b"]


def test_deserialise_with_csv_format_string():
    """Test deserialise with 'csv' string"""
    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.to_csv(df, index=False)
    result = PandasDeserialiser.deserialise(bytes_data, "csv")
    assert isinstance(result, pd.DataFrame)
    assert list(result.columns) == ["a", "b"]


def test_deserialise_with_json_format_enum():
    """Test deserialise with JSON DataFormat enum"""
    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.to_json(df)
    result = PandasDeserialiser.deserialise(bytes_data, DataFormat.JSON)
    assert isinstance(result, pd.DataFrame)
    assert list(result.columns) == ["a", "b"]


def test_deserialise_with_json_format_string():
    """Test deserialise with 'json' string"""
    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.to_json(df)
    result = PandasDeserialiser.deserialise(bytes_data, "json")
    assert isinstance(result, pd.DataFrame)
    assert list(result.columns) == ["a", "b"]


def test_deserialise_with_uppercase_format_string():
    """Test deserialise with uppercase format string"""
    df = pd.DataFrame({"a": [1, 2]})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.deserialise(bytes_data, "PARQUET")
    assert isinstance(result, pd.DataFrame)


def test_deserialise_with_mixed_case_format_string():
    """Test deserialise with mixed case format string"""
    df = pd.DataFrame({"a": [1, 2]})
    bytes_data = PandasSerialiser.to_csv(df, index=False)
    result = PandasDeserialiser.deserialise(bytes_data, "CsV")
    assert isinstance(result, pd.DataFrame)


def test_deserialise_raises_on_non_bytes():
    """Test deserialise raises error for non-bytes input"""
    with pytest.raises(DeserialiserInputTypeError) as exc_info:
        PandasDeserialiser.deserialise([1, 2, 3], DataFormat.PARQUET)
    assert "expects bytes" in str(exc_info.value)


def test_deserialise_raises_on_string_input():
    """Test deserialise raises error for string input"""
    with pytest.raises(DeserialiserInputTypeError):
        PandasDeserialiser.deserialise("not bytes", DataFormat.CSV)


def test_deserialise_raises_on_dict_input():
    """Test deserialise raises error for dict input"""
    with pytest.raises(DeserialiserInputTypeError):
        PandasDeserialiser.deserialise({"a": [1, 2]}, DataFormat.JSON)


def test_deserialise_raises_on_none_input():
    """Test deserialise raises error for None input"""
    with pytest.raises(DeserialiserInputTypeError):
        PandasDeserialiser.deserialise(None, DataFormat.PARQUET)


def test_deserialise_raises_on_unsupported_format():
    """Test deserialise raises error for unsupported format"""
    with pytest.raises(ValueError) as exc_info:
        PandasDeserialiser.deserialise(b"data", "xml")
    assert "Unknown DataFormat" in str(exc_info.value)


def test_deserialise_raises_on_invalid_data():
    """Test deserialise raises error for invalid data"""
    with pytest.raises(DeserialiserFailedError):
        PandasDeserialiser.deserialise(b"invalid data", DataFormat.PARQUET)


def test_deserialise_empty_csv():
    """Test deserialise with empty CSV raises error"""
    bytes_data = b""
    with pytest.raises(DeserialiserFailedError):
        PandasDeserialiser.deserialise(bytes_data, DataFormat.CSV)


# ==================== from_parquet() Method Tests ====================


def test_from_parquet_basic():
    """Test from_parquet with basic DataFrame"""
    df = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.from_parquet(bytes_data)
    assert isinstance(result, pd.DataFrame)
    assert result.equals(df)


def test_from_parquet_returns_dataframe():
    """Test from_parquet returns pandas DataFrame"""
    df = pd.DataFrame({"col1": [1, 2, 3], "col2": [4.5, 5.6, 6.7]})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.from_parquet(bytes_data)
    assert type(result).__name__ == "DataFrame"


def test_from_parquet_preserves_data_types():
    """Test from_parquet preserves data types"""
    df = pd.DataFrame({"int": [1, 2, 3], "float": [1.1, 2.2, 3.3], "str": ["a", "b", "c"]})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.from_parquet(bytes_data)
    assert result["int"].dtype == df["int"].dtype
    assert result["float"].dtype == df["float"].dtype


def test_from_parquet_with_datetime_column():
    """Test from_parquet with datetime column"""
    df = pd.DataFrame({"date": pd.date_range("2024-01-01", periods=3)})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.from_parquet(bytes_data)
    assert isinstance(result, pd.DataFrame)
    assert len(result) == 3


def test_from_parquet_with_mixed_types():
    """Test from_parquet with mixed data types"""
    df = pd.DataFrame({"int": [1, 2, 3], "float": [1.1, 2.2, 3.3], "str": ["a", "b", "c"], "bool": [True, False, True]})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.from_parquet(bytes_data)
    assert result.equals(df)


def test_from_parquet_raises_on_invalid_bytes():
    """Test from_parquet raises error for invalid parquet bytes"""
    with pytest.raises(DeserialiserFailedError):
        PandasDeserialiser.from_parquet(b"not valid parquet")


def test_from_parquet_large_dataframe():
    """Test from_parquet with large DataFrame"""
    df = pd.DataFrame({"col": range(10000)})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.from_parquet(bytes_data)
    assert len(result) == 10000


def test_from_parquet_multiindex():
    """Test from_parquet with MultiIndex DataFrame"""
    arrays = [["A", "A", "B", "B"], [1, 2, 1, 2]]
    index = pd.MultiIndex.from_arrays(arrays, names=["letter", "number"])
    df = pd.DataFrame({"value": [10, 20, 30, 40]}, index=index)
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.from_parquet(bytes_data)
    assert isinstance(result, pd.DataFrame)


# ==================== from_csv() Method Tests ====================


def test_from_csv_basic():
    """Test from_csv with basic DataFrame"""
    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.to_csv(df, index=False)
    result = PandasDeserialiser.from_csv(bytes_data)
    assert isinstance(result, pd.DataFrame)
    assert list(result.columns) == ["a", "b"]


def test_from_csv_returns_dataframe():
    """Test from_csv returns pandas DataFrame"""
    df = pd.DataFrame({"col1": [1, 2, 3], "col2": [4, 5, 6]})
    bytes_data = PandasSerialiser.to_csv(df, index=False)
    result = PandasDeserialiser.from_csv(bytes_data)
    assert type(result).__name__ == "DataFrame"


def test_from_csv_with_custom_encoding():
    """Test from_csv with custom encoding"""
    df = pd.DataFrame({"a": ["中文", "日本語"]})
    bytes_data = PandasSerialiser.to_csv(df, index=False, encoding="utf-8")
    result = PandasDeserialiser.from_csv(bytes_data, encoding="utf-8")
    assert isinstance(result, pd.DataFrame)


def test_from_csv_empty():
    """Test from_csv with empty CSV raises error"""
    bytes_data = b""
    with pytest.raises(DeserialiserFailedError):
        PandasDeserialiser.from_csv(bytes_data)


def test_from_csv_with_na_values():
    """Test from_csv with NA values"""
    df = pd.DataFrame({"a": [1, None, 3], "b": [4, 5, None]})
    bytes_data = PandasSerialiser.to_csv(df, index=False)
    result = PandasDeserialiser.from_csv(bytes_data)
    assert isinstance(result, pd.DataFrame)
    assert result.isna().sum().sum() >= 2  # At least 2 NA values


def test_from_csv_raises_on_invalid_bytes():
    """Test from_csv raises error for invalid CSV bytes"""
    with pytest.raises(DeserialiserFailedError):
        PandasDeserialiser.from_csv(b"\xff\xfe")  # Invalid UTF-8


def test_from_csv_with_special_characters():
    """Test from_csv with special characters"""
    df = pd.DataFrame({"col": ["test,with,comma", "test\nwith\nnewline"]})
    bytes_data = PandasSerialiser.to_csv(df, index=False)
    result = PandasDeserialiser.from_csv(bytes_data)
    assert isinstance(result, pd.DataFrame)


def test_from_csv_single_column():
    """Test from_csv with single column"""
    df = pd.DataFrame({"x": [1, 2, 3]})
    bytes_data = PandasSerialiser.to_csv(df, index=False)
    result = PandasDeserialiser.from_csv(bytes_data)
    assert list(result.columns) == ["x"]


# ==================== from_json() Method Tests ====================


def test_from_json_basic():
    """Test from_json with basic DataFrame"""
    df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.to_json(df)
    result = PandasDeserialiser.from_json(bytes_data)
    assert isinstance(result, pd.DataFrame)
    assert list(result.columns) == ["a", "b"]


def test_from_json_returns_dataframe():
    """Test from_json returns pandas DataFrame"""
    df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]})
    bytes_data = PandasSerialiser.to_json(df)
    result = PandasDeserialiser.from_json(bytes_data)
    assert type(result).__name__ == "DataFrame"


def test_from_json_with_orient_records():
    """Test from_json with orient='records'"""
    df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
    bytes_data = PandasSerialiser.to_json(df, orient="records")
    result = PandasDeserialiser.from_json(bytes_data, orient="records")
    assert isinstance(result, pd.DataFrame)


def test_from_json_with_orient_split():
    """Test from_json with orient='split'"""
    df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
    bytes_data = PandasSerialiser.to_json(df, orient="split")
    result = PandasDeserialiser.from_json(bytes_data, orient="split")
    assert isinstance(result, pd.DataFrame)


def test_from_json_with_orient_index():
    """Test from_json with orient='index'"""
    df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
    bytes_data = PandasSerialiser.to_json(df, orient="index")
    result = PandasDeserialiser.from_json(bytes_data, orient="index")
    assert isinstance(result, pd.DataFrame)


def test_from_json_with_orient_columns():
    """Test from_json with orient='columns'"""
    df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
    bytes_data = PandasSerialiser.to_json(df, orient="columns")
    result = PandasDeserialiser.from_json(bytes_data, orient="columns")
    assert isinstance(result, pd.DataFrame)


def test_from_json_with_orient_values():
    """Test from_json with orient='values'"""
    df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
    bytes_data = PandasSerialiser.to_json(df, orient="values")
    result = PandasDeserialiser.from_json(bytes_data, orient="values")
    assert isinstance(result, pd.DataFrame)


def test_from_json_with_custom_encoding():
    """Test from_json with custom encoding"""
    df = pd.DataFrame({"a": [1, 2]})
    bytes_data = PandasSerialiser.to_json(df, encoding="utf-8")
    result = PandasDeserialiser.from_json(bytes_data, encoding="utf-8")
    assert isinstance(result, pd.DataFrame)


def test_from_json_empty():
    """Test from_json with empty JSON"""
    bytes_data = b"{}"
    result = PandasDeserialiser.from_json(bytes_data)
    assert isinstance(result, pd.DataFrame)


def test_from_json_raises_on_invalid_bytes():
    """Test from_json raises error for invalid JSON bytes"""
    with pytest.raises(DeserialiserFailedError):
        PandasDeserialiser.from_json(b"not valid json")


def test_from_json_with_unicode():
    """Test from_json with Unicode characters"""
    df = pd.DataFrame({"text": ["Hello 世界", "Привет"]})
    bytes_data = PandasSerialiser.to_json(df)
    result = PandasDeserialiser.from_json(bytes_data)
    assert isinstance(result, pd.DataFrame)


def test_from_json_with_lines_format():
    """Test from_json with lines=True (newline-delimited JSON)"""
    df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
    bytes_data = PandasSerialiser.to_json(df, orient="records", lines=True)
    result = PandasDeserialiser.from_json(bytes_data, orient="records", lines=True)
    assert isinstance(result, pd.DataFrame)
    assert len(result) == 2


# ==================== Roundtrip Tests ====================


def test_roundtrip_parquet():
    """Test serialise and deserialise roundtrip for PARQUET"""
    original = pd.DataFrame({"int": [1, 2, 3], "float": [1.1, 2.2, 3.3], "str": ["a", "b", "c"]})
    bytes_data = PandasSerialiser.serialise(original, DataFormat.PARQUET)
    result = PandasDeserialiser.deserialise(bytes_data, DataFormat.PARQUET)
    assert result.equals(original)


def test_roundtrip_csv():
    """Test serialise and deserialise roundtrip for CSV"""
    original = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.serialise(original, DataFormat.CSV, index=False)
    result = PandasDeserialiser.deserialise(bytes_data, DataFormat.CSV)
    assert list(result.columns) == list(original.columns)
    assert len(result) == len(original)


def test_roundtrip_json():
    """Test serialise and deserialise roundtrip for JSON"""
    original = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
    bytes_data = PandasSerialiser.serialise(original, DataFormat.JSON)
    result = PandasDeserialiser.deserialise(bytes_data, DataFormat.JSON)
    assert list(result.columns) == list(original.columns)
    assert len(result) == len(original)


def test_roundtrip_all_formats():
    """Test roundtrip for all supported formats"""
    original = pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})

    for format in [DataFormat.PARQUET, DataFormat.CSV, DataFormat.JSON]:
        if format == DataFormat.CSV:
            bytes_data = PandasSerialiser.serialise(original, format, index=False)
        else:
            bytes_data = PandasSerialiser.serialise(original, format)
        result = PandasDeserialiser.deserialise(bytes_data, format)
        assert isinstance(result, pd.DataFrame)
        assert len(result) == len(original)


def test_roundtrip_with_unicode():
    """Test roundtrip with Unicode data"""
    original = pd.DataFrame({"text": ["Hello 世界", "Привет", "مرحبا"]})

    # Test with CSV and JSON (parquet handles this automatically)
    for format in [DataFormat.CSV, DataFormat.JSON]:
        if format == DataFormat.CSV:
            bytes_data = PandasSerialiser.serialise(original, format, index=False)
        else:
            bytes_data = PandasSerialiser.serialise(original, format)
        result = PandasDeserialiser.deserialise(bytes_data, format)
        assert isinstance(result, pd.DataFrame)
        assert len(result) == len(original)


def test_roundtrip_large_dataframe():
    """Test roundtrip with large DataFrame"""
    original = pd.DataFrame({"col": range(1000)})
    bytes_data = PandasSerialiser.serialise(original, DataFormat.PARQUET)
    result = PandasDeserialiser.deserialise(bytes_data, DataFormat.PARQUET)
    assert len(result) == 1000


# ==================== Integration and Edge Case Tests ====================


def test_deserialise_all_formats_produce_dataframes():
    """Test all formats produce DataFrame output"""
    df = pd.DataFrame({"a": [1, 2, 3]})

    parquet_bytes = PandasSerialiser.serialise(df, DataFormat.PARQUET)
    csv_bytes = PandasSerialiser.serialise(df, DataFormat.CSV, index=False)
    json_bytes = PandasSerialiser.serialise(df, DataFormat.JSON)

    assert isinstance(PandasDeserialiser.deserialise(parquet_bytes, DataFormat.PARQUET), pd.DataFrame)
    assert isinstance(PandasDeserialiser.deserialise(csv_bytes, DataFormat.CSV), pd.DataFrame)
    assert isinstance(PandasDeserialiser.deserialise(json_bytes, DataFormat.JSON), pd.DataFrame)


def test_deserialise_preserves_column_order():
    """Test deserialise preserves column order"""
    df = pd.DataFrame({"z": [1, 2], "a": [3, 4], "m": [5, 6]})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.from_parquet(bytes_data)
    assert list(result.columns) == ["z", "a", "m"]


def test_deserialise_with_kwargs_passthrough():
    """Test that kwargs are passed through to underlying methods"""
    df = pd.DataFrame({"a": [1, 2, 3]})
    bytes_data = PandasSerialiser.to_csv(df, index=False)
    result = PandasDeserialiser.deserialise(bytes_data, DataFormat.CSV)
    assert isinstance(result, pd.DataFrame)


def test_dataframe_with_categorical_data():
    """Test deserialising DataFrame with categorical data"""
    df = pd.DataFrame({"cat": pd.Categorical(["a", "b", "c", "a"])})
    bytes_data = PandasSerialiser.to_parquet(df)
    result = PandasDeserialiser.from_parquet(bytes_data)
    assert isinstance(result, pd.DataFrame)
    assert len(result) == 4
