from dataclasses import replace
from decimal import Decimal

import pytest

from binance_quant.futures_models import (
    FuturesAction,
    FuturesPosition,
    FuturesPrediction,
    FuturesSymbolMetadata,
)
from binance_quant.futures_risk import (
    FuturesRiskContext,
    FuturesRiskEngine,
    FuturesRiskSettings,
)


def metadata(min_notional: Decimal = Decimal("10")):
    return FuturesSymbolMetadata(
        "BTCUSDT",
        "BTC",
        "USDT",
        min_qty=Decimal("0.001"),
        step_size=Decimal("0.001"),
        tick_size=Decimal("0.1"),
        min_notional=min_notional,
    )


def flat_position():
    return FuturesPosition(
        "BTCUSDT",
        Decimal("0"),
        Decimal("0"),
        Decimal("60000"),
        Decimal("0"),
        Decimal("0"),
        Decimal("0"),
        2,
        "isolated",
    )


def long_position():
    return FuturesPosition(
        "BTCUSDT",
        Decimal("0.002"),
        Decimal("60000"),
        Decimal("60100"),
        Decimal("55000"),
        Decimal("5"),
        Decimal("0.2"),
        2,
        "isolated",
    )


def settings():
    return FuturesRiskSettings(
        margin_amount=Decimal("5"),
        default_leverage=2,
        max_leverage=3,
        max_total_margin=Decimal("20"),
        min_confidence=Decimal("0.70"),
        daily_realized_loss_limit=Decimal("10"),
        max_margin_loss_percent=Decimal("50"),
        min_liquidation_buffer_percent=Decimal("2"),
        min_stop_distance_percent=Decimal("0.3"),
        max_stop_distance_percent=Decimal("5"),
        min_take_profit_distance_percent=Decimal("0.3"),
        max_take_profit_distance_percent=Decimal("10"),
    )


def evaluate_without_raising(context, engine=None):
    try:
        return (engine or FuturesRiskEngine(settings())).evaluate(context)
    except Exception as exc:
        pytest.fail(f"evaluate raised {exc.__class__.__name__}")


def test_risk_approves_open_long_and_clips_leverage():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=10,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    context = FuturesRiskContext(
        prediction=prediction,
        metadata=metadata(),
        position=flat_position(),
        mark_price=Decimal("60000"),
        total_isolated_margin=Decimal("5"),
        daily_realized_pnl=Decimal("0"),
        hedge_mode=False,
    )

    decision = FuturesRiskEngine(settings()).evaluate(context)

    assert decision.approved
    assert decision.reason == "approved"
    assert decision.leverage == 3
    assert decision.notional == Decimal("15")
    assert decision.margin_amount == Decimal("5")


def test_risk_uses_default_leverage_when_prediction_leverage_is_none():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved
    assert decision.leverage == 2
    assert decision.notional == Decimal("10")


def test_risk_rejects_hedge_mode():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            True,
        )
    )

    assert decision.approved is False
    assert decision.reason == "hedge mode active"


def test_risk_rejects_low_confidence():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.69"),
        "weak",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "confidence below minimum"


def test_risk_rejects_open_when_daily_loss_limit_reached_but_allows_close():
    engine = FuturesRiskEngine(settings())
    open_prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    close_prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.CLOSE,
        Decimal("0.82"),
        "risk",
        close_percent=50,
    )

    open_decision = engine.evaluate(
        FuturesRiskContext(
            open_prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("-10"),
            False,
        )
    )
    close_decision = engine.evaluate(
        FuturesRiskContext(
            close_prediction,
            metadata(),
            long_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("-10"),
            False,
        )
    )

    assert open_decision.approved is False
    assert open_decision.reason == "daily net loss limit reached"
    assert close_decision.approved is True


def test_risk_rejects_no_position_close():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.CLOSE,
        Decimal("0.82"),
        "risk",
        close_percent=50,
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "no position to close"


def test_risk_rejects_invalid_close_percent():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.CLOSE,
        Decimal("0.82"),
        "risk",
        close_percent=30,
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            long_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "invalid close percent"


def test_risk_rejects_same_direction_add_and_opposite_open():
    engine = FuturesRiskEngine(settings())
    open_long = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    open_short = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_SHORT,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("61000"),
        take_profit_price=Decimal("57000"),
    )

    same = engine.evaluate(
        FuturesRiskContext(
            open_long,
            metadata(),
            long_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )
    opposite = engine.evaluate(
        FuturesRiskContext(
            open_short,
            metadata(),
            long_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert same.reason == "same-direction position exists"
    assert opposite.reason == "opposite position requires close first"


def test_risk_rejects_invalid_stop_take_profit_direction():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("61000"),
        take_profit_price=Decimal("63000"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "invalid stop or take-profit direction"


def test_risk_rejects_missing_protection_prices():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "missing protection prices"


def test_risk_rejects_margin_cap():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("16"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "total margin above max"


def test_risk_rejects_stop_distance_outside_limits():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59900"),
        take_profit_price=Decimal("63000"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "stop distance outside limits"


def test_risk_rejects_take_profit_distance_outside_limits():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("60100"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "take-profit distance outside limits"


def test_risk_rejects_stop_loss_above_margin_risk_budget():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=15,
        stop_loss_price=Decimal("57000"),
        take_profit_price=Decimal("63000"),
    )
    risk_settings = replace(settings(), max_leverage=15)

    decision = FuturesRiskEngine(risk_settings).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("0"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "estimated stop loss above margin risk budget"


def test_risk_rejects_open_when_notional_below_min_notional():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "trend",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(min_notional=Decimal("100")),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "notional below min notional"


def test_risk_rejects_non_finite_confidence_without_raising():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("NaN"),
        "bad input",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )

    decision = evaluate_without_raising(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "invalid risk input"


def test_risk_rejects_non_finite_mark_price_without_raising():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "bad input",
        leverage=2,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )

    decision = evaluate_without_raising(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("NaN"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "invalid risk input"


def test_risk_rejects_non_finite_protection_price_without_raising():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "bad input",
        leverage=2,
        stop_loss_price=Decimal("NaN"),
        take_profit_price=Decimal("63000"),
    )

    decision = evaluate_without_raising(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "invalid risk input"


@pytest.mark.parametrize("leverage", [0, -1])
def test_risk_rejects_zero_or_negative_leverage(leverage):
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "bad input",
        leverage=leverage,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )

    decision = FuturesRiskEngine(settings()).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("5"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "invalid risk input"


def test_risk_rejects_open_when_estimated_liquidation_buffer_is_too_small():
    prediction = FuturesPrediction(
        "BTCUSDT",
        FuturesAction.OPEN_LONG,
        Decimal("0.82"),
        "high leverage",
        leverage=50,
        stop_loss_price=Decimal("59000"),
        take_profit_price=Decimal("63000"),
    )
    risky_settings = replace(
        settings(),
        max_leverage=50,
        min_liquidation_buffer_percent=Decimal("3"),
    )

    decision = FuturesRiskEngine(risky_settings).evaluate(
        FuturesRiskContext(
            prediction,
            metadata(),
            flat_position(),
            Decimal("60000"),
            Decimal("0"),
            Decimal("0"),
            False,
        )
    )

    assert decision.approved is False
    assert decision.reason == "estimated liquidation buffer below minimum"
