"""
Unit tests for trading/tp_resolver.py.

Covers:
  - Basic BUY/SELL math (rr_multiplier × sl_ticks × tick_size).
  - MIN_TP_TICKS floor.
  - Hard clamp on rr_multiplier ([RR_MULT_MIN, RR_MULT_MAX]).
  - "contracts cancels" property (algebraic invariant).
  - Validation raises on bad inputs.
  - Asset-class smoke (FX, equity index, metal, energy).

Run via pytest:
    pytest tests/test_tp_resolver.py
Or standalone (parity with other test_*.py runners):
    python3 tests/test_tp_resolver.py
"""

import sys
from pathlib import Path

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

import pytest

from core import config_futures as cfg_fut
from trading.tp_resolver import (
    MAX_RR_EFFECTIVE,
    RR_MULT_MAX,
    RR_MULT_MIN,
    TPResolution,
    resolve_tp_price,
)


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


# ============================================================
# BASIC MATH
# ============================================================

def test_basic_buy_mes():
    """MES: tick_size=0.25, tick_value=1.25, sl_ticks=20, rr=0.50.
       tp_ticks = round(0.50 * 20) = 10; tp_distance = 10 * 0.25 = 2.5;
       tp_price = 5800 + 2.5 = 5802.50."""
    res = resolve_tp_price(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.50,
        contracts=2, sl_ticks=20,
    )
    assert isinstance(res, TPResolution)
    assert res.tp_ticks_final == 10
    assert res.tp_price == 5802.50
    assert res.tp_distance == pytest.approx(2.5)
    # contracts × sl_ticks × tick_value = 2 × 20 × 1.25 = 50.0
    assert res.sl_usd_actual == pytest.approx(50.0)
    assert res.tp_usd_target == pytest.approx(25.0)
    assert res.rr_effective == pytest.approx(0.5)
    assert res.min_tp_ticks_applied is False


def test_basic_sell_mes():
    res = resolve_tp_price(
        symbol="MES", direction="SELL",
        entry_price=5800.0, rr_multiplier=0.50,
        contracts=2, sl_ticks=20,
    )
    assert res.tp_price == 5797.50  # SELL: entry - tp_distance


# ============================================================
# MIN_TP_TICKS FLOOR
# ============================================================

def test_min_tp_ticks_lifts_short_tp():
    """MES MIN_TP_TICKS=4. With sl_ticks=8, rr=0.20 -> raw=2 < floor 4."""
    res = resolve_tp_price(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.20,
        contracts=1, sl_ticks=8,
    )
    assert res.min_tp_ticks_applied is True
    assert res.tp_ticks_final == 4
    # rr_effective = 4/8 = 0.5 (lifted above input rr)
    assert res.rr_effective == pytest.approx(0.5)


def test_min_tp_ticks_inactive_when_above():
    """sl_ticks=20, rr=0.50 -> raw=10 > MES floor 4. Floor not active."""
    res = resolve_tp_price(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.50,
        contracts=1, sl_ticks=20,
    )
    assert res.min_tp_ticks_applied is False
    assert res.tp_ticks_final == 10


# ============================================================
# HARD CLAMP
# ============================================================

def test_hard_clamp_upper():
    """rr=0.95 -> clamped to 0.80."""
    res = resolve_tp_price(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.95,
        contracts=1, sl_ticks=20,
    )
    assert res.rr_multiplier_used == RR_MULT_MAX
    assert res.tp_ticks_final == round(RR_MULT_MAX * 20)


def test_hard_clamp_lower():
    """rr=0.05 -> clamped to 0.10."""
    res = resolve_tp_price(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.05,
        contracts=1, sl_ticks=20,
    )
    assert res.rr_multiplier_used == RR_MULT_MIN
    # rr_used=0.10, sl_ticks=20 -> raw=2; MIN_TP_TICKS=4 wins
    assert res.tp_ticks_final == 4


# ============================================================
# CONTRACTS CANCELS (algebraic invariant)
# ============================================================

def test_contracts_does_not_affect_tp_price():
    """tp_distance = rr × sl_ticks × tick_size  ⇒  contracts cancels."""
    base = resolve_tp_price(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.50,
        contracts=1, sl_ticks=20,
    )
    bigger = resolve_tp_price(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.50,
        contracts=10, sl_ticks=20,
    )
    assert base.tp_price == bigger.tp_price
    assert base.tp_ticks_final == bigger.tp_ticks_final
    # But dollar-domain values DO scale with contracts:
    assert bigger.sl_usd_actual == 10 * base.sl_usd_actual
    assert bigger.tp_usd_target == 10 * base.tp_usd_target


