"""Unit tests for the probabilistic (Bayesian) pipeline.

Tests cover:
- Regime-to-prior mapping
- Likelihood ratio computation
- Log-odds accumulation and sigmoid round-trip
- Shannon entropy computation
- Entropy gating (SKIP on high entropy)
- EV_R computation
- BUY / WATCH / SKIP verdict thresholds
- Edge cases (no signals, boundary values)

Requirements: 6.1–6.9, 14.1–14.5
"""

from __future__ import annotations

from datetime import datetime, timezone

from services.aggregation.regime import MarketRegime, RegimeClassification
from services.signal_engine.config import ProbabilisticConfig
from services.signal_engine.models import (
    ConfluenceSignal,
    NormalizedInput,
    SignalDirection,
    Verdict,
)
from services.signal_engine.probabilistic import (
    _compute_ev_r,
    _compute_likelihood_ratios,
    _logit,
    _regime_to_prior,
    _shannon_entropy,
    _sigmoid,
    run_probabilistic_pipeline,
)

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

def _make_normalized(
    macro_bias: float = 0.5,
    valuation_score: float | None = 0.7,
    earnings_proximity_days: int | None = 30,
) -> NormalizedInput:
    return NormalizedInput(
        ticker="TEST",
        evaluated_at=datetime.now(tz=timezone.utc),
        bars={},
        macro_bias=macro_bias,
        valuation_score=valuation_score,
        earnings_proximity_days=earnings_proximity_days,
    )


def _make_regime(
    regime: MarketRegime = MarketRegime.TREND_FOLLOWING,
    trend_indicator: float = 1.0,
) -> RegimeClassification:
    return RegimeClassification(
        regime=regime,
        trend_indicator=trend_indicator,
        volatility_ratio=1.0,
        bullish_threshold=0.15,
        bearish_threshold=-0.15,
        contradiction_penalty_multiplier=0.4,
    )


def _make_confluence(
    signal_type: str = "fibonacci",
    direction: SignalDirection = SignalDirection.BULLISH,
    confluence_score: float = 0.8,
    active_timeframes: list[str] | None = None,
) -> ConfluenceSignal:
    if active_timeframes is None:
        active_timeframes = ["D", "W"]
    return ConfluenceSignal(
        signal_type=signal_type,
        direction=direction,
        confluence_score=confluence_score,
        active_timeframes=active_timeframes,
        per_timeframe={tf: confluence_score for tf in active_timeframes},
    )


DEFAULT_CONFIG = ProbabilisticConfig()


# ---------------------------------------------------------------------------
# Regime → prior mapping
# ---------------------------------------------------------------------------


class TestRegimeToPrior:
    def test_trend_following_bullish(self):
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=1.0)
        assert _regime_to_prior(regime, DEFAULT_CONFIG) == 0.58

    def test_trend_following_bearish(self):
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=-1.0)
        assert _regime_to_prior(regime, DEFAULT_CONFIG) == 0.42

    def test_trend_following_zero_indicator(self):
        """Zero trend_indicator is not positive → bear prior."""
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=0.0)
        assert _regime_to_prior(regime, DEFAULT_CONFIG) == 0.42

    def test_mean_reversion(self):
        regime = _make_regime(MarketRegime.MEAN_REVERSION)
        assert _regime_to_prior(regime, DEFAULT_CONFIG) == 0.50

    def test_panic(self):
        regime = _make_regime(MarketRegime.PANIC)
        assert _regime_to_prior(regime, DEFAULT_CONFIG) == 0.42

    def test_uncertainty(self):
        regime = _make_regime(MarketRegime.UNCERTAINTY)
        assert _regime_to_prior(regime, DEFAULT_CONFIG) == 0.50


# ---------------------------------------------------------------------------
# Logit / sigmoid helpers
# ---------------------------------------------------------------------------


class TestLogitSigmoid:
    def test_logit_sigmoid_round_trip(self):
        for p in [0.1, 0.25, 0.5, 0.75, 0.9]:
            assert abs(_sigmoid(_logit(p)) - p) < 1e-10

    def test_logit_at_half(self):
        assert abs(_logit(0.5)) < 1e-10

    def test_sigmoid_at_zero(self):
        assert abs(_sigmoid(0.0) - 0.5) < 1e-10

    def test_sigmoid_large_positive(self):
        assert _sigmoid(1000) == 1.0

    def test_sigmoid_large_negative(self):
        assert _sigmoid(-1000) == 0.0


# ---------------------------------------------------------------------------
# Shannon entropy
# ---------------------------------------------------------------------------


