"""TDD for :mod:`cerbero_bite.core.entry_validator`.

Spec: ``docs/01-strategy-rules.md §2,§3.1`` and ``docs/03-algorithms.md §1``.
"""

from __future__ import annotations

from decimal import Decimal

import pytest

from cerbero_bite.config import EntryConfig, StrategyConfig, golden_config
from cerbero_bite.core.entry_validator import (
    EntryContext,
    TrendContext,
    compute_bias,
    validate_entry,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _good_ctx(**overrides: object) -> EntryContext:
    base: dict[str, object] = {
        "capital_usd": Decimal("1500"),
        "dvol_now": Decimal("50"),
        "funding_perp_annualized": Decimal("0.10"),
        "eth_holdings_pct_of_portfolio": Decimal("0.10"),
        "next_macro_event_in_days": None,
        "has_open_position": False,
    }
    base.update(overrides)
    return EntryContext(**base)  # type: ignore[arg-type]


@pytest.fixture
def cfg() -> StrategyConfig:
    return golden_config()


# ---------------------------------------------------------------------------
# validate_entry — happy path
# ---------------------------------------------------------------------------


def test_validate_entry_accepts_clean_context(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(), cfg)
    assert decision.accepted is True
    assert decision.reasons == []


# ---------------------------------------------------------------------------
# validate_entry — single-reason rejections
# ---------------------------------------------------------------------------


def test_open_position_blocks_entry(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(has_open_position=True), cfg)
    assert decision.accepted is False
    assert any("position" in r for r in decision.reasons)


def test_capital_below_minimum_blocks_entry(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(capital_usd=Decimal("719.99")), cfg)
    assert decision.accepted is False
    assert any("capital" in r for r in decision.reasons)


def test_capital_at_minimum_is_accepted(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(capital_usd=Decimal("720")), cfg)
    assert decision.accepted is True


def test_dvol_below_minimum_blocks_entry(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(dvol_now=Decimal("34.99")), cfg)
    assert decision.accepted is False
    assert any("dvol" in r for r in decision.reasons)


def test_dvol_above_maximum_blocks_entry(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(dvol_now=Decimal("90.01")), cfg)
    assert decision.accepted is False
    assert any("dvol" in r for r in decision.reasons)


def test_dvol_at_boundaries_is_accepted(cfg: StrategyConfig) -> None:
    assert validate_entry(_good_ctx(dvol_now=Decimal("35")), cfg).accepted
    assert validate_entry(_good_ctx(dvol_now=Decimal("90")), cfg).accepted


def test_macro_event_within_dte_blocks_entry(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(next_macro_event_in_days=5), cfg)
    assert decision.accepted is False
    assert any("macro" in r for r in decision.reasons)


def test_macro_event_at_dte_boundary_blocks_entry(cfg: StrategyConfig) -> None:
    # next_macro_event_in_days <= dte_target → block. dte_target = 18.
    decision = validate_entry(_good_ctx(next_macro_event_in_days=18), cfg)
    assert decision.accepted is False


def test_macro_event_beyond_dte_is_ok(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(next_macro_event_in_days=19), cfg)
    assert decision.accepted is True


def test_macro_none_is_ok(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(next_macro_event_in_days=None), cfg)
    assert decision.accepted is True


def test_funding_above_abs_cap_blocks(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(funding_perp_annualized=Decimal("0.81")), cfg)
    assert decision.accepted is False
    assert any("funding" in r for r in decision.reasons)


def test_funding_negative_below_neg_cap_blocks(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(funding_perp_annualized=Decimal("-0.81")), cfg)
    assert decision.accepted is False


def test_funding_at_cap_is_accepted(cfg: StrategyConfig) -> None:
    assert validate_entry(_good_ctx(funding_perp_annualized=Decimal("0.80")), cfg).accepted


def test_eth_holdings_above_cap_blocks(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(eth_holdings_pct_of_portfolio=Decimal("0.31")), cfg)
    assert decision.accepted is False
    assert any("holdings" in r for r in decision.reasons)


def test_eth_holdings_at_cap_is_accepted(cfg: StrategyConfig) -> None:
    assert validate_entry(
        _good_ctx(eth_holdings_pct_of_portfolio=Decimal("0.30")), cfg
    ).accepted


# ---------------------------------------------------------------------------
# validate_entry — accumulates ALL failure reasons
# ---------------------------------------------------------------------------


def test_dealer_short_gamma_blocks_entry(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(dealer_net_gamma=Decimal("-5")), cfg)
    assert decision.accepted is False
    assert any("dealer short-gamma" in r for r in decision.reasons)


def test_dealer_long_gamma_passes(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(dealer_net_gamma=Decimal("100")), cfg)
    assert decision.accepted is True


def test_dealer_gamma_none_skips_filter(cfg: StrategyConfig) -> None:
    decision = validate_entry(_good_ctx(dealer_net_gamma=None), cfg)
    assert decision.accepted is True


def test_liquidation_squeeze_high_blocks_entry(cfg: StrategyConfig) -> None:
    decision = validate_entry(
        _good_ctx(liquidation_squeeze_risk_high=True), cfg
    )
    assert decision.accepted is False
    assert any("liquidation squeeze" in r for r in decision.reasons)


def test_liquidation_squeeze_filter_disabled_in_config(
    cfg: StrategyConfig,
) -> None:
    permissive = golden_config(
        entry=EntryConfig(
            **{**cfg.entry.model_dump(), "liquidation_filter_enabled": False}
        )
    )
    decision = validate_entry(
        _good_ctx(liquidation_squeeze_risk_high=True), permissive
    )
    assert decision.accepted is True


def test_dealer_gamma_filter_disabled_in_config(cfg: StrategyConfig) -> None:
    permissive = golden_config(
        entry=EntryConfig(
            **{**cfg.entry.model_dump(), "dealer_gamma_filter_enabled": False}
        )
    )
    decision = validate_entry(
        _good_ctx(dealer_net_gamma=Decimal("-1000")), permissive
    )
    assert decision.accepted is True


# ---------------------------------------------------------------------------
# IV richness gate (§2.9)
# ---------------------------------------------------------------------------


def _strict_iv_rv_cfg(
    cfg: StrategyConfig, *, threshold: Decimal = Decimal("5")
) -> StrategyConfig:
    return golden_config(
        entry=EntryConfig(
            **{
                **cfg.entry.model_dump(),
                "iv_minus_rv_filter_enabled": True,
                "iv_minus_rv_min": threshold,
            }
        )
    )


def test_iv_richness_gate_disabled_by_default_lets_thin_premium_pass(
    cfg: StrategyConfig,
) -> None:
    # Default config: filter disabled. Anche con IV-RV negativa (RV>IV)
    # l'entry deve passare per non rompere setup pre-calibrazione.
    decision = validate_entry(_good_ctx(iv_minus_rv=Decimal("-2")), cfg)
    assert decision.accepted is True


def test_iv_richness_gate_blocks_when_below_floor(cfg: StrategyConfig) -> None:
    strict = _strict_iv_rv_cfg(cfg, threshold=Decimal("5"))
    decision = validate_entry(_good_ctx(iv_minus_rv=Decimal("3")), strict)
    assert decision.accepted is False
    assert any("IV richness" in r for r in decision.reasons)


def test_iv_richness_gate_passes_when_above_floor(cfg: StrategyConfig) -> None:
    strict = _strict_iv_rv_cfg(cfg, threshold=Decimal("5"))
    decision = validate_entry(_good_ctx(iv_minus_rv=Decimal("6")), strict)
    assert decision.accepted is True


def test_iv_richness_gate_passes_at_exact_threshold(cfg: StrategyConfig) -> None:
    # Soglia inclusiva: IV-RV == soglia → accettato (gate è "<", non "<=").
    strict = _strict_iv_rv_cfg(cfg, threshold=Decimal("5"))
    decision = validate_entry(_good_ctx(iv_minus_rv=Decimal("5")), strict)
    assert decision.accepted is True


def test_iv_richness_gate_skipped_when_data_missing(cfg: StrategyConfig) -> None:
    # MCP irraggiungibile: best-effort skip, non bloccare l'entry per
    # un problema di infrastruttura.
    strict = _strict_iv_rv_cfg(cfg, threshold=Decimal("5"))
    decision = validate_entry(_good_ctx(iv_minus_rv=None), strict)
    assert decision.accepted is True


def test_validate_entry_accumulates_all_reasons(cfg: StrategyConfig) -> None:
    decision = validate_entry(
        _good_ctx(
            has_open_position=True,
            capital_usd=Decimal("100"),
            dvol_now=Decimal("10"),
            funding_perp_annualized=Decimal("1.5"),
            eth_holdings_pct_of_portfolio=Decimal("0.9"),
            next_macro_event_in_days=2,
        ),
        cfg,
    )
    assert decision.accepted is False
    assert len(decision.reasons) >= 6


# ---------------------------------------------------------------------------
# compute_bias
# ---------------------------------------------------------------------------


def _trend(
    *,
    eth_now: str = "3000",
    eth_30d_ago: str = "3000",
    funding_cross: str = "0",
    dvol: str = "50",
    adx: str = "25",
) -> TrendContext:
    return TrendContext(
        eth_now=Decimal(eth_now),
        eth_30d_ago=Decimal(eth_30d_ago),
        funding_cross_annualized=Decimal(funding_cross),
        dvol_now=Decimal(dvol),
        adx_14=Decimal(adx),
    )


def test_bias_both_bull_returns_bull_put(cfg: StrategyConfig) -> None:
    # +6% trend, +25% funding
    ctx = _trend(eth_now="3180", eth_30d_ago="3000", funding_cross="0.25")
    assert compute_bias(ctx, cfg) == "bull_put"


def test_bias_both_bear_returns_bear_call(cfg: StrategyConfig) -> None:
    # -6% trend, -25% funding
    ctx = _trend(eth_now="2820", eth_30d_ago="3000", funding_cross="-0.25")
    assert compute_bias(ctx, cfg) == "bear_call"


def test_bias_discordant_returns_none(cfg: StrategyConfig) -> None:
    # bull trend, bear funding
    ctx = _trend(eth_now="3180", eth_30d_ago="3000", funding_cross="-0.25")
    assert compute_bias(ctx, cfg) is None


def test_bias_neutral_with_high_dvol_low_adx_returns_iron_condor(
    cfg: StrategyConfig,
) -> None:
    # 0% trend, 0% funding, dvol 60, adx 15 → IC
    ctx = _trend(
        eth_now="3000", eth_30d_ago="3000", funding_cross="0", dvol="60", adx="15"
    )
    assert compute_bias(ctx, cfg) == "iron_condor"


def test_bias_neutral_with_low_dvol_returns_none(cfg: StrategyConfig) -> None:
    ctx = _trend(
        eth_now="3000", eth_30d_ago="3000", funding_cross="0", dvol="50", adx="15"
    )
    assert compute_bias(ctx, cfg) is None


def test_bias_neutral_with_high_adx_returns_none(cfg: StrategyConfig) -> None:
    ctx = _trend(
        eth_now="3000", eth_30d_ago="3000", funding_cross="0", dvol="60", adx="22"
    )
    assert compute_bias(ctx, cfg) is None


def test_bias_one_neutral_one_bull_returns_none(cfg: StrategyConfig) -> None:
    # +6% trend, +10% funding (neutral)
    ctx = _trend(eth_now="3180", eth_30d_ago="3000", funding_cross="0.10")
    assert compute_bias(ctx, cfg) is None


def test_bias_at_bull_threshold_is_bull_put(cfg: StrategyConfig) -> None:
    # exactly +5% trend, exactly +20% funding
    ctx = _trend(eth_now="3150", eth_30d_ago="3000", funding_cross="0.20")
    assert compute_bias(ctx, cfg) == "bull_put"


def test_bias_at_bear_threshold_is_bear_call(cfg: StrategyConfig) -> None:
    ctx = _trend(eth_now="2850", eth_30d_ago="3000", funding_cross="-0.20")
    assert compute_bias(ctx, cfg) == "bear_call"


def test_bias_zero_division_safe(cfg: StrategyConfig) -> None:
    # eth_30d_ago == 0 must not crash; treat as no-bias (neutral)
    ctx = _trend(eth_now="3000", eth_30d_ago="0", funding_cross="0", dvol="60", adx="15")
    assert compute_bias(ctx, cfg) is None


# ---------------------------------------------------------------------------
# IV-RV adaptive gate
# ---------------------------------------------------------------------------


def _adaptive_cfg(**entry_overrides: object) -> StrategyConfig:
    """Golden config con gate adattivo abilitato di default per test."""
    base_entry: dict[str, object] = {
        "iv_minus_rv_filter_enabled": True,
        "iv_minus_rv_adaptive_enabled": True,
        "iv_minus_rv_min": Decimal("0"),
        "iv_minus_rv_percentile": Decimal("0.25"),
        "iv_minus_rv_window_target_days": 60,
        "iv_minus_rv_window_min_days": 30,
    }
    base_entry.update(entry_overrides)
    return golden_config(entry=base_entry)


def test_adaptive_pass_when_iv_rv_above_p25() -> None:
    cfg = _adaptive_cfg()
    history = tuple(Decimal(i) for i in range(1, 201))
    decision = validate_entry(
        _good_ctx(
            iv_minus_rv=Decimal("80"),
            iv_rv_history=history,
            iv_rv_n_days=30,
        ),
        cfg,
    )
    assert decision.accepted is True
    assert not any("IV richness" in r for r in decision.reasons)


def test_adaptive_blocks_when_iv_rv_below_p25() -> None:
    cfg = _adaptive_cfg()
    history = tuple(Decimal(i) for i in range(1, 201))
    decision = validate_entry(
        _good_ctx(
            iv_minus_rv=Decimal("20"),
            iv_rv_history=history,
            iv_rv_n_days=30,
        ),
        cfg,
    )
    assert decision.accepted is False
    assert any("IV richness" in r and "rolling" in r for r in decision.reasons)


def test_adaptive_with_n_days_zero_passes_warmup() -> None:
    """Warmup hard: nessun giorno coperto → gate skip (fail-open)."""
    cfg = _adaptive_cfg()
    decision = validate_entry(
        _good_ctx(
            iv_minus_rv=Decimal("0.1"),
            iv_rv_history=(),
            iv_rv_n_days=0,
        ),
        cfg,
    )
    assert decision.accepted is True


def test_adaptive_with_floor_floor_binds_when_p25_low() -> None:
    cfg = _adaptive_cfg(iv_minus_rv_min=Decimal("3"))
    history = tuple(Decimal("0.5") for _ in range(200))
    decision = validate_entry(
        _good_ctx(
            iv_minus_rv=Decimal("1"),
            iv_rv_history=history,
            iv_rv_n_days=30,
        ),
        cfg,
    )
    assert decision.accepted is False
    assert any("IV richness" in r for r in decision.reasons)


def test_legacy_static_gate_still_works_when_adaptive_disabled() -> None:
    cfg = golden_config(entry={
        "iv_minus_rv_filter_enabled": True,
        "iv_minus_rv_adaptive_enabled": False,
        "iv_minus_rv_min": Decimal("3"),
    })
    decision = validate_entry(
        _good_ctx(iv_minus_rv=Decimal("2"), iv_rv_history=(), iv_rv_n_days=0),
        cfg,
    )
    assert decision.accepted is False
    assert any("IV richness below floor" in r for r in decision.reasons)


def test_iv_minus_rv_none_skips_gate_in_both_modes() -> None:
    cfg = _adaptive_cfg()
    decision = validate_entry(
        _good_ctx(
            iv_minus_rv=None,
            iv_rv_history=tuple(Decimal(i) for i in range(1, 201)),
            iv_rv_n_days=30,
        ),
        cfg,
    )
    assert decision.accepted is True


def test_adaptive_with_n_days_one_uses_history_for_percentile() -> None:
    """Singolo giorno disponibile (cadenza qualunque): gate attivo,
    soglia = P25 della finestra ricevuta. Dimostra che il warmup hard
    finisce a n_days=1 (non 30 come nella vecchia implementazione)."""
    cfg = _adaptive_cfg()
    history = tuple(Decimal(i) for i in range(1, 101))  # 1..100, P25 = 25.75
    # IV-RV sopra P25 → pass
    pass_decision = validate_entry(
        _good_ctx(
            iv_minus_rv=Decimal("30"),
            iv_rv_history=history,
            iv_rv_n_days=1,
        ),
        cfg,
    )
    assert pass_decision.accepted is True
    # IV-RV sotto P25 → block
    block_decision = validate_entry(
        _good_ctx(
            iv_minus_rv=Decimal("10"),
            iv_rv_history=history,
            iv_rv_n_days=1,
        ),
        cfg,
    )
    assert block_decision.accepted is False
    assert any("IV richness" in r and "rolling" in r for r in block_decision.reasons)


# ---------------------------------------------------------------------------
# Vol-of-Vol guard
# ---------------------------------------------------------------------------


def _vov_cfg(threshold: Decimal = Decimal("5")) -> StrategyConfig:
    return golden_config(entry={
        "vol_of_vol_guard_enabled": True,
        "vol_of_vol_threshold_pt": threshold,
        "vol_of_vol_lookback_hours": 24,
    })


def test_vov_guard_blocks_on_large_dvol_shift() -> None:
    cfg = _vov_cfg()
    decision = validate_entry(
        _good_ctx(dvol_now=Decimal("56"), dvol_24h_ago=Decimal("50")), cfg
    )
    assert decision.accepted is False
    assert any("DVOL shifted" in r for r in decision.reasons)


def test_vov_guard_passes_on_small_dvol_shift() -> None:
    cfg = _vov_cfg()
    decision = validate_entry(
        _good_ctx(dvol_now=Decimal("52"), dvol_24h_ago=Decimal("50")), cfg
    )
    assert decision.accepted is True


def test_vov_guard_passes_when_lookback_missing() -> None:
    """fail-open su gap dati: se dvol_24h_ago=None il guard non scatta."""
    cfg = _vov_cfg()
    decision = validate_entry(
        _good_ctx(dvol_now=Decimal("99"), dvol_24h_ago=None), cfg
    )
    # dvol_now=99 sarebbe oltre dvol_max=90; testiamo solo l'effetto VoV
    # consultando le reasons (dvol_now potrebbe avere altre reason ma non
    # quella VoV).
    assert not any("DVOL shifted" in r for r in decision.reasons)


def test_vov_guard_disabled_does_nothing() -> None:
    cfg = golden_config(entry={"vol_of_vol_guard_enabled": False})
    decision = validate_entry(
        _good_ctx(dvol_now=Decimal("55"), dvol_24h_ago=Decimal("50")), cfg
    )
    # Nessuna reason VoV (il delta=5 sarebbe oltre soglia se attivo)
    assert not any("DVOL shifted" in r for r in decision.reasons)
