"""Unit tests for services.signal_engine.signals.ma_stack — Moving average stack evaluator.

Requirements: 2.2, 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.ma_stack import (
    MA_PERIODS,
    MIN_BARS,
    MAStackEvaluator,
)

# ---------------------------------------------------------------------------
# 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.

    ``start`` is the first bar's close; each subsequent bar adds ``step``.
    """
    return [_bar(start + i * step) for i in range(n)]


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


def test_ma_periods_constant() -> None:
    assert MA_PERIODS == [10, 20, 50, 200]


def test_min_bars_constant() -> None:
    assert MIN_BARS == 200


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


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


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


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


# ---------------------------------------------------------------------------
# No alignment → None
# ---------------------------------------------------------------------------


def test_returns_none_when_all_mas_equal() -> None:
    """When all bars have the same close, all MAs are equal — no alignment."""
    evaluator = MAStackEvaluator()
    bars = _make_bars(200, close=100.0)
    assert evaluator.evaluate(bars, "D") is None


# ---------------------------------------------------------------------------
# Full bullish alignment
# ---------------------------------------------------------------------------


def test_full_bullish_alignment() -> None:
    """Requirement 2.2: MA_10 > MA_20 > MA_50 > MA_200 → bullish, strength 1.0."""
    evaluator = MAStackEvaluator()
    # Strongly uptrending: recent prices much higher than old prices
    # This ensures MA_10 > MA_20 > MA_50 > MA_200
    bars = _trending_bars(200, start=50.0, step=1.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    assert result.signal_type == "ma_stack"
    assert result.direction == SignalDirection.BULLISH
    assert result.strength == 1.0
    assert result.confidence == 1.0 * 0.9
    assert result.metadata["alignment"] == "full_bullish"


# ---------------------------------------------------------------------------
# Full bearish alignment
# ---------------------------------------------------------------------------


def test_full_bearish_alignment() -> None:
    """Requirement 2.2: MA_10 < MA_20 < MA_50 < MA_200 → bearish, strength 1.0."""
    evaluator = MAStackEvaluator()
    # Strongly downtrending: recent prices much lower than old prices
    bars = _trending_bars(200, start=250.0, step=-1.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    assert result.signal_type == "ma_stack"
    assert result.direction == SignalDirection.BEARISH
    assert result.strength == 1.0
    assert result.confidence == 1.0 * 0.9
    assert result.metadata["alignment"] == "full_bearish"


# ---------------------------------------------------------------------------
# Partial bullish alignment (3/4)
# ---------------------------------------------------------------------------


def test_partial_bullish_alignment() -> None:
    """3 out of 4 MAs in bullish order → strength 0.6."""
    evaluator = MAStackEvaluator()
    # Build bars where MA_10 > MA_20 > MA_50 but MA_50 < MA_200
    # Use flat early prices (high MA_200) then a moderate uptrend at the end
    bars = _make_bars(150, close=200.0) + _trending_bars(50, start=100.0, step=2.0)
    result = evaluator.evaluate(bars, "D")
    # The recent uptrend should give MA_10 > MA_20 > MA_50
    # but MA_200 includes the high early prices, so MA_50 < MA_200
    if result is not None:
        assert result.direction == SignalDirection.BULLISH
        assert result.strength == 0.6
        assert result.confidence == 0.6 * 0.9
        assert result.metadata["alignment"] == "partial_bullish"


# ---------------------------------------------------------------------------
# Partial bearish alignment (3/4)
# ---------------------------------------------------------------------------


def test_partial_bearish_alignment() -> None:
    """3 out of 4 MAs in bearish order → strength 0.6."""
    evaluator = MAStackEvaluator()
    # Build bars where MA_10 < MA_20 < MA_50 but MA_50 > MA_200
    # Use flat low early prices (low MA_200) then a moderate downtrend at the end
    bars = _make_bars(150, close=50.0) + _trending_bars(50, start=200.0, step=-2.0)
    result = evaluator.evaluate(bars, "D")
    if result is not None:
        assert result.direction == SignalDirection.BEARISH
        assert result.strength == 0.6
        assert result.confidence == 0.6 * 0.9
        assert result.metadata["alignment"] == "partial_bearish"


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


def test_signal_result_structure() -> None:
    """Requirement 2.7: SignalResult has all required fields."""
    evaluator = MAStackEvaluator()
    bars = _trending_bars(200, start=50.0, step=1.0)
    result = evaluator.evaluate(bars, "H4")
    assert result is not None
    assert result.signal_type == "ma_stack"
    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_all_ma_values() -> None:
    """Metadata should include all four MA values and alignment type."""
    evaluator = MAStackEvaluator()
    bars = _trending_bars(200, start=50.0, step=1.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None
    meta = result.metadata
    assert "ma_10" in meta
    assert "ma_20" in meta
    assert "ma_50" in meta
    assert "ma_200" in meta
    assert "alignment" in meta


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


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


# ---------------------------------------------------------------------------
# Exactly 200 bars (boundary)
# ---------------------------------------------------------------------------


def test_exactly_200_bars_works() -> None:
    """Exactly 200 bars should be sufficient (boundary condition)."""
    evaluator = MAStackEvaluator()
    bars = _trending_bars(200, start=50.0, step=1.0)
    result = evaluator.evaluate(bars, "D")
    assert result is not None


def test_199_bars_returns_none() -> None:
    """199 bars is insufficient."""
    evaluator = MAStackEvaluator()
    bars = _trending_bars(199, start=50.0, step=1.0)
    result = evaluator.evaluate(bars, "D")
    assert result is None
