"""Unit tests for services.signal_engine.signals.rsi — RSI evaluator.

Requirements: 2.3, 2.6, 2.7
"""
from __future__ import annotations

from datetime import datetime, timezone

from services.signal_engine.models import OHLCVBar, SignalDirection
from services.signal_engine.signals.rsi import (
    DEFAULT_MIN_BARS,
    DEFAULT_RSI_PERIOD,
    OVERBOUGHT_THRESHOLD,
    OVERSOLD_THRESHOLD,
    RSIEvaluator,
    compute_rsi,
)

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


def _bar(close: float) -> OHLCVBar:
    """Create a minimal OHLCVBar for testing."""
    return OHLCVBar(
        timestamp=datetime(2024, 1, 1, tzinfo=timezone.utc),
        open=close,
        high=close,
        low=close,
        close=close,
        volume=1000.0,
    )


def _make_bars(n: int, close: float = 100.0) -> list[OHLCVBar]:
    """Create *n* identical bars with the same close price."""
    return [_bar(close) for _ in range(n)]


def _trending_bars(n: int, start: float, step: float) -> list[OHLCVBar]:
    """Create *n* bars with linearly increasing/decreasing close prices."""
    return [_bar(start + i * step) for i in range(n)]


def _alternating_bars(
    n: int,
    base: float,
    gain: float,
    loss: float,
) -> list[OHLCVBar]:
    """Create bars that alternate between gaining and losing.

    Useful for producing RSI values in the neutral zone.
    """
    bars: list[OHLCVBar] = [_bar(base)]
    price = base
    for i in range(1, n):
        if i % 2 == 1:
            price += gain
        else:
            price -= loss
        bars.append(_bar(price))
    return bars


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------


def test_default_period() -> None:
    assert DEFAULT_RSI_PERIOD == 14


def test_default_min_bars() -> None:
    assert DEFAULT_MIN_BARS == 15


def test_thresholds() -> None:
    assert OVERBOUGHT_THRESHOLD == 70.0
    assert OVERSOLD_THRESHOLD == 30.0


# ---------------------------------------------------------------------------
# Insufficient data → None
# ---------------------------------------------------------------------------


def test_returns_none_when_insufficient_bars() -> None:
    """Requirement 2.6: return None when fewer than 15 bars."""
    evaluator = RSIEvaluator()
    bars = _make_bars(14)
    assert evaluator.evaluate(bars, "D") is None


def test_returns_none_with_empty_bars() -> None:
    evaluator = RSIEvaluator()
    assert evaluator.evaluate([], "D") is None


def test_returns_none_with_one_bar() -> None:
    evaluator = RSIEvaluator()
    assert evaluator.evaluate([_bar(100.0)], "D") is None


# ---------------------------------------------------------------------------
# Neutral zone → None
# ---------------------------------------------------------------------------


def test_returns_none_in_neutral_zone() -> None:
    """RSI between 30 and 70 should return None (no signal)."""
    evaluator = RSIEvaluator()
    # Alternating gains and losses produce RSI near 50
    bars = _alternating_bars(30, base=100.0, gain=1.0, loss=1.0)
    result = evaluator.evaluate(bars, "D")
    # RSI should be near 50 → neutral → None
    assert result is None


def test_flat_market_returns_none() -> None:
    """All bars with the same close → no price changes → no signal.

    When all changes are zero, avg_gain=0 and avg_loss=0.
    avg_loss=0 means RSI=100, which is overbought. But with truly flat
    prices (no gains, no losses), RSI is technically 100 (all gains are 0,
    all losses are 0 → RS = 0/0 edge case handled as RSI=100).
    """
    evaluator = RSIEvaluator()
    bars = _make_bars(30, close=100.0)
    rsi = compute_rsi(bars)
    # With zero changes: avg_gain=0, avg_loss=0 → RSI=100 (per our implementation)
    assert rsi == 100.0


# ---------------------------------------------------------------------------
# Overbought signal (RSI > 70) → BEARISH
# ---------------------------------------------------------------------------


