"""Tests: circuit breaker, regime detection, position management, earnings
blackout, backtest cost modeling — the audit-driven safety wave."""
from datetime import date, datetime, timedelta
from types import SimpleNamespace

from app.agents.regime import RegimeAgent
from app.agents.risk_manager import RiskManagerAgent
from app.services import position_mgmt
from app.services.backtest import run_backtest
from app.services.market_data import MockMarketDataProvider, bars_are_fresh

provider = MockMarketDataProvider()


# ── Stale-data circuit breaker (audit B-3) ─────────────────
def test_fresh_bars_pass():
    bars = provider.get_history("TSLA", 90)
    assert bars_are_fresh(bars) is True


def test_stale_bars_tripped():
    old = [{"date": (date.today() - timedelta(days=30)).isoformat(),
            "open": 1, "high": 1, "low": 1, "close": 1, "volume": 1}]
    assert bars_are_fresh(old) is False
    assert bars_are_fresh([]) is False
    assert bars_are_fresh([{"open": 1}]) is False  # malformed = stale


# ── Regime detection ───────────────────────────────────────
def test_regime_classifies():
    result = RegimeAgent().run({"history": provider.get_history("SPY", 90)})
    assert result.data["regime"] in ("bull", "bear", "chop", "unknown")
    assert result.data["risk_scale"] in (0.5, 1.0)


def test_high_vol_halves_position_size():
    # Low-priced quote so the 20% notional cap never binds — isolates the
    # risk_scale effect (at high prices the notional cap correctly limits
    # the normal position first, so the ratio wouldn't be exactly half).
    base = {"quote": {"price": 50.0}, "direction": "CALL", "atr": 5.0,
            "confidence": 80.0, "probability": 60.0, "open_risk_usd": 0.0}
    normal = RiskManagerAgent().run({**base, "risk_scale": 1.0}).data
    halved = RiskManagerAgent().run({**base, "risk_scale": 0.5}).data
    assert halved["max_risk_usd"] <= normal["max_risk_usd"] * 0.55
    assert halved["position_size"] <= normal["position_size"] * 0.55


# ── Earnings blackout ──────────────────────────────────────
def test_earnings_blackout_rejects():
    quote = provider.get_quote("TSLA")
    r = RiskManagerAgent().run({
        "quote": quote, "direction": "CALL", "atr": 5.0,
        "confidence": 90.0, "probability": 70.0, "open_risk_usd": 0.0,
        "days_to_earnings": 2,
    })
    assert r.data["approved"] is False
    assert any("Earnings" in reason for reason in r.data["rejection_reasons"])


def test_no_earnings_data_no_rejection():
    quote = provider.get_quote("TSLA")
    r = RiskManagerAgent().run({
        "quote": quote, "direction": "CALL", "atr": 5.0,
        "confidence": 90.0, "probability": 70.0, "open_risk_usd": 0.0,
        "days_to_earnings": None,
    })
    assert not any("Earnings" in reason for reason in r.data["rejection_reasons"])


# ── Position management ────────────────────────────────────
def _trade(**overrides):
    base = dict(direction="BUY", entry_price=100.0, stop_loss=90.0,
                initial_stop=90.0, quantity=10, scaled_out=0,
                opened_at=datetime.utcnow())
    base.update(overrides)
    return SimpleNamespace(**base)


def test_breakeven_trail_at_1r():
    t = _trade()
    actions = position_mgmt.evaluate(t, price=110.0, now=datetime.utcnow())  # +1R
    assert actions["new_stop"] == 100.0


def test_no_trail_before_1r():
    t = _trade()
    actions = position_mgmt.evaluate(t, price=105.0, now=datetime.utcnow())  # +0.5R
    assert actions["new_stop"] is None
    assert actions["scale_out_qty"] == 0


def test_scale_out_at_1_5r():
    t = _trade()
    actions = position_mgmt.evaluate(t, price=115.0, now=datetime.utcnow())  # +1.5R
    assert actions["scale_out_qty"] == 5


def test_scale_out_only_once():
    t = _trade(scaled_out=1)
    actions = position_mgmt.evaluate(t, price=118.0, now=datetime.utcnow())
    assert actions["scale_out_qty"] == 0


def test_time_stop():
    t = _trade(opened_at=datetime.utcnow() - timedelta(days=11))
    actions = position_mgmt.evaluate(t, price=101.0, now=datetime.utcnow())
    assert actions["time_exit"] is True


def test_short_side_trail():
    t = _trade(direction="SELL", entry_price=100.0, stop_loss=110.0, initial_stop=110.0)
    actions = position_mgmt.evaluate(t, price=90.0, now=datetime.utcnow())  # +1R short
    assert actions["new_stop"] == 100.0


# ── Backtest cost modeling (audit H-1) ─────────────────────
def test_commissions_strictly_reduce_results():
    # Commissions-only comparison: identical entries/exits, so the trade set
    # is unchanged and every trade's R is strictly lower. (Slippage shifts
    # entry/stop/target geometry and changes WHICH trades occur, so it is
    # not monotonic on small samples — modeled, but not asserted monotonic.)
    free = run_backtest(symbols=["TSLA", "NVDA"], days=180, min_score=55,
                        slippage_bps=0, commission_per_share=0)
    with_commissions = run_backtest(symbols=["TSLA", "NVDA"], days=180, min_score=55,
                                    slippage_bps=0, commission_per_share=0.05)
    if free["overall"]["trades"] > 0:
        assert with_commissions["overall"]["trades"] == free["overall"]["trades"]
        assert with_commissions["overall"]["total_r"] < free["overall"]["total_r"]


def test_slippage_is_modeled():
    result = run_backtest(symbols=["TSLA"], days=180, min_score=55,
                          slippage_bps=20, commission_per_share=0.01)
    assert "slippage" in result["note"]
    assert result["params"]["slippage_bps"] == 20
