"""Tests for slope indicator."""

from __future__ import annotations

import numpy as np
import pandas as pd
import pytest

from indikator.slope import slope


class TestBasicFunctionality:
    """Test core functionality of slope."""

    def test_returns_series_from_dataframe(
        self, simple_uptrend_df: pd.DataFrame
    ) -> None:
        """Should return a Series when input is a DataFrame."""
        result = slope(simple_uptrend_df["close"], window=3)

        assert isinstance(result, pd.Series)
        assert result.name == "close"
        assert len(result) == len(simple_uptrend_df)

    def test_returns_series_from_series(self, simple_uptrend_df: pd.DataFrame) -> None:
        """Should return a Series when input is a Series."""
        s = simple_uptrend_df["close"]
        result = slope(s, window=3)

        assert isinstance(result, pd.Series)
        assert result.name == "close"
        assert len(result) == len(s)

    def test_custom_column_selection(self) -> None:
        """Should select correct column from DataFrame."""
        df = pd.DataFrame({
            "close": [100.0, 102.0, 105.0, 108.0],
            "vwap": [101.0, 103.0, 106.0, 109.0],
        })

        result = slope(df["vwap"], window=3)
        assert result.name == "vwap"
        # Should match slopes of vwap data
        assert not pd.isna(result.iloc[2])

    def test_output_length_matches_input(self, simple_uptrend_df: pd.DataFrame) -> None:
        """Output should have same length as input."""
        result = slope(simple_uptrend_df["close"], window=3)
        assert len(result) == len(simple_uptrend_df)

    def test_empty_dataframe_returns_empty_series(self) -> None:
        """Empty input should raise ValueError."""
        with pytest.raises(ValueError, match="Data must not be empty"):
            slope(pd.Series(dtype=float))


class TestSlopeCalculation:
    """Test slope calculation logic."""

    def test_uptrend_has_positive_slope(self, simple_uptrend_df: pd.DataFrame) -> None:
        """Uptrend should produce positive slope values."""
        result = slope(simple_uptrend_df["close"], window=3)

        # Get non-NaN values (after window is satisfied)
        slopes = result.dropna()
        assert (slopes > 0).all()

    def test_downtrend_has_negative_slope(
        self, simple_downtrend_df: pd.DataFrame
    ) -> None:
        """Downtrend should produce negative slope values."""
        result = slope(simple_downtrend_df["close"], window=3)

        slopes = result.dropna()
        assert (slopes < 0).all()

    def test_flat_prices_have_zero_slope(self, flat_prices_df: pd.DataFrame) -> None:
        """Constant prices should result in zero slope."""
        result = slope(flat_prices_df["close"], window=3)

        slopes = result.dropna()
        # Allow small numerical errors
        assert np.allclose(slopes, 0.0, atol=1e-10)

    def test_linear_growth_has_constant_slope(self) -> None:
        """Perfect linear growth should have constant slope."""
        # Linear: 100, 102, 104, 106, 108, 110
        data = pd.Series(np.linspace(100, 110, 6), name="close")
        result = slope(data, window=3)

        slopes = result.dropna()
        # All slopes should be approximately equal
        assert np.allclose(slopes, slopes.iloc[0], rtol=1e-10)

    def test_steeper_trend_has_larger_slope_magnitude(self) -> None:
        """Steeper trends should have larger absolute slope values."""
        # Gentle trend
        gentle = pd.Series([100.0, 101.0, 102.0, 103.0, 104.0], name="close")
        result_gentle = slope(gentle, window=3)

        # Steep trend
        steep = pd.Series([100.0, 105.0, 110.0, 115.0, 120.0], name="close")
        result_steep = slope(steep, window=3)

        avg_slope_gentle = result_gentle.dropna().mean()
        avg_slope_steep = result_steep.dropna().mean()

        assert avg_slope_steep > avg_slope_gentle

    def test_window_parameter_affects_smoothness(self) -> None:
        """Larger window should produce smoother slope values."""
        # Noisy data
        np.random.seed(42)
        data = pd.Series(100 + np.cumsum(np.random.randn(50) * 0.5 + 0.5), name="close")

        result_small = slope(data, window=3)
        result_large = slope(data, window=10)

        # Larger window should have less variation (smoother)
        std_small = result_small.dropna().std()
        std_large = result_large.dropna().std()

        assert std_small > std_large


