"""
Smoke tests for trading/sizing.py.

Three tiers:
  1. Primitives: budget_pre_check, compute_contracts (Day 1 code,
     never had a test file — covered here).
  2. Conversion helpers: points_to_ticks, points_to_dollars.
  3. Public entry point: size_for_entry (EntryDecision -> SizingDecision).

Run:
    cd ~/apex_v16
    python -m tests.test_sizing
"""

from __future__ import annotations

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from core import config_futures as cfg_fut
from core.contracts import EntryDecision
from trading.sizing import (
    BudgetCheckResult,
    SizingAudit,
    SizingDecision,
    SizingResult,
    budget_pre_check,
    compute_contracts,
    points_to_dollars,
    points_to_ticks,
    size_for_entry,
)


def _ok(label: str) -> None:
    print(f"  ok  {label}")


def make_entry(
    *,
    direction: str = "BUY",
    entry_price: float = 5800.00,
    sl_price: float = 5797.50,
    tp_price: float = 5805.00,
    confidence: int = 75,
    risk_multiplier: float = 1.0,
) -> EntryDecision:
    return EntryDecision(
        direction=direction,
        entry_price=entry_price,
        sl_price=sl_price,
        tp_price=tp_price,
        rr_multiplier=0.50,
        confidence=confidence,
        rationale="test",
        metadata={"risk_multiplier": risk_multiplier},
    )


# ============================================================
# 1. PRIMITIVES — budget_pre_check (Day 1 backfill)
# ============================================================

def test_budget_pre_check_ok():
    """Headroom >> typical risk -> not skipped."""
    res = budget_pre_check(
        symbol="MES",
        tick_value=1.25, min_sl_ticks=8, max_sl_ticks=40,
        daily_pnl=0.0, daily_loss_hard_stop=-1500.0,
        max_risk_vs_daily_budget=0.33,
    )
    assert isinstance(res, BudgetCheckResult)
    assert res.skip is False
    assert res.reason == "budget_ok"
    _ok("budget_pre_check: ample headroom -> not skipped")


def test_budget_pre_check_hard_skip():
    """Daily budget already drained -> min trade exceeds cap -> hard skip."""
    res = budget_pre_check(
        symbol="MES",
        tick_value=1.25, min_sl_ticks=8, max_sl_ticks=40,
        daily_pnl=-1490.0,                     # near hard stop
        daily_loss_hard_stop=-1500.0,
        max_risk_vs_daily_budget=0.33,
    )
    assert res.skip is True
    assert "budget_hard" in res.reason
    _ok("budget_pre_check: budget drained -> hard skip")


def test_budget_pre_check_invalid_inputs():
    """tick_value <= 0 raises (no silent default tick math)."""
    try:
        budget_pre_check(
            symbol="X", tick_value=0,
            min_sl_ticks=8, max_sl_ticks=40,
            daily_pnl=0.0, daily_loss_hard_stop=-1500.0,
            max_risk_vs_daily_budget=0.33,
        )
    except ValueError as e:
        assert "tick_value" in str(e)
        _ok("budget_pre_check: tick_value=0 -> ValueError")
        return
    raise AssertionError("expected ValueError")


# ============================================================
# 2. PRIMITIVES — compute_contracts (Day 1 backfill)
# ============================================================

def test_compute_contracts_base():
    """
    MES, 100k balance, risk 0.003 = $300. sl_ticks=30 viene "floored" al
    worst-case MAX_SL_TICKS[MES]=40 (MES è in WORST_CASE_SIZING_ASSETS),
    quindi sl_usd_per_contract = 40 * $1.25 = $50/ct.
    target = 300 / 50 = 6.0 → 6 contratti (entro MAX 8).
    real_risk = 6 * 50 = $300. Daily cap $495 → 300 fits.
    """
    res = compute_contracts(
        symbol="MES", sl_ticks=30,
        account_balance=100_000.0, tick_value=1.25,
        risk_per_trade=0.003, max_contracts=8,
        min_contract_fraction=0.7,
        daily_pnl=0.0, daily_loss_hard_stop=-1500.0,
        max_risk_vs_daily_budget=0.33,
    )
    assert res.skip is False
    assert res.contracts == 6
    assert res.real_risk_usd == 300.0
    assert res.sl_ticks_original == 30
    assert res.sl_ticks_floored is True
    _ok(f"compute_contracts: base case -> {res.contracts}ct ${res.real_risk_usd}")


