"""Oracle-based verification for Aion mode correctness (Phase 15).

Verifies that:
1. VAL-02: Aion bars spanning UTC midnight have continuous trade IDs, no orphan
   bar, and correct VWAP independently verified from raw trades
2. VAL-03: Bars constructed from gapped trades have has_gap=true; bars from
   complete trades have has_gap=false

Oracle methodology: NEVER use the system under test to generate expected values.
Independently recompute from raw source. Assert numerical identity, not approximate.
"""

from __future__ import annotations

from conftest import assert_stathera_continuity, make_test_trade
from opendeviationbar._core import PyOpenDeviationBarProcessor

# =============================================================================
# Constants
# =============================================================================

BASE_TS_MS = 1704067200000  # 2024-01-01T00:00:00Z in ms
MIDNIGHT_JAN2_MS = 1704153600000  # 2024-01-02T00:00:00Z in ms
THRESHOLD_DBPS = 250
BASE_PRICE = 50000.0


# =============================================================================
# Trade data generators (deterministic, standalone)
# =============================================================================


def _generate_breach_trades(
    n: int = 200,
    threshold_dbps: int = THRESHOLD_DBPS,
    base_price: float = BASE_PRICE,
    base_ts_ms: int = BASE_TS_MS,
    start_tid: int = 1000,
) -> list[dict]:
    """Generate trades that oscillate enough to trigger open deviation bar breaches.

    Price oscillates in a sawtooth: rises by step until > 1.5x threshold
    from base, then reverses direction. Guaranteed multiple breaches.

    Returns list of trade dicts with all fields needed by the processor.
    """
    threshold = base_price * (threshold_dbps / 100_000)
    step = threshold / 3  # ~3 trades per breach

    trades: list[dict] = []
    price = base_price
    direction = 1.0

    for i in range(n):
        price += direction * step
        # Reverse when we've moved 1.5x threshold from base
        if abs(price - base_price) > 1.5 * threshold:
            direction *= -1.0

        trades.append(
            {
                "timestamp": base_ts_ms + i * 1000,
                "price": round(price, 8),
                "quantity": round(1.0 + (i % 5) * 0.5, 8),
                "is_buyer_maker": (i % 2 == 0),
                "agg_trade_id": start_tid + i,
                "first_trade_id": (start_tid + i) * 10,
                "last_trade_id": (start_tid + i) * 10 + 2,
            }
        )

    return trades


# =============================================================================
# Assertion helpers
# =============================================================================


def _assert_bars_equal(
    bar_a: dict,
    bar_b: dict,
    skip_keys: set[str] | None = None,
) -> None:
    """Assert two bar dicts are field-equal, skipping specified keys.

    For float fields, uses relative tolerance of 1e-10.
    Default skip_keys={"is_orphan"}.
    """
    skip = skip_keys if skip_keys is not None else {"is_orphan"}
    keys_a = set(bar_a.keys()) - skip
    keys_b = set(bar_b.keys()) - skip
    assert keys_a == keys_b, (
        f"Key mismatch: only_a={keys_a - keys_b}, only_b={keys_b - keys_a}"
    )

    for key in sorted(keys_a):
        va, vb = bar_a[key], bar_b[key]
        if isinstance(va, float) and isinstance(vb, float):
            if va == 0.0 and vb == 0.0:
                continue
            denom = max(abs(va), abs(vb), 1e-15)
            assert abs(va - vb) / denom < 1e-10, (
                f"Float mismatch on '{key}': {va} vs {vb}"
            )
        else:
            assert va == vb, f"Value mismatch on '{key}': {va} vs {vb}"


def _compute_vwap(trades: list[dict]) -> float:
    """Independently compute VWAP from raw trades.

    Oracle methodology: NEVER use the system under test to generate
    expected values. Compute from first principles.
    """
    total_turnover = sum(t["price"] * t["quantity"] for t in trades)
    total_volume = sum(t["quantity"] for t in trades)
    if total_volume == 0:
        return 0.0
    return total_turnover / total_volume


# =============================================================================
# VAL-02: Multi-day midnight-spanning continuity
# =============================================================================


