from decimal import Decimal

import pytest

from binance_quant.models import (
    AccountSnapshot,
    Balance,
    OrderSide,
    Signal,
    SignalType,
    SymbolMetadata,
    TradingMode,
)
from binance_quant.risk import RiskEngine, RiskSettings


METADATA = SymbolMetadata(
    symbol="BTCUSDT",
    base_asset="BTC",
    quote_asset="USDT",
    min_notional=Decimal("5"),
    min_qty=Decimal("0.00001"),
    step_size=Decimal("0.00001"),
)


def account(usdt: str = "100", btc: str = "0.1") -> AccountSnapshot:
    return AccountSnapshot(
        {
            "USDT": Balance("USDT", Decimal(usdt)),
            "BTC": Balance("BTC", Decimal(btc)),
        }
    )


def buy_signal() -> Signal:
    return Signal("BTCUSDT", SignalType.BUY, OrderSide.BUY, "test buy")


def sell_signal() -> Signal:
    return Signal("BTCUSDT", SignalType.SELL, OrderSide.SELL, "test sell")


def test_rejects_unsupported_symbol():
    engine = RiskEngine(RiskSettings())

    decision = engine.evaluate(
        Signal("ETHUSDT", SignalType.BUY, OrderSide.BUY, "test buy"),
        METADATA,
        account(),
        Decimal("10"),
        now_ms=1,
    )

    assert not decision.approved
    assert decision.reason == "unsupported symbol"


def test_rejects_hold_signal():
    engine = RiskEngine(RiskSettings())

    decision = engine.evaluate(
        Signal("BTCUSDT", SignalType.HOLD, None, "no crossover"),
        METADATA,
        account(),
        quote_amount=Decimal("10"),
        now_ms=1,
    )

    assert not decision.approved
    assert decision.reason == "hold signal"


def test_approves_valid_buy():
    engine = RiskEngine(RiskSettings())

    decision = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=1)

    assert decision.approved
    assert decision.quote_amount == Decimal("10")


def test_rejects_order_below_min_notional():
    engine = RiskEngine(RiskSettings())

    decision = engine.evaluate(buy_signal(), METADATA, account(), Decimal("1"), now_ms=1)

    assert not decision.approved
    assert decision.reason == "quote amount below min notional"


def test_rejects_quote_amount_above_max():
    engine = RiskEngine(RiskSettings(max_quote_amount=Decimal("10")))

    decision = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10.01"), now_ms=1)

    assert not decision.approved
    assert decision.reason == "quote amount above max"


def test_rejects_buy_when_quote_balance_is_low():
    engine = RiskEngine(RiskSettings())

    decision = engine.evaluate(buy_signal(), METADATA, account(usdt="4"), Decimal("10"), now_ms=1)

    assert not decision.approved
    assert decision.reason == "insufficient quote balance"


def test_rejects_sell_when_base_value_is_low():
    engine = RiskEngine(RiskSettings())

    decision = engine.evaluate(
        sell_signal(),
        METADATA,
        account(btc="0.0001"),
        Decimal("10"),
        now_ms=1,
        last_price=Decimal("50000"),
    )

    assert not decision.approved
    assert decision.reason == "insufficient base balance"


def test_rejects_sell_when_last_price_is_missing():
    engine = RiskEngine(RiskSettings())

    decision = engine.evaluate(sell_signal(), METADATA, account(btc="0.1"), Decimal("10"), now_ms=1)

    assert not decision.approved
    assert decision.reason == "insufficient base balance"


def test_approves_sell_when_base_value_exceeds_quote_amount():
    engine = RiskEngine(RiskSettings())

    decision = engine.evaluate(
        sell_signal(),
        METADATA,
        account(btc="0.001"),
        Decimal("10"),
        now_ms=1,
        last_price=Decimal("11000"),
    )

    assert decision.approved


def test_approves_sell_when_base_value_equals_quote_amount():
    engine = RiskEngine(RiskSettings())

    decision = engine.evaluate(
        sell_signal(),
        METADATA,
        account(btc="0.001"),
        Decimal("10"),
        now_ms=1,
        last_price=Decimal("10000"),
    )

    assert decision.approved


def test_rejects_repeated_same_side_signal():
    engine = RiskEngine(RiskSettings(prevent_repeated_side=True))
    first = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=1)
    second = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=2)

    assert first.approved
    assert not second.approved
    assert second.reason == "duplicate same-side signal"


def test_rejects_during_cooldown():
    engine = RiskEngine(RiskSettings(cooldown_seconds=60, prevent_repeated_side=False))
    first = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=1_000)
    second = engine.evaluate(
        sell_signal(),
        METADATA,
        account(),
        Decimal("10"),
        now_ms=30_000,
        last_price=Decimal("50000"),
    )

    assert first.approved
    assert not second.approved
    assert second.reason == "cooldown active"


def test_rejected_signal_does_not_update_duplicate_side_state():
    engine = RiskEngine(RiskSettings(prevent_repeated_side=True))
    rejected = engine.evaluate(buy_signal(), METADATA, account(), Decimal("1"), now_ms=1)
    approved = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=2)

    assert not rejected.approved
    assert rejected.reason == "quote amount below min notional"
    assert approved.approved


def test_rejected_signal_does_not_start_cooldown():
    engine = RiskEngine(RiskSettings(cooldown_seconds=60, prevent_repeated_side=False))
    rejected = engine.evaluate(buy_signal(), METADATA, account(), Decimal("1"), now_ms=1_000)
    approved = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=30_000)

    assert not rejected.approved
    assert rejected.reason == "quote amount below min notional"
    assert approved.approved


def test_approves_exact_quote_boundaries():
    max_engine = RiskEngine(RiskSettings(max_quote_amount=Decimal("10")))
    min_engine = RiskEngine(RiskSettings())

    max_decision = max_engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=1)
    min_decision = min_engine.evaluate(buy_signal(), METADATA, account(), Decimal("5"), now_ms=1)

    assert max_decision.approved
    assert min_decision.approved


def test_rejects_unknown_mode():
    engine = RiskEngine(RiskSettings(mode="paper"))

    decision = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=1)

    assert not decision.approved
    assert decision.reason == "unsupported trading mode"


def test_accepts_explicit_testnet_live_mode():
    engine = RiskEngine(RiskSettings(mode=TradingMode.TESTNET_LIVE))

    decision = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=1)

    assert decision.approved


def test_accepts_explicit_live_mode():
    engine = RiskEngine(RiskSettings(mode=TradingMode.LIVE))

    decision = engine.evaluate(buy_signal(), METADATA, account(), Decimal("10"), now_ms=1)

    assert decision.approved


def test_rejects_non_positive_max_quote_amount_setting():
    with pytest.raises(ValueError, match="max_quote_amount must be positive"):
        RiskSettings(max_quote_amount=Decimal("0"))


def test_rejects_negative_cooldown_seconds_setting():
    with pytest.raises(ValueError, match="cooldown_seconds must be non-negative"):
        RiskSettings(cooldown_seconds=-1)