def test_compute_contracts_size_too_small():
    """Huge SL -> target fraction below floor -> skip size_too_small.
    Use 'ES' (no entry in MIN/MAX_SL_TICKS, no worst-case floor) so sl_ticks
    non viene "floored" e il caso di studio resta valido."""
    res = compute_contracts(
        symbol="ES", sl_ticks=400,            # huge -> $500/ct
        account_balance=100_000.0, tick_value=1.25,
        risk_per_trade=0.003, max_contracts=8,
        min_contract_fraction=0.7,
        daily_pnl=0.0, daily_loss_hard_stop=-1500.0,
        max_risk_vs_daily_budget=0.33,
    )
    assert res.skip is True
    assert "size_too_small" in res.reason
    assert res.sl_ticks_floored is False
    assert res.sl_ticks_original == 400
    _ok("compute_contracts: huge SL -> size_too_small")


def test_compute_contracts_invalid_sl_zero():
    """sl_ticks=0 raises (no silent /0)."""
    try:
        compute_contracts(
            symbol="MES", sl_ticks=0,
            account_balance=100_000.0, tick_value=1.25,
            risk_per_trade=0.003, max_contracts=8,
            min_contract_fraction=0.7,
            daily_pnl=0.0, daily_loss_hard_stop=-1500.0,
            max_risk_vs_daily_budget=0.33,
        )
    except ValueError as e:
        assert "sl_ticks" in str(e)
        _ok("compute_contracts: sl_ticks=0 -> ValueError")
        return
    raise AssertionError("expected ValueError")


# ============================================================
# 3. CONVERSION HELPERS
# ============================================================

def test_points_to_ticks_mes():
    # MES tick_size = 0.25; 2.5 points = 10 ticks
    assert points_to_ticks("MES", 2.5) == 10
    _ok("points_to_ticks: MES 2.5p -> 10 ticks")


def test_points_to_dollars_mes():
    # MES tick_value = 1.25; 10 ticks * 1.25 = $12.50
    assert points_to_dollars("MES", 2.5) == 12.50
    _ok("points_to_dollars: MES 2.5p -> $12.50/ct")


def test_points_to_dollars_unknown_symbol_raises():
    try:
        points_to_dollars("BOGUS", 1.0)
    except ValueError as e:
        assert "BOGUS" in str(e)
        _ok("points_to_dollars: unknown symbol -> ValueError")
        return
    raise AssertionError("expected ValueError")


def test_points_to_ticks_zero_raises():
    try:
        points_to_ticks("MES", 0)
    except ValueError as e:
        assert "distance_points" in str(e)
        _ok("points_to_ticks: distance=0 -> ValueError")
        return
    raise AssertionError("expected ValueError")


# ============================================================
# 4. size_for_entry — base case
# ============================================================

