"""smart_router.plan_orders: sizing, caps, dust/min-notional skip, maker+abort pricing, reduce_only."""
from __future__ import annotations

from fxhnt.domain.execution.smart_router import RouteInput, plan_orders
from fxhnt.ports.crypto_exchange import OrderBook, SymbolFilter

F = SymbolFilter(symbol="X", step_size=0.001, tick_size=0.1, min_notional=5.0)
OB = OrderBook(symbol="X", bid=100.0, ask=100.2, bid_qty=10.0, ask_qty=10.0)   # depth ~1000 USD/side


def _inp(delta_usd, pos_qty=0.0, adv=1e9, max_slip=25.0, part=0.5, depth=0.5):
    return RouteInput(symbol="X", delta_usd=delta_usd, current_qty=pos_qty, filt=F, book=OB,
                      adv_usd=adv, participation_rate=part, depth_cap=depth, max_slippage_bps=max_slip)


def test_buy_maker_at_bid_with_abort() -> None:
    p = plan_orders([_inp(500.0)])[0]
    assert p.side == "BUY" and p.skip_reason is None
    assert p.price == 100.0                                   # maker buy posts at the bid (touch)
    # 500 USD / mid 100.1 = 4.995..., floored to step 0.001 = 4.995
    assert abs(p.qty - 4.995) < 1e-9
    assert p.abort_price > OB.mid                             # buy abort is above mid (max adverse)


def test_min_notional_skip() -> None:
    p = plan_orders([_inp(3.0)])[0]
    assert p.skip_reason == "min_notional"


def test_dust_skip() -> None:
    # delta_usd=0.01 -> use_usd/mid rounds to 0.0 after step_size floor -> "dust"
    p = plan_orders([_inp(0.01)])[0]
    assert p.skip_reason == "dust"


def test_no_book_when_zero_depth() -> None:
    # Real prices but zero depth on both sides -> no_book
    ob_zero = OrderBook(symbol="X", bid=100.0, ask=100.2, bid_qty=0.0, ask_qty=0.0)
    inp_buy = RouteInput(symbol="X", delta_usd=500.0, current_qty=0.0, filt=F, book=ob_zero,
                         adv_usd=1e9, participation_rate=0.5, depth_cap=0.5, max_slippage_bps=25.0)
    p_buy = plan_orders([inp_buy])[0]
    assert p_buy.skip_reason == "no_book"

    inp_sell = RouteInput(symbol="X", delta_usd=-500.0, current_qty=0.0, filt=F, book=ob_zero,
                          adv_usd=1e9, participation_rate=0.5, depth_cap=0.5, max_slippage_bps=25.0)
    p_sell = plan_orders([inp_sell])[0]
    assert p_sell.skip_reason == "no_book"

    # Zero prices also trigger no_book
    ob_no_px = OrderBook(symbol="X", bid=0.0, ask=0.0, bid_qty=10.0, ask_qty=10.0)
    inp_nopx = RouteInput(symbol="X", delta_usd=500.0, current_qty=0.0, filt=F, book=ob_no_px,
                          adv_usd=1e9, participation_rate=0.5, depth_cap=0.5, max_slippage_bps=25.0)
    p_nopx = plan_orders([inp_nopx])[0]
    assert p_nopx.skip_reason == "no_book"


def test_participation_and_depth_cap_limit_size() -> None:
    # huge target, ADV tiny -> participation cap binds
    p = plan_orders([_inp(1_000_000.0, adv=2000.0, part=0.05, depth=0.5)])[0]
    assert p.qty * OB.mid <= 0.05 * 2000.0 + 1e-6            # capped to participation×ADV (=100 USD)
    assert p.capped is True


def test_reduce_only_when_trade_reduces_position() -> None:
    # short 10 base; a BUY of 2 base reduces -> reduce_only
    p = plan_orders([_inp(200.0, pos_qty=-10.0)])[0]
    assert p.side == "BUY" and p.reduce_only is True


def test_reduce_only_false_when_increasing() -> None:
    # long 10 base; buying more increases the position -> NOT reduce_only
    p = plan_orders([_inp(200.0, pos_qty=10.0)])[0]
    assert p.side == "BUY" and p.reduce_only is False


def test_reduce_only_false_when_flipping() -> None:
    # short 1 base; buying enough to over-cover flips to long -> NOT reduce_only
    p = plan_orders([_inp(500.0, pos_qty=-1.0)])[0]
    assert p.side == "BUY" and p.reduce_only is False
    # qty must exceed the absolute short (1.0) so it genuinely flips
    assert p.qty > 1.0


def test_sell_maker_at_ask() -> None:
    p = plan_orders([_inp(-500.0)])[0]
    assert p.side == "SELL" and p.price == 100.2 and p.abort_price < OB.mid


def test_reduce_only_at_exact_cover_boundary() -> None:
    """Fix 3: an order sized to exactly cover the open position must always be reduce_only.

    The router sizes in base units as floor(usd / mid, step_size).  For a pos_qty long,
    we set delta_usd to produce a qty that precisely equals pos_qty after step-floor, so
    the cover is exact (qty == current_qty, the boundary case).  We verify reduce_only is
    True — the invariant that an exact cover never accidentally opens new exposure.

    Arithmetic:
      mid = (100.0 + 100.2) / 2 = 100.1
      For pos_qty = 4.995 (already a multiple of step_size=0.001):
        qty = floor(4.995 * 100.1 / 100.1, 0.001) = floor(4.995, 0.001) = 4.995  ✓
    """
    mid = OB.mid   # 100.1

    # SELL that exactly covers a long at the boundary.
    pos_qty = 4.995   # a clean multiple of step_size; floor(pos_qty * mid / mid) = pos_qty
    delta_sell = -(pos_qty * mid)
    p_sell = plan_orders([_inp(delta_sell, pos_qty=pos_qty)])[0]
    assert p_sell.side == "SELL"
    assert p_sell.skip_reason is None, f"order skipped unexpectedly: {p_sell.skip_reason}"
    assert p_sell.qty == pos_qty, f"expected qty=={pos_qty}, got {p_sell.qty}"
    assert p_sell.reduce_only is True, "exact SELL-covers-long boundary must be reduce_only"

    # BUY that exactly covers a short at the boundary.
    short_qty = -4.995
    delta_buy = abs(short_qty) * mid
    p_buy = plan_orders([_inp(delta_buy, pos_qty=short_qty)])[0]
    assert p_buy.side == "BUY"
    assert p_buy.skip_reason is None, f"order skipped unexpectedly: {p_buy.skip_reason}"
    assert p_buy.qty == abs(short_qty), f"expected qty=={abs(short_qty)}, got {p_buy.qty}"
    assert p_buy.reduce_only is True, "exact BUY-covers-short boundary must be reduce_only"
