"""Oracle bit-exact regression tests for golden snapshot verification.

Issue #300: Captures the EXACT output of the current codebase BEFORE any
memory refactoring changes. All subsequent refactoring plans must prove
output identity against these golden fixtures.

Two modes:
- Generate: GENERATE_GOLDEN=1 pytest tests/test_oracle_regression.py -v -x
- Verify:   pytest tests/test_oracle_regression.py -v -x
"""

import os
from pathlib import Path

import polars as pl
import pyarrow as pa
import pytest
from opendeviationbar import OpenDeviationBarProcessor

FIXTURE_DIR = Path(__file__).parent / "fixtures"
GOLDEN_PARQUET = FIXTURE_DIR / "golden_bars_btcusdt_250_2024-01-01.parquet"
TRADES_CSV = FIXTURE_DIR / "BTCUSDT-aggTrades-2024-01-01.csv"

# Column names for the headerless 8-column Binance Vision CSV
CSV_COLUMNS = [
    "agg_trade_id",
    "price",
    "quantity",
    "first_trade_id",
    "last_trade_id",
    "timestamp",
    "is_buyer_maker",
    "is_best_match",
]

# Column dtypes for CSV parsing
CSV_SCHEMA = {
    "agg_trade_id": pl.Int64,
    "price": pl.Float64,
    "quantity": pl.Float64,
    "first_trade_id": pl.Int64,
    "last_trade_id": pl.Int64,
    "timestamp": pl.Int64,
    "is_buyer_maker": pl.Boolean,
    "is_best_match": pl.Boolean,
}

# Boolean string mapping for Binance Vision CSV (True/False strings)
BOOL_MAPPING = {"True": True, "False": False}


def _load_trades_as_arrow() -> pa.RecordBatch:
    """Load BTCUSDT 2024-01-01 CSV as Arrow RecordBatch for process_trades_arrow."""
    # Read CSV with string columns first (Binance Vision uses True/False strings)
    df = pl.read_csv(
        TRADES_CSV,
        has_header=False,
        new_columns=CSV_COLUMNS,
        schema_overrides={
            "agg_trade_id": pl.Int64,
            "price": pl.Float64,
            "quantity": pl.Float64,
            "first_trade_id": pl.Int64,
            "last_trade_id": pl.Int64,
            "timestamp": pl.Int64,
            "is_buyer_maker": pl.Utf8,
            "is_best_match": pl.Utf8,
        },
    )

    # Convert True/False strings to boolean (native expression, no map_elements)
    df = df.with_columns(
        (pl.col("is_buyer_maker") == "True").alias("is_buyer_maker"),
        (pl.col("is_best_match") == "True").alias("is_best_match"),
    )

    # Rename quantity -> volume to match Arrow import expectations
    df = df.rename({"quantity": "volume"})

    return df.to_arrow()


def _produce_golden_bars() -> pl.DataFrame:
    """Process all trades and return golden bars as Polars DataFrame.

    Uses the full Python pipeline through the PyO3 boundary:
    CSV -> Polars -> Arrow -> PyOpenDeviationBarProcessor -> Arrow -> Polars
    """
    batch = _load_trades_as_arrow()

    # Create processor with inter-bar and intra-bar features enabled
    processor = OpenDeviationBarProcessor(
        threshold_decimal_bps=250,
        inter_bar_lookback_count=500,
        include_intra_bar_features=True,
        compute_tier2=True,
        compute_tier3=True,
    )

    # Process via Arrow path
    result_batch = processor.process_trades_arrow(batch)
    completed_df = pl.from_arrow(result_batch)

    # Add is_orphan=False for completed bars
    completed_df = completed_df.with_columns(pl.lit(False).alias("is_orphan"))

    # Get orphan bar from reset_at_ouroboros
    orphan_dict = processor.reset_at_ouroboros()
    if orphan_dict is not None:
        # Convert orphan dict to single-row DataFrame
        # The dict has _ms suffixed timestamp keys from opendeviationbar_to_dict
        orphan_row = {}
        for col in completed_df.columns:
            if col == "is_orphan":
                orphan_row[col] = True
            elif col == "open_time":
                # Dict path uses open_time_us (microseconds), Arrow uses open_time (microseconds)
                orphan_row[col] = orphan_dict.get("open_time_us", 0)
            elif col == "close_time":
                orphan_row[col] = orphan_dict.get("close_time_us", 0)
            elif col == "data_source":
                orphan_row[col] = orphan_dict.get("data_source", "unknown")
            elif col in orphan_dict:
                orphan_row[col] = orphan_dict[col]
            # For columns not in dict (e.g., aggregation_density vs aggregation_density_f64)
            # Try common aliases
            elif col == "aggregation_density":
                orphan_row[col] = orphan_dict.get("aggregation_density_f64", 0.0)
            else:
                orphan_row[col] = None

        orphan_df = pl.DataFrame(
            {k: [v] for k, v in orphan_row.items()},
            schema=completed_df.schema,
        )
        result_df = pl.concat([completed_df, orphan_df])
    else:
        result_df = completed_df

    return result_df