def test_size_for_entry_mes_base_case():
    """
    MES, balance $100k, risk 0.003 = $300 target.
    SL distance = 5800 - 5797.50 = 2.50 points = 10 ticks (Brain-proposed).
    Worst-case sizing (MES ∈ WORST_CASE_SIZING_ASSETS): sl_ticks_for_sizing =
    MAX_SL_TICKS[MES] = 40 → sl_usd_per_contract = 40 * $1.25 = $50/ct.
    target_contracts = 300 / 50 = 6.0 → 6 ct (= MAX MES). real_risk = $300.
    decision.sl_ticks resta 10 (SL inviato al broker = quello del Brain).
    """
    decision = size_for_entry(
        entry=make_entry(entry_price=5800.00, sl_price=5797.50),
        symbol="MES", account_balance=100_000.0,
        risk_per_trade=0.003, daily_pnl=0.0,
        daily_loss_hard_stop=-1500.0,
        max_risk_vs_daily_budget=0.33,
    )
    assert isinstance(decision, SizingDecision)
    assert decision.skip is False
    assert decision.contracts == 6
    assert decision.sl_ticks == 10
    assert decision.real_risk_usd == 300.0
    # Audit
    assert decision.audit.symbol == "MES"
    assert decision.audit.tick_size == 0.25
    assert decision.audit.tick_value == 1.25
    assert decision.audit.sl_usd_per_contract == 50.0
    assert decision.audit.risk_usd_target == 300.0
    assert decision.audit.target_float == 6.0
    assert decision.audit.max_contracts_used == 6
    assert decision.audit.clamp_active is False
    assert decision.audit.inverted_quote is False
    assert decision.audit.sl_ticks_floored is True
    assert decision.audit.sl_ticks_original == 10
    _ok(f"size_for_entry: MES base -> 6ct ${decision.real_risk_usd} worst-case sizing")


# ============================================================
# 5. clamp_active False when target naturally fits
# ============================================================

def test_size_for_entry_clamp_inactive_when_target_fits():
    """
    Asset non-worst-case e fuori da MIN_CONTRACTS_PER_TRADE (MCL): il target
    atterra naturalmente sotto max_contracts e nessun floor entra in gioco.
    MCL: tick_size=0.01, tick_value=$1, MIN_SL_TICKS=20, MAX_SL_TICKS=80,
    MAX_CONTRACTS=4. SL distance=0.70 → 70 ticks (>MIN, no floor).
    sl_usd = 70 * 1 = $70/ct. risk=80k*0.003=$240 → target=3.43 → 3 ct < 4.
    """
    decision = size_for_entry(
        entry=make_entry(entry_price=70.00, sl_price=69.30),
        symbol="MCL", account_balance=80_000.0,
        risk_per_trade=0.003, daily_pnl=0.0,
        daily_loss_hard_stop=-1500.0,
        max_risk_vs_daily_budget=0.33,
    )
    assert decision.skip is False
    assert decision.contracts == 3
    assert decision.audit.clamp_active is False
    assert decision.audit.sl_ticks_floored is False
    _ok(f"size_for_entry: target {decision.audit.target_float} fits -> clamp_active=False")


# ============================================================
# 6. size_too_small skip
# ============================================================

def test_size_for_entry_size_too_small_skip():
    """SL so wide that target_float < 0.7 -> skip."""
    # 6E tick_value 6.25, tick_size 0.00005. Make SL very wide:
    # 0.0050 points / 0.00005 = 100 ticks * $6.25 = $625/ct.
    # risk 100k * 0.003 = $300 -> target 300/625 = 0.48 < 0.70 -> skip.
    decision = size_for_entry(
        entry=make_entry(entry_price=1.0850, sl_price=1.0800),
        symbol="6E", account_balance=100_000.0,
        risk_per_trade=0.003, daily_pnl=0.0,
        daily_loss_hard_stop=-1500.0,
        max_risk_vs_daily_budget=0.33,
    )
    assert decision.skip is True
    assert decision.contracts == 0
    assert "size_too_small" in decision.reason
    _ok(f"size_for_entry: size_too_small -> skip (target {decision.audit.target_float})")


# ============================================================
# 7. risk_exceeds_daily_budget skip
# ============================================================

def test_size_for_entry_risk_exceeds_budget():
    """Daily near hard stop -> any reasonable trade exceeds 33% cap -> skip."""
    decision = size_for_entry(
        entry=make_entry(entry_price=5800.00, sl_price=5787.50),  # $62.50/ct
        symbol="MES", account_balance=100_000.0,
        risk_per_trade=0.003, daily_pnl=-1300.0,                  # only $200 left
        daily_loss_hard_stop=-1500.0,
        max_risk_vs_daily_budget=0.33,                            # cap = $66
    )
    assert decision.skip is True
    assert "risk_exceeds_daily_budget" in decision.reason
    _ok("size_for_entry: real_risk > daily cap -> skip")


