"""OFI port — TDD against the Rust ml-features `ofi_calculator.rs` test oracle (Blocks F/G/H/CC)."""
from __future__ import annotations

import math

from fxhnt.domain.features.ofi import (
    OfiFeatures,
    book_deltas_per_level,
    log_gofi,
    ofi_per_level,
    slope_and_convexity,
)
from fxhnt.domain.marketdata import BidAskLevel, Mbp10Snapshot

_B, _A = 150_000_000_000, 150_010_000_000   # 150.00 / 150.01


def _snap(*levels: BidAskLevel) -> Mbp10Snapshot:
    return Mbp10Snapshot(symbol="ES", ts=0, levels=tuple(levels))


def _lvl(bid_px: int, bid_sz: int, ask_px: int, ask_sz: int) -> BidAskLevel:
    return BidAskLevel(bid_px=bid_px, bid_sz=bid_sz, ask_px=ask_px, ask_sz=ask_sz)


def test_ofi_per_level_balanced_unchanged_is_zero() -> None:
    snap = _snap(_lvl(_B, 100, _A, 100), _lvl(_B - 10**9, 100, _A + 10**9, 100))
    assert ofi_per_level(snap, snap) == [0.0, 0.0, 0.0, 0.0, 0.0]   # P_b≥P_b ∧ P_a≤P_a, balanced sizes


def test_ofi_per_level_upward_move_is_positive() -> None:
    prev = _snap(_lvl(_B, 100, _A, 100))
    curr = _snap(_lvl(_B + 5_000_000, 100, _A + 5_000_000, 100))  # bid rises (+V_b), ask rises (−V_a^prev)
    ofi = ofi_per_level(curr, prev)
    assert ofi[0] == 200.0 and ofi[0] > 0                          # 100 − (−100)


def test_ofi_per_level_truncates_to_common_levels() -> None:
    curr = _snap(_lvl(_B, 100, _A, 100))                            # 1 level
    prev = _snap(_lvl(_B, 100, _A, 100), _lvl(_B - 10**9, 50, _A + 10**9, 50))  # 2 levels
    assert ofi_per_level(curr, prev) == [0.0] * 5


def test_log_gofi_sign_and_value() -> None:
    out = log_gofi([10.0, -10.0, 1000.0, -1000.0, 0.0])
    assert out[0] == math.log1p(10.0) and out[1] == -math.log1p(10.0)
    assert out[2] == math.log1p(1000.0) and out[3] == -math.log1p(1000.0)
    assert out[4] == 0.0                                            # 0 → 0 (ln1p(0)=0)


def test_book_deltas_size_changes() -> None:
    prev = _snap(_lvl(_B, 100, _A, 100))
    curr = _snap(_lvl(_B, 130, _A, 80))
    bid_d, ask_d = book_deltas_per_level(curr, prev)
    assert bid_d[0] == 30.0 and ask_d[0] == -20.0


def test_slope_convexity_linear_cumulative_depth() -> None:
    # uniform per-level size 10 → cumulative [10,20,30,40,50]: slope 10, convexity 0 (linear)
    levels = [_lvl(_B - i * 10**9, 10, _A + i * 10**9, 10) for i in range(5)]
    sc = slope_and_convexity(_snap(*levels))
    assert abs(sc[0] - 10.0) < 1e-9 and abs(sc[1]) < 1e-9          # bid slope 10, convexity 0
    assert abs(sc[2] - 10.0) < 1e-9 and abs(sc[3]) < 1e-9


def test_slope_convexity_needs_three_levels() -> None:
    assert slope_and_convexity(_snap(_lvl(_B, 10, _A, 10), _lvl(_B, 10, _A, 10))) == [0.0, 0.0, 0.0, 0.0]


def test_ofi_features_cold_start_then_flows() -> None:
    f = OfiFeatures()
    levels = [_lvl(_B - i * 10**9, 10, _A + i * 10**9, 10) for i in range(5)]
    r0 = f.update(_snap(*levels))
    assert r0["ofi_l1"] == 0.0 and r0["bid_delta_l1"] == 0.0        # cold start: no flow yet
    assert abs(r0["cum_bid_slope"] - 10.0) < 1e-9                   # but slope/convexity are current-only
    # second snapshot: bid grows at L1 -> positive delta
    levels2 = [_lvl(_B - i * 10**9, 10 + (20 if i == 0 else 0), _A + i * 10**9, 10) for i in range(5)]
    r1 = f.update(_snap(*levels2))
    assert r1["bid_delta_l1"] == 20.0