# ============================================================
# VALIDATION
# ============================================================

@pytest.mark.parametrize("kwargs", [
    {"entry_price": 0},
    {"entry_price": -1},
    {"rr_multiplier": 0},
    {"rr_multiplier": -0.5},
    {"sl_ticks": 0},
    {"contracts": 0},
])
def test_raises_on_bad_inputs(kwargs):
    base = dict(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.50,
        contracts=1, sl_ticks=20,
    )
    base.update(kwargs)
    with pytest.raises(ValueError):
        resolve_tp_price(**base)


def test_raises_on_unknown_direction():
    with pytest.raises(ValueError):
        resolve_tp_price(
            symbol="MES", direction="LONG",
            entry_price=5800.0, rr_multiplier=0.50,
            contracts=1, sl_ticks=20,
        )


def test_raises_on_unknown_symbol():
    with pytest.raises(KeyError):
        resolve_tp_price(
            symbol="ZZZ_NONEXISTENT", direction="BUY",
            entry_price=5800.0, rr_multiplier=0.50,
            contracts=1, sl_ticks=20,
        )


# ============================================================
# ASSET CLASS SMOKE
# ============================================================

_ASSET_SMOKE_CASES = [
    ("MES",  5800.0,    20),     # equity index
    ("MNQ",  21000.0,   16),     # equity index
    ("MGC",  2400.0,    30),     # metal
    ("MCL",  75.0,      40),     # energy
    ("6E",   1.0850,    20),     # FX standard
    ("6J",   0.0066500, 30),     # FX inverted, digits=7
]


@pytest.mark.parametrize("symbol,entry,sl_ticks", _ASSET_SMOKE_CASES)
def test_asset_class_smoke(symbol, entry, sl_ticks):
    res = resolve_tp_price(
        symbol=symbol, direction="BUY",
        entry_price=entry, rr_multiplier=0.50,
        contracts=1, sl_ticks=sl_ticks,
    )
    assert res.tp_price > entry
    assert res.tp_ticks_final >= cfg_fut.MIN_TP_TICKS[symbol]
    assert res.sl_usd_actual > 0
    assert res.tp_usd_target > 0


# ============================================================
# MAX_RR_EFFECTIVE cap (V18 12-mag)
# ============================================================

def test_ai_suggested_rr_capped_at_max():
    """MYM live incident: sl_ticks=13, tp_suggested 138 ticks away → cap a 2× SL.

    Senza cap: tp_ticks=round(138*0.85)=117, rr_effective=9.0x.
    Con cap=2.0: tp_ticks=int(2.0*13)=26, rr_effective=2.0, rr_capped=True.
    """
    res = resolve_tp_price(
        symbol="MYM", direction="BUY",
        entry_price=49759.0, rr_multiplier=0.50,    # ignored (ai path wins)
        contracts=6, sl_ticks=13,
        tp_price_suggested=49897.0,                 # 138 ticks away
    )
    assert res.tp_source == "ai_suggested"
    assert res.rr_capped is True
    assert res.tp_ticks_final == int(MAX_RR_EFFECTIVE * 13)
    assert res.tp_ticks_final == 26
    assert res.rr_effective == pytest.approx(2.0)
    # TP price reflects the capped distance, NOT the VWAP target.
    assert res.tp_price == 49759.0 + 26 * 1.0       # MYM tick_size=1.0
    _ok("ai_suggested cap: MYM sl=13 tp_suggested=138 → tp=26, rr=2.0")


def test_ai_suggested_no_cap_when_below_max():
    """tp_suggested entro 2× SL → cap non engage."""
    # MES: sl_ticks=20, tp_suggested 30 ticks away. rr_raw=30/20=1.5 < 2.0.
    # After 15% margin: 0.85 * 30 = 25.5 → 26 ticks → rr=1.30 < 2.0.
    res = resolve_tp_price(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.50,
        contracts=2, sl_ticks=20,
        tp_price_suggested=5800.0 + 30 * 0.25,
    )
    assert res.tp_source == "ai_suggested"
    assert res.rr_capped is False
    assert res.tp_ticks_final == 26                 # post-margin, pre-cap
    assert res.rr_effective == pytest.approx(1.30)