# ============================================================
# 8. 6J inverted_quote dollar correctness
# ============================================================

def test_size_for_entry_6j_dollar_correctness():
    """
    6J: tick_size = 0.0000005, tick_value = $6.25, inverted_quote = True.
    Manual math:
      sl_distance_points = 0.0001000 (typical 6J SL)
      sl_ticks = 0.0001 / 0.0000005 = 200
      sl_usd_per_contract = 200 * 6.25 = $1250
      risk_usd = 100k * 0.003 = $300
      target_contracts = 300 / 1250 = 0.24 < 0.70 -> skip size_too_small
    """
    decision = size_for_entry(
        entry=make_entry(entry_price=0.0070000, sl_price=0.0069000),
        symbol="6J", account_balance=100_000.0,
        risk_per_trade=0.003, daily_pnl=0.0,
        daily_loss_hard_stop=-1500.0,
        max_risk_vs_daily_budget=0.33,
    )
    # Math diagnostics confirm tick conversion is correct
    assert decision.audit.sl_distance_points == 0.0001
    assert decision.audit.tick_size == 0.0000005
    assert decision.audit.tick_value == 6.25
    assert decision.sl_ticks == 200
    assert decision.audit.sl_usd_per_contract == 1250.0
    assert decision.audit.risk_usd_target == 300.0
    # inverted_quote propagated for audit but doesn't alter math
    assert decision.audit.inverted_quote is True
    # Outcome: target 0.24 < 0.70 -> skip
    assert decision.skip is True
    assert "size_too_small" in decision.reason
    _ok(f"size_for_entry: 6J math correct (sl_ticks={decision.sl_ticks}, "
        f"$/ct={decision.audit.sl_usd_per_contract}), inverted_quote audit-only")


def test_size_for_entry_6j_executable_size():
    """6J with smaller SL produces contracts >= 2 (V18 MIN floor) — execution path with inverted_quote."""
    decision = size_for_entry(
        entry=make_entry(entry_price=0.0070000, sl_price=0.0069975),
        symbol="6J", account_balance=500_000.0,    # big enough to size
        risk_per_trade=0.003, daily_pnl=0.0,
        daily_loss_hard_stop=-3000.0,
        max_risk_vs_daily_budget=0.50,
    )
    # 0.0000025 / 0.0000005 = 5 ticks * 6.25 = $31.25/ct
    # risk 500k * 0.003 = $1500 -> target ~48 -> MAX 6J = 2 (V18) -> 2 contracts
    assert decision.skip is False
    assert decision.contracts == 2                  # V18: MAX_CONTRACTS_PER_TRADE["6J"]=2
    assert decision.audit.clamp_active is True
    assert decision.audit.inverted_quote is True
    _ok(f"size_for_entry: 6J executable {decision.contracts}ct, clamp on MAX_CONTRACTS")


# ============================================================
# 9. Edge cases: ValueError on bad input
# ============================================================

def test_size_for_entry_sl_distance_zero_raises():
    """entry_price == sl_price -> sl_distance = 0 -> ValueError (not silent skip)."""
    try:
        size_for_entry(
            entry=make_entry(entry_price=5800.0, sl_price=5800.0),
            symbol="MES", account_balance=100_000.0,
            risk_per_trade=0.003, daily_pnl=0.0,
            daily_loss_hard_stop=-1500.0, max_risk_vs_daily_budget=0.33,
        )
    except ValueError as e:
        assert "sl_distance_points" in str(e)
        _ok("size_for_entry: entry==sl -> ValueError (no silent /0)")
        return
    raise AssertionError("expected ValueError")


def test_size_for_entry_unknown_symbol_raises():
    try:
        size_for_entry(
            entry=make_entry(),
            symbol="BOGUS", account_balance=100_000.0,
            risk_per_trade=0.003, daily_pnl=0.0,
            daily_loss_hard_stop=-1500.0, max_risk_vs_daily_budget=0.33,
        )
    except ValueError as e:
        assert "BOGUS" in str(e) or "ASSETS_MAP" in str(e)
        _ok("size_for_entry: unknown symbol -> ValueError")
        return
    raise AssertionError("expected ValueError")


