"""Unit Tests for Independent Risk Engine & Capital Protection."""
from __future__ import annotations

import unittest
from decimal import Decimal

from backend.app.risk.circuit_breakers import CircuitBreakerManager
from backend.app.risk.engine import RiskEngine
from backend.app.risk.kill_switch import KillSwitch
from backend.app.risk.sizer import PositionSizer
from backend.app.schemas.risk import AccountState, InstrumentFilters
from backend.app.schemas.signal import MarketRegime, SignalAction, SignalCandidate


class TestRiskEngine(unittest.TestCase):
    def setUp(self) -> None:
        self.risk_engine = RiskEngine()
        self.sizer = PositionSizer(risk_pct=0.0025, taker_fee_pct=0.00055, slippage_pct=0.00020, uncertainty_pct=0.00030)
        self.cb = CircuitBreakerManager(max_daily_loss_pct=0.010, max_drawdown_pct=0.050, max_positions=2)
        self.filters = InstrumentFilters(
            symbol="BTCUSDT",
            min_order_qty=Decimal("0.001"),
            qty_step=Decimal("0.001"),
            min_notional=Decimal("5.0"),
            tick_size=Decimal("0.1"),
            max_leverage=Decimal("1.0"),
        )

    def test_position_sizing_fixed_risk(self) -> None:
        """Verify position sizing respects exactly 0.25% risk budget."""
        equity = Decimal("10000.00")
        entry = Decimal("60000.00")
        stop = Decimal("59000.00")  # $1000 stop distance
        result = self.sizer.calculate_size(equity, entry, stop, self.filters)

        self.assertTrue(result.approved)
        # Expected risk budget: 10000 * 0.0025 = $25.00
        self.assertEqual(result.risk_budget, Decimal("25.0000"))
        # Effective loss per unit ~ 1000 + friction (60000 * 0.0016 = 96) -> 1096
        # Expected quantity ~ 25 / 1096 = 0.022 BTC
        self.assertGreater(result.quantity, Decimal("0"))
        self.assertLessEqual(result.quantity * result.effective_loss_per_unit, result.risk_budget)

    def test_small_account_protection_rejects_sub_minimum_order(self) -> None:
        """Verify small account ($50) trade is rejected when calculated size < exchange min notional ($5)."""
        equity = Decimal("50.00")  # $50 balance
        entry = Decimal("60000.00")
        stop = Decimal("59000.00")
        result = self.sizer.calculate_size(equity, entry, stop, self.filters)

        # Risk budget = 50 * 0.0025 = $0.125
        # Required size ~ 0.125 / 1000 = 0.0001 BTC < min_order_qty 0.001
        self.assertFalse(result.approved)
        self.assertIn("Small Account Protection", result.rejection_reason)

    def test_daily_loss_circuit_breaker(self) -> None:
        """Verify daily loss >= 1.0% trips circuit breaker."""
        account = AccountState(
            equity=Decimal("9880.00"),
            available_balance=Decimal("9880.00"),
            starting_day_equity=Decimal("10000.00"),
            peak_equity=Decimal("10000.00"),
            open_positions_count=0,
            current_leverage=Decimal("1.0"),
            realized_daily_pnl=Decimal("-120.00"),  # 1.2% daily loss
            unrealized_daily_pnl=Decimal("0.00"),
        )
        status = self.cb.check_portfolio_limits(account)
        self.assertTrue(status.tripped)
        self.assertIn("Daily loss circuit breaker tripped", status.reason)

    def test_max_drawdown_circuit_breaker(self) -> None:
        """Verify portfolio drawdown >= 5.0% trips circuit breaker."""
        account = AccountState(
            equity=Decimal("9400.00"),
            available_balance=Decimal("9400.00"),
            starting_day_equity=Decimal("10000.00"),
            peak_equity=Decimal("10000.00"),  # 6% drawdown from peak
            open_positions_count=0,
            current_leverage=Decimal("1.0"),
            realized_daily_pnl=Decimal("0.00"),
            unrealized_daily_pnl=Decimal("0.00"),
        )
        status = self.cb.check_portfolio_limits(account)
        self.assertTrue(status.tripped)
        self.assertIn("Maximum drawdown circuit breaker tripped", status.reason)

    def test_max_simultaneous_positions_limit(self) -> None:
        """Verify cannot exceed 2 simultaneous positions."""
        account = AccountState(
            equity=Decimal("10000.00"),
            available_balance=Decimal("10000.00"),
            starting_day_equity=Decimal("10000.00"),
            peak_equity=Decimal("10000.00"),
            open_positions_count=2,  # Already at limit
            current_leverage=Decimal("1.0"),
            realized_daily_pnl=Decimal("0.00"),
            unrealized_daily_pnl=Decimal("0.00"),
        )
        status = self.cb.check_portfolio_limits(account)
        self.assertTrue(status.tripped)
        self.assertIn("Maximum simultaneous positions limit reached", status.reason)

    def test_kill_switch_persistence_and_veto(self) -> None:
        """Verify kill switch persists to database and rejects proposed orders."""
        ks = KillSwitch()
        ks.trigger("Manual safety drill test", "TEST_SUITE")
        self.assertTrue(ks.is_active())

        # Propose valid signal
        signal = SignalCandidate(
            symbol="BTCUSDT",
            timeframe="15m",
            action=SignalAction.LONG,
            raw_probability=0.70,
            calibrated_probability=0.70,
            expected_return=0.03,
            expected_risk=0.015,
            expected_value=0.005,
            entry_price=Decimal("60000.00"),
            stop_loss_price=Decimal("59000.00"),
            take_profit_price=Decimal("62000.00"),
            regime=MarketRegime.TRENDING_BULL,
            reason="Strong trend",
            model_version="test_v1",
            timestamp="2026-10-08T00:00:00Z",
        )
        account = AccountState(
            equity=Decimal("10000.00"),
            available_balance=Decimal("10000.00"),
            starting_day_equity=Decimal("10000.00"),
            peak_equity=Decimal("10000.00"),
            open_positions_count=0,
            current_leverage=Decimal("1.0"),
            realized_daily_pnl=Decimal("0.00"),
            unrealized_daily_pnl=Decimal("0.00"),
        )

        decision = self.risk_engine.evaluate_signal(signal, account, self.filters)
        self.assertFalse(decision.approved)
        self.assertIn("Kill Switch", decision.rejection_reason)

        # Reset kill switch
        ks.reset("TEST_ADMIN")
        self.assertFalse(ks.is_active())


if __name__ == "__main__":
    unittest.main()