def test_multi_day_continuity():
    """VAL-02: Aion bars spanning midnight have continuous TIDs, no orphan, correct VWAP.

    Feed trades spanning 2024-01-01 23:59:xx to 2024-01-02 00:00:xx.
    Small price movement (within threshold) so no bar completes during the span.
    Then feed breach-triggering trades after midnight so bar completes.
    """
    micro_step = BASE_PRICE * THRESHOLD_DBPS / 100_000 / 20  # 5% of threshold

    # Pre-midnight trades: 30 trades with small jitter, no breach
    pre_midnight = []
    for i in range(30):
        ts = MIDNIGHT_JAN2_MS - 30000 + i * 1000
        pre_midnight.append(
            make_test_trade(
                agg_trade_id=1000 + i,
                timestamp_ms=ts,
                price=round(BASE_PRICE + (i % 3 - 1) * micro_step, 8),
                quantity=1.0,
            )
        )

    # Post-midnight trades: 30 trades with small jitter, no breach
    post_midnight = []
    for i in range(30):
        ts = MIDNIGHT_JAN2_MS + (i + 1) * 1000
        idx = 30 + i
        post_midnight.append(
            make_test_trade(
                agg_trade_id=1000 + idx,
                timestamp_ms=ts,
                price=round(BASE_PRICE + (idx % 3 - 1) * micro_step, 8),
                quantity=1.0,
            )
        )

    # Breach trades after midnight to force bar completions
    breach_trades = _generate_breach_trades(
        n=100, base_ts_ms=MIDNIGHT_JAN2_MS + 31000, start_tid=1060
    )

    all_trades = pre_midnight + post_midnight + breach_trades

    # --- Aion-mode path ---
    aion_proc = PyOpenDeviationBarProcessor(THRESHOLD_DBPS)
    aion_completed = aion_proc.process_trades_streaming(all_trades)
    aion_incomplete = aion_proc.get_incomplete_bar()

    # Collect all Aion bars for assertions
    aion_all_bars = list(aion_completed)
    if aion_incomplete is not None:
        aion_all_bars.append(aion_incomplete)

    # Assertion 1: Stathera continuity across all Aion bars
    if len(aion_all_bars) >= 2:
        assert_stathera_continuity(aion_all_bars)

    # Assertion 3: Find the Aion bar that spans midnight
    midnight_us = MIDNIGHT_JAN2_MS * 1000  # Convert to microseconds
    spanning_bar = None
    for bar in aion_all_bars:
        if bar["open_time_us"] < midnight_us and bar["close_time_us"] >= midnight_us:
            spanning_bar = bar
            break

    assert spanning_bar is not None, (
        "Should have at least one Aion bar spanning midnight"
    )

    # Assertion 4: Independent VWAP verification for the spanning bar
    # Collect trades whose TIDs fall within the spanning bar's range
    bar_first_tid = spanning_bar["first_agg_trade_id"]
    bar_last_tid = spanning_bar["last_agg_trade_id"]

    trades_in_bar = [
        t
        for t in all_trades
        if bar_first_tid <= t["agg_trade_id"] <= bar_last_tid
    ]
    assert len(trades_in_bar) > 0, "Should have trades in spanning bar"

    expected_vwap = _compute_vwap(trades_in_bar)
    actual_vwap = spanning_bar["vwap"]
    denom = max(abs(expected_vwap), abs(actual_vwap), 1e-15)
    assert abs(expected_vwap - actual_vwap) / denom < 1e-10, (
        f"VWAP mismatch: expected={expected_vwap}, actual={actual_vwap}"
    )


# =============================================================================
# VAL-03: Gap flag lifecycle
# =============================================================================