def test_size_for_entry_balance_zero_raises():
    """account_balance <= 0 -> ValueError (delegated from compute_contracts)."""
    try:
        size_for_entry(
            entry=make_entry(),
            symbol="MES", account_balance=0.0,
            risk_per_trade=0.003, daily_pnl=0.0,
            daily_loss_hard_stop=-1500.0, max_risk_vs_daily_budget=0.33,
        )
    except ValueError as e:
        assert "account_balance" in str(e)
        _ok("size_for_entry: balance=0 -> ValueError")
        return
    raise AssertionError("expected ValueError")


# ============================================================
# 10. risk_multiplier read from entry.metadata
# ============================================================

def test_size_for_entry_reads_risk_multiplier_from_metadata():
    """
    Brain provides risk_multiplier=0.5 via EntryDecision.metadata —
    real risk should halve compared to default 1.0.
    """
    base = size_for_entry(
        entry=make_entry(entry_price=5800.00, sl_price=5787.50),  # 50 ticks
        symbol="MES", account_balance=100_000.0,
        risk_per_trade=0.003, daily_pnl=0.0,
        daily_loss_hard_stop=-1500.0, max_risk_vs_daily_budget=0.33,
    )
    halved = size_for_entry(
        entry=make_entry(entry_price=5800.00, sl_price=5787.50,
                         risk_multiplier=0.5),
        symbol="MES", account_balance=100_000.0,
        risk_per_trade=0.003, daily_pnl=0.0,
        daily_loss_hard_stop=-1500.0, max_risk_vs_daily_budget=0.33,
    )
    assert halved.audit.risk_usd_target == base.audit.risk_usd_target / 2
    assert halved.audit.risk_multiplier == 0.5
    # Halved target_float -> contracts at most equal to base, often less
    assert halved.contracts <= base.contracts
    _ok(f"size_for_entry: risk_multiplier 0.5 halves risk_usd_target "
        f"({base.audit.risk_usd_target} -> {halved.audit.risk_usd_target})")


# ============================================================
# 11. WORST-CASE SIZING FLOOR (micro indici + MGC)
#
# Per MNQ/MES/MYM/MGC il sizing usa sempre MAX_SL_TICKS[symbol]
# come sl_usd_per_contract base, indipendentemente dallo sl_ticks
# proposto dal Brain. L'SL inviato al broker resta quello del Brain.
# Bilancio di riferimento $50K, risk_per_trade 0.003 → risk_usd=$150.
# ============================================================

def _compute(symbol: str, sl_ticks: int, *, tick_value: float,
             max_contracts: int, balance: float = 50_000.0):
    return compute_contracts(
        symbol=symbol, sl_ticks=sl_ticks,
        account_balance=balance, tick_value=tick_value,
        risk_per_trade=0.003, max_contracts=max_contracts,
        min_contract_fraction=0.7,
        daily_pnl=0.0, daily_loss_hard_stop=-2000.0,
        max_risk_vs_daily_budget=0.50,
    )


def test_worst_case_mnq_tight_sl_to_6ct():
    """MNQ sl=6 (tight) → floor a MAX_SL_TICKS[MNQ]=48 → 6 contratti."""
    res = _compute("MNQ", sl_ticks=6, tick_value=0.50, max_contracts=12)
    assert res.skip is False
    assert res.contracts == 6
    assert res.sl_ticks_floored is True
    assert res.sl_ticks_original == 6
    _ok("worst-case MNQ sl=6 → 6 ct (floored to MAX 48)")


