"""Unit tests for services.signal_engine.signals.base helper functions.

Tests swing high/low detection, lookback validation, and SMA computation.
Requirements: 2.6, 2.7
"""
from __future__ import annotations

from datetime import datetime, timezone

from services.signal_engine.models import OHLCVBar
from services.signal_engine.signals.base import (
    compute_sma,
    find_swing_high,
    find_swing_low,
    validate_lookback,
)

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


def _bar(
    close: float,
    high: float | None = None,
    low: float | None = None,
    ts_offset: int = 0,
) -> OHLCVBar:
    """Create a minimal OHLCVBar for testing."""
    return OHLCVBar(
        timestamp=datetime(2024, 1, 1, tzinfo=timezone.utc),
        open=close,
        high=high if high is not None else close,
        low=low if low is not None else close,
        close=close,
        volume=1000.0,
    )


# ---------------------------------------------------------------------------
# find_swing_high
# ---------------------------------------------------------------------------


def test_find_swing_high_basic() -> None:
    bars = [_bar(10, high=12), _bar(10, high=15), _bar(10, high=11)]
    result = find_swing_high(bars, lookback=3)
    assert result is not None
    idx, price = result
    assert idx == 1
    assert price == 15.0


def test_find_swing_high_lookback_subset() -> None:
    bars = [_bar(10, high=20), _bar(10, high=12), _bar(10, high=15), _bar(10, high=11)]
    # lookback=2 → only last 2 bars (index 2 and 3 in original)
    result = find_swing_high(bars, lookback=2)
    assert result is not None
    idx, price = result
    assert idx == 2  # bar at index 2 has high=15
    assert price == 15.0


def test_find_swing_high_insufficient_data() -> None:
    bars = [_bar(10, high=12)]
    assert find_swing_high(bars, lookback=5) is None


def test_find_swing_high_zero_lookback() -> None:
    bars = [_bar(10, high=12)]
    assert find_swing_high(bars, lookback=0) is None


def test_find_swing_high_negative_lookback() -> None:
    bars = [_bar(10, high=12)]
    assert find_swing_high(bars, lookback=-1) is None


def test_find_swing_high_tie_takes_last() -> None:
    """When multiple bars share the same high, the last one wins (>=)."""
    bars = [_bar(10, high=15), _bar(10, high=15), _bar(10, high=10)]
    result = find_swing_high(bars, lookback=3)
    assert result is not None
    idx, price = result
    assert idx == 1
    assert price == 15.0


# ---------------------------------------------------------------------------
# find_swing_low
# ---------------------------------------------------------------------------


def test_find_swing_low_basic() -> None:
    bars = [_bar(10, low=8), _bar(10, low=5), _bar(10, low=9)]
    result = find_swing_low(bars, lookback=3)
    assert result is not None
    idx, price = result
    assert idx == 1
    assert price == 5.0


def test_find_swing_low_lookback_subset() -> None:
    bars = [_bar(10, low=2), _bar(10, low=8), _bar(10, low=5), _bar(10, low=9)]
    # lookback=2 → only last 2 bars (index 2 and 3)
    result = find_swing_low(bars, lookback=2)
    assert result is not None
    idx, price = result
    assert idx == 2  # bar at index 2 has low=5
    assert price == 5.0


def test_find_swing_low_insufficient_data() -> None:
    bars = [_bar(10, low=8)]
    assert find_swing_low(bars, lookback=5) is None


def test_find_swing_low_zero_lookback() -> None:
    bars = [_bar(10, low=8)]
    assert find_swing_low(bars, lookback=0) is None


def test_find_swing_low_tie_takes_last() -> None:
    """When multiple bars share the same low, the last one wins (<=)."""
    bars = [_bar(10, low=5), _bar(10, low=5), _bar(10, low=10)]
    result = find_swing_low(bars, lookback=3)
    assert result is not None
    idx, price = result
    assert idx == 1
    assert price == 5.0


# ---------------------------------------------------------------------------
# validate_lookback
# ---------------------------------------------------------------------------


def test_validate_lookback_sufficient() -> None:
    bars = [_bar(10)] * 20
    assert validate_lookback(bars, min_bars=20) is True


def test_validate_lookback_more_than_enough() -> None:
    bars = [_bar(10)] * 50
    assert validate_lookback(bars, min_bars=20) is True


def test_validate_lookback_insufficient() -> None:
    bars = [_bar(10)] * 5
    assert validate_lookback(bars, min_bars=20) is False


def test_validate_lookback_empty() -> None:
    assert validate_lookback([], min_bars=1) is False


def test_validate_lookback_zero_min() -> None:
    assert validate_lookback([], min_bars=0) is True


# ---------------------------------------------------------------------------
# compute_sma
# ---------------------------------------------------------------------------


def test_compute_sma_basic() -> None:
    bars = [_bar(10), _bar(20), _bar(30)]
    result = compute_sma(bars, period=3)
    assert result is not None
    assert result == 20.0


def test_compute_sma_subset() -> None:
    bars = [_bar(100), _bar(10), _bar(20), _bar(30)]
    # period=3 → average of last 3 bars: (10+20+30)/3 = 20
    result = compute_sma(bars, period=3)
    assert result is not None
    assert result == 20.0


def test_compute_sma_single_bar() -> None:
    bars = [_bar(42)]
    result = compute_sma(bars, period=1)
    assert result is not None
    assert result == 42.0


def test_compute_sma_insufficient_data() -> None:
    bars = [_bar(10), _bar(20)]
    assert compute_sma(bars, period=5) is None


def test_compute_sma_zero_period() -> None:
    bars = [_bar(10)]
    assert compute_sma(bars, period=0) is None


def test_compute_sma_negative_period() -> None:
    bars = [_bar(10)]
    assert compute_sma(bars, period=-1) is None