def test_gap_flag_lifecycle():
    """VAL-03: has_gap=true with gapped trades, has_gap=false after repair.

    Phase 1: Feed trades with TID gap (skip 1005-1009). Verify gap flags set.
    Phase 2: Feed complete trades (no gap). Verify gap flags clear.
    """
    # Use small threshold (10 dbps) so breaches happen quickly with few trades
    small_threshold = 10

    # --- Phase 1: Gapped trades ---
    # Trades 1000-1004 (continuous), then skip to 1010-1020
    # Use sawtooth prices to guarantee breaches with 10 dbps threshold
    threshold_amount = BASE_PRICE * (small_threshold / 100_000)
    step = threshold_amount / 2  # Fast breaches

    gapped_trades = []
    price = BASE_PRICE
    direction = 1.0

    # Continuous trades: TIDs 1000-1004
    for i in range(5):
        price += direction * step
        if abs(price - BASE_PRICE) > 1.5 * threshold_amount:
            direction *= -1.0
        gapped_trades.append(
            make_test_trade(
                agg_trade_id=1000 + i,
                timestamp_ms=BASE_TS_MS + i * 1000,
                price=round(price, 8),
                quantity=1.0,
            )
        )

    # Gap: skip TIDs 1005-1009
    # Continue from TID 1010 with a time gap too
    for i in range(11):  # TIDs 1010-1020
        price += direction * step
        if abs(price - BASE_PRICE) > 1.5 * threshold_amount:
            direction *= -1.0
        gapped_trades.append(
            make_test_trade(
                agg_trade_id=1010 + i,
                timestamp_ms=BASE_TS_MS + (10 + i) * 1000,
                price=round(price, 8),
                quantity=1.0,
            )
        )

    proc_gap = PyOpenDeviationBarProcessor(small_threshold)
    gapped_completed = proc_gap.process_trades_streaming(gapped_trades)
    gapped_incomplete = proc_gap.get_incomplete_bar()

    # Collect all bars to find the one with the gap
    all_gapped_bars = list(gapped_completed)
    if gapped_incomplete is not None:
        all_gapped_bars.append(gapped_incomplete)

    # Find bar(s) with has_gap=True
    gap_bars = [b for b in all_gapped_bars if b.get("has_gap") is True]
    assert len(gap_bars) > 0, (
        f"Should have at least 1 bar with has_gap=True. "
        f"Bars: {[(b.get('first_agg_trade_id'), b.get('last_agg_trade_id'), b.get('has_gap')) for b in all_gapped_bars]}"
    )

    for gb in gap_bars:
        assert gb["gap_trade_count"] > 0, "Gap bar should have gap_trade_count > 0"
        assert gb["max_gap_duration_us"] > 0, "Gap bar should have max_gap_duration_us > 0"
        assert gb["is_exchange_gap"] is False, (
            "is_exchange_gap should be False (Python sets this, not Rust)"
        )

    # --- Phase 2: Repaired (complete) trades ---
    # Same TID range 1000-1020, ALL TIDs present (no gap)
    complete_trades = []
    price = BASE_PRICE
    direction = 1.0

    for i in range(21):  # TIDs 1000-1020, all present
        price += direction * step
        if abs(price - BASE_PRICE) > 1.5 * threshold_amount:
            direction *= -1.0
        complete_trades.append(
            make_test_trade(
                agg_trade_id=1000 + i,
                timestamp_ms=BASE_TS_MS + i * 1000,
                price=round(price, 8),
                quantity=1.0,
            )
        )

    proc_clean = PyOpenDeviationBarProcessor(small_threshold)
    clean_completed = proc_clean.process_trades_streaming(complete_trades)
    clean_incomplete = proc_clean.get_incomplete_bar()

    all_clean_bars = list(clean_completed)
    if clean_incomplete is not None:
        all_clean_bars.append(clean_incomplete)

    # ALL bars should have has_gap=False
    for bar in all_clean_bars:
        assert bar["has_gap"] is False, (
            f"Clean bar should have has_gap=False, got True for "
            f"TIDs {bar['first_agg_trade_id']}-{bar['last_agg_trade_id']}"
        )
        assert bar["gap_trade_count"] == 0, "Clean bar should have gap_trade_count=0"
        assert bar["max_gap_duration_us"] == 0, "Clean bar should have max_gap_duration_us=0"


# =============================================================================
# Bonus: Gap detection requires prior trade (Pitfall 4)
# =============================================================================


def test_gap_detection_requires_prior_trade():
    """Documents Pitfall 4: gap detection requires last_agg_trade_id > 0.

    The first trade always passes without gap check. After TID 1000 is processed,
    a jump to TID 2000 IS detected as a gap because last_agg_trade_id is now > 0.
    """
    trades = [
        make_test_trade(
            agg_trade_id=1000,
            timestamp_ms=BASE_TS_MS,
            price=BASE_PRICE,
            quantity=1.0,
        ),
        make_test_trade(
            agg_trade_id=2000,
            timestamp_ms=BASE_TS_MS + 1000,
            price=BASE_PRICE + 1.0,  # Tiny move, stays in same bar
            quantity=1.0,
        ),
    ]

    proc = PyOpenDeviationBarProcessor(THRESHOLD_DBPS)
    completed = proc.process_trades_streaming(trades)
    incomplete = proc.get_incomplete_bar()

    # The bar containing both trades should detect the gap
    all_bars = list(completed)
    if incomplete is not None:
        all_bars.append(incomplete)

    assert len(all_bars) > 0, "Should have at least 1 bar"

    # Since both trades are in the same bar (tiny price move at 250 dbps),
    # the incomplete bar should have has_gap=True
    bar_with_gap = all_bars[-1]  # The bar containing both trades
    assert bar_with_gap["has_gap"] is True, (
        f"Gap should be detected after first trade sets last_agg_trade_id > 0. "
        f"TIDs: {bar_with_gap['first_agg_trade_id']}-{bar_with_gap['last_agg_trade_id']}"
    )
