"""Unit tests for regime detector (services/aggregation/regime.py).

Tests specific (R, V_r) → regime classification, threshold adjustments
per regime, and insufficient data fallback to uncertainty.

Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.9
"""
from __future__ import annotations

import pytest

from services.aggregation.regime import (
    MarketRegime,
    classify_regime,
    compute_ema,
)

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


def _make_uptrend_prices(n: int = 120) -> list[float]:
    """Generate prices with EMA_20 > EMA_100 (uptrend, R=+1)."""
    # Start low, end high — recent prices much higher than old ones
    return [100.0 + i * 0.5 for i in range(n)]


def _make_downtrend_prices(n: int = 120) -> list[float]:
    """Generate prices with EMA_20 < EMA_100 (downtrend, R=-1)."""
    # Start high, end low — recent prices much lower than old ones
    return [200.0 - i * 0.5 for i in range(n)]


def _make_flat_prices(n: int = 120) -> list[float]:
    """Generate flat prices where EMA_20 ≈ EMA_100 (R=0)."""
    return [100.0] * n


def _make_low_vol_returns(n: int = 120) -> list[float]:
    """Generate returns with σ_20 / σ_100 < 1.0 (low recent volatility)."""
    # First 100 returns have higher variance, last 20 have lower variance
    base = [0.02 * ((-1) ** i) for i in range(n - 20)]
    recent = [0.005 * ((-1) ** i) for i in range(20)]
    return base + recent


def _make_high_vol_returns(n: int = 120) -> list[float]:
    """Generate returns with σ_20 / σ_100 > 1.5 (panic volatility)."""
    # First 100 returns have low variance, last 20 have very high variance
    base = [0.005 * ((-1) ** i) for i in range(n - 20)]
    recent = [0.08 * ((-1) ** i) for i in range(20)]
    return base + recent


def _make_moderate_vol_returns(n: int = 120) -> list[float]:
    """Generate returns with V_r between 1.0 and 1.2."""
    # Slightly higher recent volatility
    base = [0.01 * ((-1) ** i) for i in range(n - 20)]
    recent = [0.012 * ((-1) ** i) for i in range(20)]
    return base + recent


# ---------------------------------------------------------------------------
# compute_ema
# ---------------------------------------------------------------------------


class TestComputeEma:
    """Test EMA computation."""

    def test_single_value(self):
        assert compute_ema([100.0], 1) == pytest.approx(100.0)

    def test_constant_values(self):
        """EMA of constant values equals that constant."""
        assert compute_ema([50.0] * 20, 20) == pytest.approx(50.0)

    def test_empty_raises(self):
        with pytest.raises(ValueError):
            compute_ema([], 10)

    def test_zero_period_raises(self):
        with pytest.raises(ValueError):
            compute_ema([1.0, 2.0], 0)


# ---------------------------------------------------------------------------
# Regime classification: specific (R, V_r) → expected regime
# ---------------------------------------------------------------------------


class TestRegimeClassification:
    """Test specific (R, V_r) → expected regime classification (Req 7.3)."""

    def test_trend_following_uptrend(self):
        """R=+1, V_r < 1.2 → trend_following."""
        prices = _make_uptrend_prices()
        returns = _make_moderate_vol_returns()
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.TREND_FOLLOWING
        assert result.trend_indicator == 1.0

    def test_trend_following_downtrend(self):
        """R=-1, V_r < 1.2 → trend_following."""
        prices = _make_downtrend_prices()
        returns = _make_moderate_vol_returns()
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.TREND_FOLLOWING
        assert result.trend_indicator == -1.0

    def test_panic_regime(self):
        """V_r > 1.5 → panic (regardless of R)."""
        prices = _make_uptrend_prices()
        returns = _make_high_vol_returns()
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.PANIC

    def test_mean_reversion_regime(self):
        """R=0, V_r < 1.0 → mean_reversion."""
        prices = _make_flat_prices()
        returns = _make_low_vol_returns()
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.MEAN_REVERSION

    def test_uncertainty_regime(self):
        """R=0, V_r between 1.0 and 1.5 → uncertainty."""
        prices = _make_flat_prices()
        # Returns with V_r between 1.0 and 1.5 but not < 1.0
        # Use moderate vol that gives V_r ≈ 1.1 with flat prices
        returns = _make_moderate_vol_returns()
        result = classify_regime(prices, returns)
        # With flat prices R=0, and moderate vol V_r ≈ 1.1 (> 1.0)
        # This falls into uncertainty (R=0 AND V_r >= 1.0)
        assert result.regime == MarketRegime.UNCERTAINTY


