"""Cross-language dead-letter round-trip test (RESIL-02).

Verifies Rust-written Parquet dead-letter files are readable by Python
polars.read_parquet() with correct column names, types, and nullable handling.

The Rust test `test_write_roundtrip_artifact` in dead_letter.rs writes a known
Parquet file to a temp directory. This test reads that file and validates the
schema matches CORE_COLUMNS exactly.
"""

from __future__ import annotations

import subprocess
import tempfile
from pathlib import Path

import polars as pl
import pytest

# Expected column names in exact order -- must match CORE_COLUMNS in row.rs
CORE_COLUMNS = [
    "symbol",
    "threshold_decimal_bps",
    "close_time_us",
    "open_time_us",
    "open",
    "high",
    "low",
    "close",
    "volume",
    "vwap",
    "buy_volume",
    "sell_volume",
    "individual_trade_count",
    "agg_record_count",
    "duration_us",
    "ofi",
    "vwap_close_deviation",
    "price_impact",
    "kyle_lambda_proxy",
    "trade_intensity",
    "volume_per_trade",
    "aggression_ratio",
    "aggregation_density",
    "turnover_imbalance",
    "ouroboros_mode",
    "exchange_session_sydney",
    "exchange_session_tokyo",
    "exchange_session_london",
    "exchange_session_newyork",
    "lookback_trade_count",
    "lookback_ofi",
    "lookback_duration_us",
    "lookback_intensity",
    "lookback_vwap_raw",
    "lookback_vwap_position",
    "lookback_count_imbalance",
    "lookback_kyle_lambda",
    "lookback_burstiness",
    "lookback_volume_skew",
    "lookback_volume_kurt",
    "lookback_price_range",
    "lookback_kaufman_er",
    "lookback_garman_klass_vol",
    "lookback_hurst",
    "lookback_permutation_entropy",
    "intra_bull_epoch_density",
    "intra_bear_epoch_density",
    "intra_bull_excess_gain",
    "intra_bear_excess_gain",
    "intra_bull_cv",
    "intra_bear_cv",
    "intra_max_drawdown",
    "intra_max_runup",
    "intra_trade_count",
    "intra_ofi",
    "intra_duration_us",
    "intra_intensity",
    "intra_vwap_position",
    "intra_count_imbalance",
    "intra_kyle_lambda",
    "intra_burstiness",
    "intra_volume_skew",
    "intra_volume_kurt",
    "intra_kaufman_er",
    "intra_garman_klass_vol",
    "intra_hurst",
    "intra_permutation_entropy",
    "has_gap",
    "gap_trade_count",
    "max_gap_duration_us",
    "is_exchange_gap",
    "first_agg_trade_id",
    "last_agg_trade_id",
    "is_orphan",
    "cache_key",
    "opendeviationbar_version",
    "source_start_ts",
    "source_end_ts",
]

ARTIFACT_DIR = Path(tempfile.gettempdir()) / "opendeviationbar-dead-letter-roundtrip"
ARTIFACT_FILE = ARTIFACT_DIR / "BTCUSDT_250_9999999999.parquet"


@pytest.fixture(scope="module", autouse=True)
def generate_rust_artifact():
    """Run the Rust test to generate the Parquet artifact before Python tests."""
    env = {"MISE_DISABLED": "1"}
    try:
        subprocess.run(
            [
                "cargo",
                "nextest",
                "run",
                "-p",
                "opendeviationbar-streaming",
                "--features",
                "clickhouse-sink",
                "-E",
                "test(roundtrip_artifact)",
            ],
            capture_output=True,
            text=True,
            check=True,
            env={**dict(__import__("os").environ), **env},
            timeout=120,
        )
    except subprocess.CalledProcessError as e:
        pytest.fail(
            f"Rust artifact generation failed:\n"
            f"stdout: {e.stdout}\n"
            f"stderr: {e.stderr}"
        )
    assert ARTIFACT_FILE.exists(), f"Artifact not found at {ARTIFACT_FILE}"


def test_parquet_readable_by_polars():
    """Rust-written dead-letter Parquet readable by polars.read_parquet."""
    df = pl.read_parquet(ARTIFACT_FILE)
    assert len(df) == 1, f"Expected 1 row, got {len(df)}"