def test_rr_fallback_cap_engages_only_when_min_tp_lifts_above_cap():
    """rr_fallback: hard-clamp [0.10, 0.80] limita rr_used, ma MIN_TP_TICKS
    può alzare il tp_ticks finale oltre 2× SL su SL strettissimi.

    MNQ: MIN_TP=8, sl_ticks=3, rr=0.10. tp_raw=0.3→0; MIN_TP_TICKS lifts→8.
    cap = 2*3 = 6. MIN_TP=8 > cap → MIN_TP wins (tp_ticks=8), rr_capped=True.
    """
    res = resolve_tp_price(
        symbol="MNQ", direction="BUY",
        entry_price=18000.0, rr_multiplier=0.10,
        contracts=1, sl_ticks=3,
    )
    assert res.tp_source == "rr_fallback"
    assert res.rr_capped is True
    assert res.min_tp_ticks_applied is True
    # MIN_TP_TICKS (8) ha priorità sul cap (6) — costi round-trip hard floor.
    assert res.tp_ticks_final == 8
    assert res.rr_effective > MAX_RR_EFFECTIVE
    _ok("rr_fallback cap: MIN_TP wins on extreme tight SL, rr_capped=True")


def test_rr_fallback_normal_path_unchanged_no_cap():
    """rr_fallback normale: hard-clamp 0.80 << cap 2.0 → no rr_capped."""
    res = resolve_tp_price(
        symbol="MES", direction="BUY",
        entry_price=5800.0, rr_multiplier=0.50,
        contracts=2, sl_ticks=20,
    )
    assert res.tp_source == "rr_fallback"
    assert res.rr_capped is False


def test_ai_suggested_sell_cap():
    """Cap simmetrico per SELL: tp_suggested 100 ticks sotto entry, sl=10."""
    # MES: sl_ticks=10, tp_suggested 100 ticks sotto. Post 15% margin: 85.
    # cap = 2*10 = 20. 85 > 20 → capped to 20.
    res = resolve_tp_price(
        symbol="MES", direction="SELL",
        entry_price=5800.0, rr_multiplier=0.50,
        contracts=2, sl_ticks=10,
        tp_price_suggested=5800.0 - 100 * 0.25,
    )
    assert res.tp_source == "ai_suggested"
    assert res.rr_capped is True
    assert res.tp_ticks_final == 20
    assert res.rr_effective == pytest.approx(2.0)
    assert res.tp_price == 5800.0 - 20 * 0.25
    _ok("ai_suggested SELL cap: rr_effective=2.0")


# ============================================================
# RUN (standalone runner — parity with other tests/test_*.py)
# ============================================================

_BAD_INPUT_CASES = [
    {"entry_price": 0},
    {"entry_price": -1},
    {"rr_multiplier": 0},
    {"rr_multiplier": -0.5},
    {"sl_ticks": 0},
    {"contracts": 0},
]


def main() -> int:
    print("test_tp_resolver.py")
    test_basic_buy_mes(); _ok("basic BUY MES math")
    test_basic_sell_mes(); _ok("basic SELL MES math")
    test_min_tp_ticks_lifts_short_tp(); _ok("MIN_TP_TICKS lifts short TP")
    test_min_tp_ticks_inactive_when_above(); _ok("MIN_TP_TICKS inactive when above")
    test_hard_clamp_upper(); _ok("hard clamp upper bound")
    test_hard_clamp_lower(); _ok("hard clamp lower bound")
    test_contracts_does_not_affect_tp_price(); _ok("contracts cancels (algebraic)")
    for kwargs in _BAD_INPUT_CASES:
        test_raises_on_bad_inputs(kwargs)
    _ok("validation raises on bad inputs")
    test_raises_on_unknown_direction(); _ok("raises on unknown direction")
    test_raises_on_unknown_symbol(); _ok("raises on unknown symbol")
    for symbol, entry, sl_ticks in _ASSET_SMOKE_CASES:
        test_asset_class_smoke(symbol, entry, sl_ticks)
        _ok(f"asset smoke: {symbol}")
    # V18 MAX_RR_EFFECTIVE cap
    test_ai_suggested_rr_capped_at_max()
    test_ai_suggested_no_cap_when_below_max()
    test_rr_fallback_cap_engages_only_when_min_tp_lifts_above_cap()
    test_rr_fallback_normal_path_unchanged_no_cap()
    test_ai_suggested_sell_cap()
    print("ALL TESTS PASSED")
    return 0


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