def test_worst_case_mnq_already_max_sl():
    """MNQ sl=48 (= MAX) → forzato a MAX comunque → 6 contratti.
    Nota: sl_ticks resta 48, quindi sl_ticks_floored=False."""
    res = _compute("MNQ", sl_ticks=48, tick_value=0.50, max_contracts=12)
    assert res.skip is False
    assert res.contracts == 6
    assert res.sl_ticks_floored is False
    assert res.sl_ticks_original == 48
    _ok("worst-case MNQ sl=48 → 6 ct (no change)")


def test_worst_case_mes_tight_sl_to_3ct():
    """MES sl=4 → floor a MAX_SL_TICKS[MES]=40 → 3 contratti."""
    res = _compute("MES", sl_ticks=4, tick_value=1.25, max_contracts=6)
    assert res.skip is False
    assert res.contracts == 3
    assert res.sl_ticks_floored is True
    assert res.sl_ticks_original == 4
    _ok("worst-case MES sl=4 → 3 ct (floored to MAX 40)")


def test_worst_case_mym_tight_sl_to_6ct():
    """MYM sl=8 → floor a MAX_SL_TICKS[MYM]=50 → 6 contratti."""
    res = _compute("MYM", sl_ticks=8, tick_value=0.50, max_contracts=12)
    assert res.skip is False
    assert res.contracts == 6
    assert res.sl_ticks_floored is True
    assert res.sl_ticks_original == 8
    _ok("worst-case MYM sl=8 → 6 ct (floored to MAX 50)")


def test_worst_case_mgc_tight_sl_to_2ct():
    """MGC sl=5 → floor a MAX_SL_TICKS[MGC]=80 → 2 contratti."""
    res = _compute("MGC", sl_ticks=5, tick_value=1.00, max_contracts=4)
    assert res.skip is False
    assert res.contracts == 2
    assert res.sl_ticks_floored is True
    assert res.sl_ticks_original == 5
    _ok("worst-case MGC sl=5 → 2 ct (floored to MAX 80)")


def test_worst_case_mgc_already_max_sl():
    """MGC sl=80 (= MAX) → 2 contratti, sl_ticks_floored=False."""
    res = _compute("MGC", sl_ticks=80, tick_value=1.00, max_contracts=4)
    assert res.skip is False
    assert res.contracts == 2
    assert res.sl_ticks_floored is False
    _ok("worst-case MGC sl=80 → 2 ct (no change)")


def test_non_worst_case_6e_floor_min_sl():
    """6E sl=8 < MIN_SL_TICKS[6E]=10 → floor up a 10."""
    res = _compute("6E", sl_ticks=8, tick_value=6.25, max_contracts=2)
    assert res.skip is False
    assert res.sl_ticks_floored is True
    assert res.sl_ticks_original == 8
    # risk=$150, sl_usd=10*6.25=$62.50 → target=2.4 → clamp a max=2
    assert res.contracts == 2
    _ok("non-worst-case 6E sl=8 → floored a MIN 10")


def test_non_worst_case_6e_above_min_unchanged():
    """6E sl=15 ≥ MIN=10 → sl_ticks invariato, sl_ticks_floored=False."""
    res = _compute("6E", sl_ticks=15, tick_value=6.25, max_contracts=2)
    assert res.sl_ticks_floored is False
    assert res.sl_ticks_original == 15
    _ok("non-worst-case 6E sl=15 → invariato")


def test_no_floor_entry_unchanged():
    """Asset senza entry in MIN/MAX_SL_TICKS (e fuori dal worst-case set):
    sl_ticks resta invariato e sl_ticks_floored=False."""
    res = _compute("ES", sl_ticks=20, tick_value=12.50, max_contracts=1)
    assert res.sl_ticks_floored is False
    assert res.sl_ticks_original == 20
    _ok("no-entry asset (ES) → invariato, sl_ticks_floored=False")


# ============================================================
# 12. MIN_CONTRACTS_PER_TRADE floor (V18 12-mag)
#
# FX majors: forzato 2 ct minimo per abilitare partial 50%.
# Sizing naturale darebbe 1 ct → demote a EXIT; il floor preserva la
# possibilità di chiudere metà posizione.
# ============================================================