def test_column_names_match_core_columns():
    """Column names exactly match CORE_COLUMNS (no renaming, no missing, no extra)."""
    df = pl.read_parquet(ARTIFACT_FILE)
    assert df.columns == CORE_COLUMNS, (
        f"Column mismatch.\n"
        f"Expected {len(CORE_COLUMNS)} columns: {CORE_COLUMNS}\n"
        f"Got {len(df.columns)} columns: {df.columns}"
    )


def test_column_count():
    """All 78 columns present (matches CORE_COLUMNS in row.rs)."""
    df = pl.read_parquet(ARTIFACT_FILE)
    assert len(df.columns) == len(CORE_COLUMNS) == 78, (
        f"Expected 78 columns, got {len(df.columns)}"
    )


def test_nullable_columns_handle_null():
    """Nullable columns (lookback_*, intra_*) correctly represent null as Python None."""
    df = pl.read_parquet(ARTIFACT_FILE)

    # The Rust test sets these to None
    assert df["lookback_duration_us"][0] is None
    assert df["lookback_intensity"][0] is None
    assert df["intra_bull_epoch_density"][0] is None
    assert df["intra_hurst"][0] is None


def test_nullable_columns_with_values():
    """Nullable columns that are set have correct values."""
    df = pl.read_parquet(ARTIFACT_FILE)

    # The Rust test sets these to specific values
    assert df["lookback_trade_count"][0] == 500
    assert abs(df["lookback_ofi"][0] - (-0.42)) < 1e-10


def test_non_nullable_field_types():
    """Non-nullable numeric fields have correct Polars types."""
    df = pl.read_parquet(ARTIFACT_FILE)

    type_checks = {
        "symbol": pl.Utf8,
        "threshold_decimal_bps": pl.UInt32,
        "close_time_us": pl.Int64,
        "open_time_us": pl.Int64,
        "open": pl.Float64,
        "high": pl.Float64,
        "low": pl.Float64,
        "close": pl.Float64,
        "volume": pl.Float64,
        "has_gap": pl.Boolean,
        "first_agg_trade_id": pl.Int64,
        "last_agg_trade_id": pl.Int64,
        "exchange_session_sydney": pl.UInt8,
        "is_orphan": pl.UInt8,
        "individual_trade_count": pl.UInt32,
        "ouroboros_mode": pl.Utf8,
    }

    for col_name, expected_type in type_checks.items():
        actual_type = df[col_name].dtype
        assert actual_type == expected_type, (
            f"Column {col_name}: expected {expected_type}, got {actual_type}"
        )


def test_non_nullable_columns_have_values():
    """Non-nullable columns have no nulls."""
    df = pl.read_parquet(ARTIFACT_FILE)

    non_nullable = [
        "symbol",
        "threshold_decimal_bps",
        "open",
        "high",
        "low",
        "close",
        "volume",
        "has_gap",
        "first_agg_trade_id",
        "last_agg_trade_id",
    ]

    for col_name in non_nullable:
        null_count = df[col_name].null_count()
        assert null_count == 0, f"Column {col_name} has {null_count} nulls"


def test_known_field_values():
    """Verify specific field values match the Rust test data."""
    df = pl.read_parquet(ARTIFACT_FILE)

    assert df["symbol"][0] == "BTCUSDT"
    assert df["threshold_decimal_bps"][0] == 250
    assert df["first_agg_trade_id"][0] == 1000
    assert df["last_agg_trade_id"][0] == 1099
    assert df["ouroboros_mode"][0] == "aion"
    assert df["exchange_session_sydney"][0] == 0
    assert df["is_orphan"][0] == 0


def test_filename_pattern_parseable():
    """Filename follows {symbol}_{threshold}_{timestamp}.parquet pattern."""
    stem = ARTIFACT_FILE.stem  # "BTCUSDT_250_9999999999"
    parts = stem.split("_")

    assert len(parts) >= 3, f"Expected >=3 parts, got {len(parts)} from '{stem}'"
    assert parts[0] == "BTCUSDT"
    assert parts[1] == "250"
    # Third part should be a numeric timestamp
    assert parts[2].isdigit(), f"Third part '{parts[2]}' is not numeric"