@pytest.mark.slow
def test_oracle_bit_exact_regression():
    """Verify current output matches golden snapshot exactly.

    On first run (or with GENERATE_GOLDEN=1), generates the golden fixture.
    On subsequent runs, verifies bit-exact match against the fixture.
    """
    generate = os.environ.get("GENERATE_GOLDEN") == "1" or not GOLDEN_PARQUET.exists()

    if generate:
        print("Generating golden fixture...")
        golden_df = _produce_golden_bars()
        golden_df.write_parquet(GOLDEN_PARQUET, compression="zstd")
        print(
            f"Golden fixture written: {GOLDEN_PARQUET} "
            f"({len(golden_df)} bars, {golden_df.estimated_size()} bytes)"
        )

        # Sanity checks
        assert len(golden_df) > 0, "Must produce at least one bar"
        assert "is_orphan" in golden_df.columns, "Must have is_orphan column"
        orphan_count = golden_df.filter(pl.col("is_orphan")).height
        assert orphan_count >= 1, "Must have at least one orphan bar"
        assert "first_agg_trade_id" in golden_df.columns
        assert "last_agg_trade_id" in golden_df.columns
        assert golden_df["first_agg_trade_id"].min() > 0, "Trade IDs must be non-zero"

        if not os.environ.get("GENERATE_GOLDEN"):
            print("Golden fixture generated on first run. Re-running verification...")
        else:
            return  # GENERATE_GOLDEN=1 mode: just generate, don't verify

    # Load golden fixture
    golden_df = pl.read_parquet(GOLDEN_PARQUET)

    # Regenerate bars from current code
    actual_df = _produce_golden_bars()

    # Assert row counts match
    assert len(golden_df) == len(actual_df), (
        f"Row count mismatch: golden={len(golden_df)} actual={len(actual_df)}"
    )

    # Assert column sets match
    assert set(golden_df.columns) == set(actual_df.columns), (
        f"Column mismatch: golden-only={set(golden_df.columns) - set(actual_df.columns)}, "
        f"actual-only={set(actual_df.columns) - set(golden_df.columns)}"
    )

    # Verify each column bit-exactly
    for col in golden_df.columns:
        golden_col = golden_df[col]
        actual_col = actual_df[col]

        if golden_col.dtype in (pl.Float32, pl.Float64):
            # For float columns: fill NaN with None, then compare
            g = golden_col.fill_nan(None)
            a = actual_col.fill_nan(None)

            # Check nulls match
            g_null = g.is_null()
            a_null = a.is_null()
            assert g_null.equals(a_null), (
                f"Column '{col}': null pattern mismatch"
            )

            # Check non-null values are bit-exact (deterministic computation on same input)
            # Use relative tolerance of 1e-12 for f64 accumulation differences
            g_vals = g.drop_nulls()
            a_vals = a.drop_nulls()
            if len(g_vals) > 0:
                diff = (g_vals - a_vals).abs()
                max_abs = g_vals.abs().max()
                tol = max(max_abs * 1e-12, 1e-12) if max_abs > 0 else 1e-12
                max_diff = diff.max()
                assert max_diff < tol, (
                    f"Column '{col}': max diff={max_diff} exceeds tol={tol}"
                )
        elif golden_col.dtype == pl.Utf8:
            assert golden_col.equals(actual_col), (
                f"Column '{col}': string values mismatch"
            )
        else:
            # Integer/boolean columns: exact equality
            assert golden_col.equals(actual_col), (
                f"Column '{col}': exact values mismatch"
            )

    # Verify orphan bar presence
    orphan_count = golden_df.filter(pl.col("is_orphan")).height
    assert orphan_count >= 1, "Must have at least one orphan bar (from reset_at_ouroboros)"

    # Verify trade IDs
    assert "first_agg_trade_id" in golden_df.columns
    assert "last_agg_trade_id" in golden_df.columns
    assert golden_df["first_agg_trade_id"].min() > 0
    assert golden_df["last_agg_trade_id"].min() > 0

    print(f"Verification PASSED: {len(golden_df)} bars match exactly")