def test_min_contracts_floor_6a():
    """6A sl=12 — sizing naturale 1ct → floor forza a 2ct."""
    res = _compute("6A", sl_ticks=12, tick_value=10.0, max_contracts=2)
    # natural: risk=$150 / sl_usd=$120 = 1.25 → round 1 → forzato a 2 dal floor
    assert res.skip is False
    assert res.contracts == 2
    # real_risk = 2 * 12 * $10 = $240 (worst-case dichiarato in spec)
    assert res.real_risk_usd == 240.0
    _ok("MIN floor: 6A sl=12 → 2 ct (worst-case $240)")


def test_min_contracts_floor_6j():
    """6J sl=20 — sizing naturale 1ct → floor forza a 2ct."""
    res = _compute("6J", sl_ticks=20, tick_value=6.25, max_contracts=2)
    # natural: risk=$150 / sl_usd=$125 = 1.2 → round 1 → forzato a 2
    assert res.skip is False
    assert res.contracts == 2
    # real_risk = 2 * 20 * $6.25 = $250
    assert res.real_risk_usd == 250.0
    _ok("MIN floor: 6J sl=20 → 2 ct (worst-case $250)")


def test_min_contracts_floor_does_not_touch_mnq():
    """MNQ non è in MIN_CONTRACTS_PER_TRADE → nessun floor.

    Caso costruito: balance basso così sizing naturale produrrebbe 1 ct.
    Verifica che il floor floor NON intervenga.
    """
    # MNQ worst-case sizing forza sl_ticks=48 → sl_usd=$24. Con balance=8k
    # risk=$24 → target=1.0 → 1 ct. MIN_CONTRACTS non ha MNQ → resta 1 ct.
    res = _compute(
        "MNQ", sl_ticks=6, tick_value=0.50, max_contracts=12,
        balance=8_000.0,
    )
    assert res.skip is False
    assert res.contracts == 1, "MNQ non deve essere toccato dal MIN floor FX"
    _ok("MIN floor: MNQ low balance → 1 ct (non impattato)")


# ============================================================
# RUN
# ============================================================

def main() -> int:
    print("test_sizing.py")
    # Primitives backfill
    test_budget_pre_check_ok()
    test_budget_pre_check_hard_skip()
    test_budget_pre_check_invalid_inputs()
    test_compute_contracts_base()
    test_compute_contracts_size_too_small()
    test_compute_contracts_invalid_sl_zero()
    # Conversion helpers
    test_points_to_ticks_mes()
    test_points_to_dollars_mes()
    test_points_to_dollars_unknown_symbol_raises()
    test_points_to_ticks_zero_raises()
    # size_for_entry
    test_size_for_entry_mes_base_case()
    test_size_for_entry_clamp_inactive_when_target_fits()
    test_size_for_entry_size_too_small_skip()
    test_size_for_entry_risk_exceeds_budget()
    test_size_for_entry_6j_dollar_correctness()
    test_size_for_entry_6j_executable_size()
    test_size_for_entry_sl_distance_zero_raises()
    test_size_for_entry_unknown_symbol_raises()
    test_size_for_entry_balance_zero_raises()
    test_size_for_entry_reads_risk_multiplier_from_metadata()
    # Worst-case sizing floor
    test_worst_case_mnq_tight_sl_to_6ct()
    test_worst_case_mnq_already_max_sl()
    test_worst_case_mes_tight_sl_to_3ct()
    test_worst_case_mym_tight_sl_to_6ct()
    test_worst_case_mgc_tight_sl_to_2ct()
    test_worst_case_mgc_already_max_sl()
    test_non_worst_case_6e_floor_min_sl()
    test_non_worst_case_6e_above_min_unchanged()
    test_no_floor_entry_unchanged()
    # MIN_CONTRACTS floor (V18)
    test_min_contracts_floor_6a()
    test_min_contracts_floor_6j()
    test_min_contracts_floor_does_not_touch_mnq()
    print("ALL 32 TESTS PASSED")
    return 0


if __name__ == "__main__":
    sys.exit(main())