class TestNaNHandling:
    """Test NaN value handling."""

    def test_insufficient_data_returns_nan(self) -> None:
        """Should return NaN for bars before window is satisfied."""
        data = pd.Series([100.0, 102.0, 105.0, 108.0], name="close")
        result = slope(data, window=3)

        # First 2 values should be NaN (window=3 needs 3 bars)
        assert pd.isna(result.iloc[0])
        assert pd.isna(result.iloc[1])
        # Third value onward should be valid
        assert not pd.isna(result.iloc[2])

    def test_single_value_returns_nan(self, single_value_df: pd.DataFrame) -> None:
        """Single value should return NaN (insufficient for slope)."""
        result = slope(single_value_df["close"], window=3)
        assert pd.isna(result.iloc[0])


class TestEdgeCases:
    """Test edge cases and error handling."""

    def test_missing_column_raises_error(self, simple_uptrend_df: pd.DataFrame) -> None:
        """Should raise ValueError if column doesn't exist."""
        with pytest.raises(KeyError):
            slope(simple_uptrend_df["nonexistent"], window=3)

    def test_minimum_window_size(self) -> None:
        """Should work with minimum window size of 2."""
        data = pd.Series([100.0, 105.0, 110.0], name="close")
        result = slope(data, window=2)

        # Should have valid slopes starting from index 1
        assert not pd.isna(result.iloc[1])

    def test_large_dataset_performance(self, large_dataset_df: pd.DataFrame) -> None:
        """Should handle large datasets efficiently (Numba optimization)."""
        result = slope(large_dataset_df["close"], window=20)

        assert len(result) == len(large_dataset_df)

    def test_infinite_values_raise_error(self) -> None:
        """Should raise SchemaError if data contains infinite values."""
        data = pd.Series([100.0, np.inf, 105.0], name="close")
        with pytest.raises(ValueError):
            slope(data)


class TestParameterValidation:
    """Test parameter validation and constraints."""

    def test_default_parameters_work(self, simple_uptrend_df: pd.DataFrame) -> None:
        """Should work with all default parameters."""
        result = slope(simple_uptrend_df["close"])
        assert isinstance(result, pd.Series)

    def test_custom_window(self, simple_uptrend_df: pd.DataFrame) -> None:
        """Should accept custom window parameter."""
        result = slope(simple_uptrend_df["close"], window=2)
        assert len(result) == len(simple_uptrend_df)


class TestTrendStrength:
    """Test interpretation of slope magnitude."""

    def test_accelerating_trend_increases_slope(self) -> None:
        """Accelerating uptrend should show increasing slope values."""
        # Accelerating: 100, 101, 103, 106, 110
        data = pd.Series([100.0, 101.0, 103.0, 106.0, 110.0], name="close")
        result = slope(data, window=3)

        slopes = result.dropna().values
        # Later slopes should be larger (accelerating)
        assert slopes[-1] > slopes[0]

    def test_decelerating_trend_decreases_slope(self) -> None:
        """Decelerating uptrend should show decreasing slope values."""
        # Decelerating: 100, 105, 108, 110, 111
        data = pd.Series([100.0, 105.0, 108.0, 110.0, 111.0], name="close")
        result = slope(data, window=3)

        slopes = result.dropna().values
        # Later slopes should be smaller (decelerating)
        assert slopes[-1] < slopes[0]


class TestOHLCVData:
    """Test with full OHLCV data."""

    @pytest.fixture
    def ohlcv_df(self) -> pd.DataFrame:
        """Sample OHLCV data."""
        return pd.DataFrame({
            "open": [100.0, 102.0, 105.0, 108.0, 110.0],
            "high": [103.0, 106.0, 108.0, 111.0, 113.0],
            "low": [99.0, 101.0, 104.0, 107.0, 109.0],
            "close": [102.0, 105.0, 107.0, 110.0, 112.0],
            "volume": [1000, 1500, 1200, 1300, 1100],
        })

    def test_works_with_ohlcv_data(self, ohlcv_df: pd.DataFrame) -> None:
        """Should work with full OHLCV DataFrame."""
        result = slope(ohlcv_df["close"], window=3)
        assert isinstance(result, pd.Series)
        assert result.name == "close"

    def test_can_calculate_slope_for_high(self, ohlcv_df: pd.DataFrame) -> None:
        """Should work with high column."""
        result = slope(ohlcv_df["high"], window=3)

        assert result.name == "high"
        slopes = result.dropna()
        assert (slopes > 0).all()  # High prices trending up