def test_overbought_produces_bearish_signal() -> None:
    """Requirement 2.3: RSI > 70 → BEARISH signal (overbought → potential reversal down)."""
    evaluator = RSIEvaluator()
    # Strong uptrend: all gains, no losses → RSI approaches 100
    bars = _trending_bars(30, start=50.0, step=2.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    assert result.signal_type == "rsi"
    assert result.direction == SignalDirection.BEARISH
    assert result.metadata["zone"] == "overbought"
    assert result.metadata["rsi"] > OVERBOUGHT_THRESHOLD


# ---------------------------------------------------------------------------
# Oversold signal (RSI < 30) → BULLISH
# ---------------------------------------------------------------------------


def test_oversold_produces_bullish_signal() -> None:
    """Requirement 2.3: RSI < 30 → BULLISH signal (oversold → potential reversal up)."""
    evaluator = RSIEvaluator()
    # Strong downtrend: all losses, no gains → RSI approaches 0
    bars = _trending_bars(30, start=200.0, step=-2.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    assert result.signal_type == "rsi"
    assert result.direction == SignalDirection.BULLISH
    assert result.metadata["zone"] == "oversold"
    assert result.metadata["rsi"] < OVERSOLD_THRESHOLD


# ---------------------------------------------------------------------------
# Strength scaling
# ---------------------------------------------------------------------------


def test_overbought_strength_scales_with_distance() -> None:
    """Strength = (RSI - 70) / 30, clamped to [0, 1]."""
    evaluator = RSIEvaluator()
    # Strong uptrend → RSI near 100 → high strength
    bars = _trending_bars(30, start=50.0, step=3.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    rsi = result.metadata["rsi"]
    expected_strength = min(1.0, max(0.0, (rsi - 70.0) / 30.0))
    assert abs(result.strength - expected_strength) < 1e-9


def test_oversold_strength_scales_with_distance() -> None:
    """Strength = (30 - RSI) / 30, clamped to [0, 1]."""
    evaluator = RSIEvaluator()
    # Strong downtrend → RSI near 0 → high strength
    bars = _trending_bars(30, start=200.0, step=-3.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    rsi = result.metadata["rsi"]
    expected_strength = min(1.0, max(0.0, (30.0 - rsi) / 30.0))
    assert abs(result.strength - expected_strength) < 1e-9


def test_strength_clamped_to_unit_interval() -> None:
    """Strength must always be in [0, 1]."""
    evaluator = RSIEvaluator()
    # Extreme uptrend → RSI ≈ 100 → strength should be clamped to 1.0
    bars = _trending_bars(30, start=10.0, step=5.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    assert 0.0 <= result.strength <= 1.0


# ---------------------------------------------------------------------------
# Confidence = strength * 0.85
# ---------------------------------------------------------------------------


def test_confidence_equals_strength_times_085() -> None:
    """Confidence = strength * 0.85."""
    evaluator = RSIEvaluator()
    bars = _trending_bars(30, start=50.0, step=2.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    expected_confidence = result.strength * 0.85
    assert abs(result.confidence - expected_confidence) < 1e-9


# ---------------------------------------------------------------------------
# Signal result structure (Requirement 2.7)
# ---------------------------------------------------------------------------


def test_signal_result_structure() -> None:
    """Requirement 2.7: SignalResult has all required fields."""
    evaluator = RSIEvaluator()
    bars = _trending_bars(30, start=50.0, step=2.0)
    result = evaluator.evaluate(bars, "H4")
    assert result is not None
    assert result.signal_type == "rsi"
    assert result.timeframe == "H4"
    assert 0.0 <= result.strength <= 1.0
    assert 0.0 <= result.confidence <= 1.0
    assert result.direction in (SignalDirection.BULLISH, SignalDirection.BEARISH)


def test_metadata_contains_rsi_and_period() -> None:
    """Metadata should include RSI value and period used."""
    evaluator = RSIEvaluator()
    bars = _trending_bars(30, start=50.0, step=2.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    meta = result.metadata
    assert "rsi" in meta
    assert "period" in meta
    assert meta["period"] == 14
    assert isinstance(meta["rsi"], float)


# ---------------------------------------------------------------------------
# Timeframe passthrough
# ---------------------------------------------------------------------------


def test_timeframe_passthrough() -> None:
    """The timeframe label is passed through to the result."""
    evaluator = RSIEvaluator()
    bars = _trending_bars(30, start=50.0, step=2.0)
    for tf in ("M30", "H1", "H4", "D", "W", "M"):
        result = evaluator.evaluate(bars, tf)
        assert result is not None
        assert result.timeframe == tf


# ---------------------------------------------------------------------------
# Boundary: exactly 15 bars
# ---------------------------------------------------------------------------


def test_exactly_15_bars_works() -> None:
    """Exactly 15 bars (period + 1) should be sufficient."""
    evaluator = RSIEvaluator()
    bars = _trending_bars(15, start=50.0, step=2.0)
    result = evaluator.evaluate(bars, "D")
    # Should produce a result (strong uptrend → overbought)
    assert result is not None


def test_14_bars_returns_none() -> None:
    """14 bars is insufficient for a 14-period RSI."""
    evaluator = RSIEvaluator()
    bars = _trending_bars(14, start=50.0, step=2.0)
    result = evaluator.evaluate(bars, "D")
    assert result is None


# ---------------------------------------------------------------------------
# compute_rsi standalone function
# ---------------------------------------------------------------------------


def test_compute_rsi_all_gains() -> None:
    """All gains, no losses → RSI approaches 100."""
    bars = _trending_bars(30, start=50.0, step=1.0)
    rsi = compute_rsi(bars)
    assert rsi is not None
    assert rsi > 95.0  # Should be very close to 100


def test_compute_rsi_all_losses() -> None:
    """All losses, no gains → RSI approaches 0."""
    bars = _trending_bars(30, start=200.0, step=-1.0)
    rsi = compute_rsi(bars)
    assert rsi is not None
    assert rsi < 5.0  # Should be very close to 0


def test_compute_rsi_insufficient_data() -> None:
    """Returns None when fewer than period + 1 bars."""
    bars = _make_bars(10)
    rsi = compute_rsi(bars)
    assert rsi is None


def test_compute_rsi_range() -> None:
    """RSI should always be in [0, 100]."""
    # Mixed trend
    bars = _trending_bars(15, start=100.0, step=0.5) + _trending_bars(15, start=107.0, step=-0.3)
    rsi = compute_rsi(bars)
    assert rsi is not None
    assert 0.0 <= rsi <= 100.0


# ---------------------------------------------------------------------------
# Custom period
# ---------------------------------------------------------------------------


def test_custom_period() -> None:
    """RSIEvaluator with a custom period should use that period."""
    evaluator = RSIEvaluator(period=7)
    assert evaluator.period == 7
    assert evaluator.min_bars == 8
    # 8 bars with uptrend should work
    bars = _trending_bars(8, start=50.0, step=2.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    assert result.metadata["period"] == 7
