"""Unit tests for technical indicators module."""

import pytest
import pandas as pd
import numpy as np

from openclaw.indicators import (
    sma,
    ema,
    rsi,
    macd,
    bollinger_bands,
)


@pytest.fixture
def sample_prices() -> pd.Series:
    """Generate sample stock prices for testing."""
    np.random.seed(42)
    # Generate 100 days of synthetic price data
    returns = np.random.normal(0.001, 0.02, 100)
    prices = 100 * np.exp(np.cumsum(returns))
    return pd.Series(prices)


@pytest.fixture
def real_stock_data() -> pd.Series:
    """Simulate realistic stock price data."""
    # Create prices that resemble real stock movement
    dates = pd.date_range(start="2024-01-01", periods=60, freq="D")
    base_price = 150.0
    trend = np.linspace(0, 10, 60)
    noise = np.random.normal(0, 2, 60)
    prices = base_price + trend + np.cumsum(noise * 0.1)
    return pd.Series(prices, index=dates)


class TestSMA:
    """Tests for Simple Moving Average."""

    def test_sma_basic(self, sample_prices: pd.Series) -> None:
        """Test SMA calculation with basic parameters."""
        period = 20
        result = sma(sample_prices, period)

        # First (period-1) values should be NaN
        assert result.iloc[: period - 1].isna().all()
        # Remaining values should not be NaN
        assert result.iloc[period - 1 :].notna().all()

    def test_sma_known_values(self) -> None:
        """Test SMA against known calculated values."""
        prices = pd.Series([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
        result = sma(prices, period=3)

        # Expected values: NaN, NaN, 2, 3, 4, 5, 6, 7, 8, 9
        expected = pd.Series([np.nan, np.nan, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
        pd.testing.assert_series_equal(result, expected)

    def test_sma_with_real_data(self, real_stock_data: pd.Series) -> None:
        """Test SMA with realistic stock data."""
        result = sma(real_stock_data, period=20)

        assert len(result) == len(real_stock_data)
        # First 19 values should be NaN
        assert result.iloc[:19].isna().all()
        # From day 20 onwards should have values
        assert result.iloc[19:].notna().all()


class TestEMA:
    """Tests for Exponential Moving Average."""

    def test_ema_basic(self, sample_prices: pd.Series) -> None:
        """Test EMA calculation with basic parameters."""
        period = 20
        result = ema(sample_prices, period)

        assert len(result) == len(sample_prices)
        # First (period-1) values should be NaN
        assert result.iloc[: period - 1].isna().all()
        # Remaining values should not be NaN
        assert result.iloc[period - 1 :].notna().all()

    def test_ema_responds_faster_than_sma(self, sample_prices: pd.Series) -> None:
        """Test that EMA responds faster to price changes than SMA."""
        period = 10
        sma_result = sma(sample_prices, period)
        ema_result = ema(sample_prices, period)

        # Calculate the rate of change
        sma_change = sma_result.diff().abs().mean()
        ema_change = ema_result.diff().abs().mean()

        # EMA should generally change more than SMA
        assert ema_change >= sma_change * 0.9  # Allow some tolerance

    def test_ema_with_real_data(self, real_stock_data: pd.Series) -> None:
        """Test EMA with realistic stock data."""
        period = 12
        result = ema(real_stock_data, period)

        assert len(result) == len(real_stock_data)
        # First (period-1) values should be NaN
        assert result.iloc[: period - 1].isna().all()
        # Remaining values should not be NaN
        assert result.iloc[period - 1 :].notna().all()


class TestRSI:
    """Tests for Relative Strength Index."""

    def test_rsi_range(self, sample_prices: pd.Series) -> None:
        """Test that RSI values are within 0-100 range."""
        result = rsi(sample_prices, period=14)

        valid_values = result.dropna()
        assert (valid_values >= 0).all()
        assert (valid_values <= 100).all()

    def test_rsi_strong_uptrend(self) -> None:
        """Test RSI in a strong uptrend (should approach 100)."""
        # Strong uptrend prices
        prices = pd.Series([100, 105, 110, 115, 120, 125, 130, 135, 140, 145])
        result = rsi(prices, period=5)

        # Last RSI value should be high (strong uptrend)
        assert result.iloc[-1] > 70

    def test_rsi_strong_downtrend(self) -> None:
        """Test RSI in a strong downtrend (should approach 0)."""
        # Strong downtrend prices
        prices = pd.Series([150, 145, 140, 135, 130, 125, 120, 115, 110, 105])
        result = rsi(prices, period=5)

        # Last RSI value should be low (strong downtrend)
        assert result.iloc[-1] < 30

    def test_rsi_with_real_data(self, real_stock_data: pd.Series) -> None:
        """Test RSI with realistic stock data."""
        result = rsi(real_stock_data, period=14)

        assert len(result) == len(real_stock_data)
        valid_values = result.dropna()
        assert (valid_values >= 0).all()
        assert (valid_values <= 100).all()


class TestMACD:
    """Tests for MACD indicator."""

    def test_macd_structure(self, sample_prices: pd.Series) -> None:
        """Test that MACD returns correct structure."""
        result = macd(sample_prices)

        assert "macd" in result
        assert "signal" in result
        assert "histogram" in result

        for key in ["macd", "signal", "histogram"]:
            assert isinstance(result[key], pd.Series)
            assert len(result[key]) == len(sample_prices)

    def test_macd_histogram_calculation(self, sample_prices: pd.Series) -> None:
        """Test that histogram is correctly calculated as MACD - Signal."""
        result = macd(sample_prices)

        expected_histogram = result["macd"] - result["signal"]
        pd.testing.assert_series_equal(result["histogram"], expected_histogram)

    def test_macd_custom_periods(self, real_stock_data: pd.Series) -> None:
        """Test MACD with custom periods."""
        result = macd(real_stock_data, fast_period=8, slow_period=17, signal_period=9)

        assert "macd" in result
        assert "signal" in result
        assert "histogram" in result

        # All series should have same length
        assert len(result["macd"]) == len(real_stock_data)
        assert len(result["signal"]) == len(real_stock_data)
        assert len(result["histogram"]) == len(real_stock_data)


class TestBollingerBands:
    """Tests for Bollinger Bands indicator."""

    def test_bollinger_structure(self, sample_prices: pd.Series) -> None:
        """Test that Bollinger Bands returns correct structure."""
        result = bollinger_bands(sample_prices)

        assert "upper" in result
        assert "middle" in result
        assert "lower" in result

        for key in ["upper", "middle", "lower"]:
            assert isinstance(result[key], pd.Series)
            assert len(result[key]) == len(sample_prices)

    def test_bollinger_relationships(self, sample_prices: pd.Series) -> None:
        """Test the mathematical relationships between bands."""
        result = bollinger_bands(sample_prices, period=20, std_dev=2.0)

        valid_idx = result["middle"].notna()

        # Upper band should always be >= middle band
        assert (result["upper"][valid_idx] >= result["middle"][valid_idx]).all()

        # Lower band should always be <= middle band
        assert (result["lower"][valid_idx] <= result["middle"][valid_idx]).all()

        # Upper band should be > lower band
        assert (result["upper"][valid_idx] > result["lower"][valid_idx]).all()

    def test_bollinger_middle_is_sma(self, sample_prices: pd.Series) -> None:
        """Test that middle band equals SMA."""
        period = 20
        result = bollinger_bands(sample_prices, period=period)
        sma_result = sma(sample_prices, period)

        pd.testing.assert_series_equal(result["middle"], sma_result)

    def test_bollinger_with_real_data(self, real_stock_data: pd.Series) -> None:
        """Test Bollinger Bands with realistic stock data."""
        result = bollinger_bands(real_stock_data, period=20, std_dev=2.0)

        assert len(result["upper"]) == len(real_stock_data)
        assert len(result["middle"]) == len(real_stock_data)
        assert len(result["lower"]) == len(real_stock_data)

        # Check band width varies (not constant)
        band_width = result["upper"] - result["lower"]
        valid_width = band_width.dropna()
        assert valid_width.std() > 0  # Band width should vary


class TestIndicatorsWithRealWorldScenarios:
    """Tests using real-world market scenario data."""

    def test_trending_market_indicators(self) -> None:
        """Test indicators in a trending market scenario."""
        # Create trending market data
        np.random.seed(123)
        trend = np.cumsum(np.random.normal(0.5, 1.5, 50))
        prices = pd.Series(100 + trend)

        # Calculate all indicators
        sma_result = sma(prices, 10)
        ema_result = ema(prices, 10)
        rsi_result = rsi(prices, 14)
        macd_result = macd(prices)
        bb_result = bollinger_bands(prices, 20)

        # Verify all produced valid results
        assert sma_result.dropna().shape[0] > 0
        assert ema_result.dropna().shape[0] > 0
        assert rsi_result.dropna().shape[0] > 0
        assert macd_result["macd"].dropna().shape[0] > 0
        assert bb_result["upper"].dropna().shape[0] > 0

    def test_volatile_market_indicators(self) -> None:
        """Test indicators in a volatile market scenario."""
        # Create volatile market data
        np.random.seed(456)
        volatility = np.random.normal(0, 3, 50)
        prices = pd.Series(100 + np.cumsum(volatility))

        # Calculate RSI - should show more variation in volatile markets
        rsi_result = rsi(prices, 14)

        # Calculate Bollinger Bands - should be wider in volatile markets
        bb_result = bollinger_bands(prices, 20)

        valid_rsi = rsi_result.dropna()
        assert valid_rsi.max() - valid_rsi.min() > 20  # RSI should vary significantly

        # Bollinger band width should be significant
        bb_width = (bb_result["upper"] - bb_result["lower"]).dropna()
        assert bb_width.mean() > 0

    def test_sideways_market_indicators(self) -> None:
        """Test indicators in a sideways/ranging market."""
        # Create sideways market data (mean-reverting)
        np.random.seed(789)
        noise = np.random.normal(0, 1, 50)
        prices = pd.Series(100 + noise)

        # In sideways market, RSI should tend toward middle (around 50)
        rsi_result = rsi(prices, 14)
        valid_rsi = rsi_result.dropna()

        # Mean should be close to 50 in sideways market
        assert 40 < valid_rsi.mean() < 60