# ---------------------------------------------------------------------------
# Threshold adjustments per regime (Req 7.4, 7.5, 7.6, 7.7)
# ---------------------------------------------------------------------------


class TestRegimeThresholds:
    """Test threshold adjustments per regime."""

    def test_panic_threshold(self):
        """Panic regime → threshold ±0.10 (Req 7.4)."""
        prices = _make_uptrend_prices()
        returns = _make_high_vol_returns()
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.PANIC
        assert result.bullish_threshold == pytest.approx(0.10)
        assert result.bearish_threshold == pytest.approx(-0.10)

    def test_mean_reversion_threshold(self):
        """Mean-reversion regime → threshold ±0.20 (Req 7.5)."""
        prices = _make_flat_prices()
        returns = _make_low_vol_returns()
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.MEAN_REVERSION
        assert result.bullish_threshold == pytest.approx(0.20)
        assert result.bearish_threshold == pytest.approx(-0.20)

    def test_trend_following_threshold(self):
        """Trend-following regime → threshold ±0.15 (Req 7.6)."""
        prices = _make_uptrend_prices()
        returns = _make_moderate_vol_returns()
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.TREND_FOLLOWING
        assert result.bullish_threshold == pytest.approx(0.15)
        assert result.bearish_threshold == pytest.approx(-0.15)

    def test_uncertainty_contradiction_multiplier(self):
        """Uncertainty regime → contradiction multiplier 0.6 (Req 7.7)."""
        prices = _make_flat_prices()
        returns = _make_moderate_vol_returns()
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.UNCERTAINTY
        assert result.contradiction_penalty_multiplier == pytest.approx(0.6)

    def test_non_uncertainty_contradiction_multiplier(self):
        """Non-uncertainty regimes → contradiction multiplier 0.4."""
        prices = _make_uptrend_prices()
        returns = _make_moderate_vol_returns()
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.TREND_FOLLOWING
        assert result.contradiction_penalty_multiplier == pytest.approx(0.4)


# ---------------------------------------------------------------------------
# Insufficient data fallback to uncertainty (Req 7.9)
# ---------------------------------------------------------------------------


class TestInsufficientDataFallback:
    """Test fallback to uncertainty when data is insufficient."""

    def test_too_few_prices(self):
        """Fewer than 100 closing prices → uncertainty."""
        prices = [100.0] * 50  # only 50 days
        returns = [0.01] * 100
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.UNCERTAINTY

    def test_too_few_returns(self):
        """Fewer than 100 returns → uncertainty."""
        prices = [100.0] * 120
        returns = [0.01] * 50  # only 50 returns
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.UNCERTAINTY

    def test_empty_prices(self):
        """Empty price list → uncertainty."""
        result = classify_regime([], [0.01] * 100)
        assert result.regime == MarketRegime.UNCERTAINTY

    def test_empty_returns(self):
        """Empty return list → uncertainty."""
        result = classify_regime([100.0] * 120, [])
        assert result.regime == MarketRegime.UNCERTAINTY

    def test_zero_sigma_returns_uncertainty(self):
        """All identical returns (σ=0) → uncertainty."""
        prices = _make_uptrend_prices()
        returns = [0.0] * 120  # zero standard deviation
        result = classify_regime(prices, returns)
        assert result.regime == MarketRegime.UNCERTAINTY

    def test_default_uncertainty_values(self):
        """Default uncertainty has standard threshold values."""
        result = classify_regime([], [])
        assert result.regime == MarketRegime.UNCERTAINTY
        assert result.bullish_threshold == pytest.approx(0.15)
        assert result.bearish_threshold == pytest.approx(-0.15)
        assert result.contradiction_penalty_multiplier == pytest.approx(0.6)
        assert result.trend_indicator == 0.0
        assert result.volatility_ratio == 1.0