class TestShannonEntropy:
    def test_max_at_half(self):
        assert abs(_shannon_entropy(0.5) - 1.0) < 1e-10

    def test_zero_at_boundaries(self):
        assert _shannon_entropy(0.0) == 0.0
        assert _shannon_entropy(1.0) == 0.0

    def test_symmetric(self):
        assert abs(_shannon_entropy(0.3) - _shannon_entropy(0.7)) < 1e-10

    def test_in_range(self):
        for p in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]:
            h = _shannon_entropy(p)
            assert 0.0 <= h <= 1.0


# ---------------------------------------------------------------------------
# Likelihood ratio computation
# ---------------------------------------------------------------------------


class TestLikelihoodRatios:
    def test_bullish_signal_produces_lr_gt_1(self):
        sig = _make_confluence(direction=SignalDirection.BULLISH, confluence_score=0.8)
        lrs = _compute_likelihood_ratios([sig])
        assert len(lrs) == 1
        assert lrs[0].lr > 1.0
        assert lrs[0].log_lr > 0.0

    def test_bearish_signal_produces_lr_lt_1(self):
        sig = _make_confluence(direction=SignalDirection.BEARISH, confluence_score=0.8)
        lrs = _compute_likelihood_ratios([sig])
        assert len(lrs) == 1
        assert lrs[0].lr < 1.0
        assert lrs[0].log_lr < 0.0

    def test_neutral_signal_produces_lr_gt_1(self):
        """Neutral signals still contribute based on strength."""
        sig = _make_confluence(direction=SignalDirection.NEUTRAL, confluence_score=0.8)
        lrs = _compute_likelihood_ratios([sig])
        assert len(lrs) == 1
        # Neutral is treated as bullish evidence (no inversion)
        assert lrs[0].lr > 1.0

    def test_empty_signals(self):
        lrs = _compute_likelihood_ratios([])
        assert lrs == []

    def test_cluster_assignment(self):
        sig = _make_confluence(signal_type="ma_stack")
        lrs = _compute_likelihood_ratios([sig])
        assert lrs[0].cluster == "momentum"


# ---------------------------------------------------------------------------
# EV_R computation
# ---------------------------------------------------------------------------


class TestEvR:
    def test_ev_r_high_p_up(self):
        signals = [_make_confluence(confluence_score=0.8)]
        ev_r = _compute_ev_r(0.8, signals)
        # E[win_R] = 0.8 * 2.0 = 1.6
        # EV_R = 0.8 * 1.6 - 0.2 * 1.0 = 1.28 - 0.2 = 1.08
        assert abs(ev_r - 1.08) < 1e-10

    def test_ev_r_at_half(self):
        signals = [_make_confluence(confluence_score=0.5)]
        ev_r = _compute_ev_r(0.5, signals)
        # E[win_R] = 0.5 * 2.0 = 1.0
        # EV_R = 0.5 * 1.0 - 0.5 * 1.0 = 0.0
        assert abs(ev_r) < 1e-10

    def test_ev_r_no_signals(self):
        ev_r = _compute_ev_r(0.7, [])
        # E[win_R] = 1.0 (fallback)
        # EV_R = 0.7 * 1.0 - 0.3 * 1.0 = 0.4
        assert abs(ev_r - 0.4) < 1e-10

    def test_ev_r_monotonic_with_p_up(self):
        signals = [_make_confluence(confluence_score=0.8)]
        ev_low = _compute_ev_r(0.5, signals)
        ev_high = _compute_ev_r(0.8, signals)
        assert ev_high > ev_low


# ---------------------------------------------------------------------------
# Full pipeline — verdict tests
# ---------------------------------------------------------------------------


class TestProbabilisticPipeline:
    def test_no_signals_returns_prior_based_result(self):
        """With no signals, P_up equals the prior."""
        normalized = _make_normalized()
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=1.0)
        result = run_probabilistic_pipeline(normalized, [], regime, DEFAULT_CONFIG)
        assert abs(result.p_up - 0.58) < 1e-6
        assert result.prior == 0.58

    def test_buy_verdict_with_strong_signals(self):
        """Strong bullish signals + favorable conditions → BUY."""
        normalized = _make_normalized(macro_bias=0.5, valuation_score=0.7)
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=1.0)
        # Multiple strong bullish signals from different clusters
        signals = [
            _make_confluence("fibonacci", SignalDirection.BULLISH, 0.9),
            _make_confluence("ma_stack", SignalDirection.BULLISH, 0.85),
            _make_confluence("rsi", SignalDirection.BULLISH, 0.8),
            _make_confluence("valuation", SignalDirection.BULLISH, 0.75),
        ]
        result = run_probabilistic_pipeline(normalized, signals, regime, DEFAULT_CONFIG)
        # With strong signals and bull prior, P_up should be high
        assert result.p_up >= 0.60
        # Verdict depends on all conditions being met
        assert result.verdict in (Verdict.BUY, Verdict.WATCH)

    def test_skip_on_high_entropy(self):
        """P_up near 0.5 → high entropy → SKIP."""
        normalized = _make_normalized()
        regime = _make_regime(MarketRegime.MEAN_REVERSION)  # prior = 0.50
        # No signals → P_up stays at 0.50 → entropy = 1.0 > 0.95
        result = run_probabilistic_pipeline(normalized, [], regime, DEFAULT_CONFIG)
        assert result.verdict == Verdict.SKIP
        assert result.entropy > 0.95
        assert any("high_entropy" in r for r in result.reasoning)

    def test_skip_on_low_p_up(self):
        """Bearish signals → low P_up → SKIP."""
        normalized = _make_normalized()
        regime = _make_regime(MarketRegime.PANIC)  # prior = 0.42
        signals = [
            _make_confluence("fibonacci", SignalDirection.BEARISH, 0.9),
            _make_confluence("ma_stack", SignalDirection.BEARISH, 0.85),
        ]
        result = run_probabilistic_pipeline(normalized, signals, regime, DEFAULT_CONFIG)
        assert result.p_up < 0.55
        assert result.verdict == Verdict.SKIP

    def test_watch_verdict(self):
        """Moderate signals → WATCH (P_up >= 0.55 but not all BUY conditions)."""
        normalized = _make_normalized(macro_bias=-0.1)  # macro_bias <= 0 blocks BUY
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=1.0)
        signals = [
            _make_confluence("fibonacci", SignalDirection.BULLISH, 0.8),
            _make_confluence("ma_stack", SignalDirection.BULLISH, 0.75),
        ]
        result = run_probabilistic_pipeline(normalized, signals, regime, DEFAULT_CONFIG)
        # P_up should be above 0.55 with bull prior + bullish signals
        if result.p_up >= 0.55 and result.entropy <= 0.95:
            assert result.verdict == Verdict.WATCH

    def test_macro_bias_blocks_buy(self):
        """macro_bias <= 0 prevents BUY even with high P_up."""
        normalized = _make_normalized(macro_bias=0.0, valuation_score=0.8)
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=1.0)
        signals = [
            _make_confluence("fibonacci", SignalDirection.BULLISH, 0.9),
            _make_confluence("ma_stack", SignalDirection.BULLISH, 0.9),
            _make_confluence("rsi", SignalDirection.BULLISH, 0.85),
            _make_confluence("valuation", SignalDirection.BULLISH, 0.8),
        ]
        result = run_probabilistic_pipeline(normalized, signals, regime, DEFAULT_CONFIG)
        assert result.verdict != Verdict.BUY

    def test_valuation_blocks_buy(self):
        """valuation_score < 0.5 prevents BUY."""
        normalized = _make_normalized(macro_bias=0.5, valuation_score=0.3)
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=1.0)
        signals = [
            _make_confluence("fibonacci", SignalDirection.BULLISH, 0.9),
            _make_confluence("ma_stack", SignalDirection.BULLISH, 0.9),
            _make_confluence("rsi", SignalDirection.BULLISH, 0.85),
            _make_confluence("valuation", SignalDirection.BULLISH, 0.8),
        ]
        result = run_probabilistic_pipeline(normalized, signals, regime, DEFAULT_CONFIG)
        assert result.verdict != Verdict.BUY

    def test_result_fields_populated(self):
        """All ProbabilisticResult fields are populated correctly."""
        normalized = _make_normalized()
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=1.0)
        signals = [_make_confluence("fibonacci", SignalDirection.BULLISH, 0.7)]
        result = run_probabilistic_pipeline(normalized, signals, regime, DEFAULT_CONFIG)

        assert 0.0 <= result.p_up <= 1.0
        assert 0.0 <= result.entropy <= 1.0
        assert isinstance(result.ev_r, float)
        assert result.prior == 0.58
        assert result.posterior == result.p_up
        assert result.regime == "trend_following"
        assert len(result.likelihood_ratios) == 1
        assert len(result.reasoning) > 0

    def test_none_valuation_treated_as_zero(self):
        """None valuation_score is treated as 0.0 for verdict logic."""
        normalized = _make_normalized(valuation_score=None)
        regime = _make_regime(MarketRegime.TREND_FOLLOWING, trend_indicator=1.0)
        signals = [
            _make_confluence("fibonacci", SignalDirection.BULLISH, 0.9),
            _make_confluence("ma_stack", SignalDirection.BULLISH, 0.9),
            _make_confluence("rsi", SignalDirection.BULLISH, 0.85),
        ]
        result = run_probabilistic_pipeline(normalized, signals, regime, DEFAULT_CONFIG)
        # valuation_score=None → 0.0 < 0.5 → BUY blocked
        assert result.verdict != Verdict.BUY
