"""Tests for volatility-adaptive barrier methods.

Tests cover:
- Volatility regime detection (LOW/NORMAL/HIGH)
- Volatility ratio computation
- Dynamic barrier suggestion for LONG and SHORT
- Minimum distance enforcement (fixes EURUSD SL=TP bug)
- Degenerate level validation
- Structure-based anchoring
"""

import numpy as np
import pandas as pd
import pytest

from ai.barrier_methods import (
    VolatilityRegime,
    DynamicBarrierResult,
    suggest_dynamic_barriers,
    validate_barrier_levels,
    compute_volatility_regime,
    compute_volatility_ratio,
    barriers_to_dict,
    MIN_GAP_PCT,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def normal_vol_atr() -> pd.Series:
    """ATR series with last value in middle percentile (NORMAL vol)."""
    np.random.seed(42)
    values = np.random.randn(200) * 0.3 + 1.0
    s = pd.Series(values).clip(0.3, 3.0)
    # Force last value to be near the median
    s.iloc[-1] = s.median()
    return s


@pytest.fixture
def low_vol_atr() -> pd.Series:
    """ATR series with last value in bottom 33rd percentile (LOW vol)."""
    np.random.seed(99)
    # Most values are around 1.0, but last 20 are much lower
    values = np.concatenate([
        np.random.randn(180) * 0.3 + 1.0,  # normal vol
        np.ones(20) * 0.3 + np.random.randn(20) * 0.01,  # very low vol at end
    ])
    return pd.Series(values).clip(0.2, 3.0)


@pytest.fixture
def high_vol_atr() -> pd.Series:
    """ATR series with last value in top 67th percentile (HIGH vol)."""
    np.random.seed(99)
    # Most values are around 0.5, but last 20 are much higher
    values = np.concatenate([
        np.random.randn(180) * 0.2 + 0.5,  # normal-low vol
        np.ones(20) * 2.5 + np.random.randn(20) * 0.05,  # very high vol at end
    ])
    return pd.Series(values).clip(0.2, 4.0)


@pytest.fixture
def sample_ohlcv() -> pd.DataFrame:
    """Sample OHLCV DataFrame for structure-based tests."""
    np.random.seed(42)
    n = 200
    close = 100 + np.cumsum(np.random.randn(n) * 0.5)
    high = close + np.abs(np.random.randn(n) * 0.3)
    low = close - np.abs(np.random.randn(n) * 0.3)
    return pd.DataFrame({
        "timestamp": np.arange(n),
        "Open": close,
        "High": high,
        "Low": low,
        "Close": close,
        "Volume": np.ones(n) * 1000,
    })


# ---------------------------------------------------------------------------
# Test: VolatilityRegime enum
# ---------------------------------------------------------------------------


class TestVolatilityRegime:
    def test_enum_values(self):
        assert VolatilityRegime.LOW.value == "LOW"
        assert VolatilityRegime.NORMAL.value == "NORMAL"
        assert VolatilityRegime.HIGH.value == "HIGH"

    def test_enum_from_string(self):
        assert VolatilityRegime("LOW") == VolatilityRegime.LOW
        assert VolatilityRegime("NORMAL") == VolatilityRegime.NORMAL
        assert VolatilityRegime("HIGH") == VolatilityRegime.HIGH


# ---------------------------------------------------------------------------
# Test: DynamicBarrierResult dataclass
# ---------------------------------------------------------------------------


class TestDynamicBarrierResult:
    def test_default_creation(self):
        result = DynamicBarrierResult(
            sl_price=99.0,
            tp_price=101.0,
            sl_distance_price=1.0,
            tp_distance_price=1.0,
            sl_multiplier=1.0,
            tp_multiplier=1.5,
            volatility_regime=VolatilityRegime.NORMAL,
            volatility_ratio=1.0,
            atr_at_entry=1.0,
        )
        assert result.sl_price == 99.0
        assert result.tp_price == 101.0
        assert result.min_distance_applied is False
        assert result.structure_anchored is False
        assert result.min_gap_applied is False

    def test_with_flags(self):
        result = DynamicBarrierResult(
            sl_price=98.0, tp_price=103.0,
            sl_distance_price=2.0, tp_distance_price=3.0,
            sl_multiplier=0.8, tp_multiplier=1.2,
            volatility_regime=VolatilityRegime.LOW,
            volatility_ratio=0.4,
            atr_at_entry=2.5,
            min_distance_applied=True,
            structure_anchored=True,
            min_gap_applied=False,
        )
        assert result.min_distance_applied is True
        assert result.structure_anchored is True
        assert result.min_gap_applied is False


# ---------------------------------------------------------------------------
# Test: compute_volatility_regime
# ---------------------------------------------------------------------------


class TestComputeVolatilityRegime:
    def test_normal_regime(self, normal_vol_atr):
        regime = compute_volatility_regime(normal_vol_atr)
        # With random walk, mid values should be NORMAL
        assert regime in (VolatilityRegime.LOW, VolatilityRegime.NORMAL, VolatilityRegime.HIGH)

    def test_low_regime(self, low_vol_atr):
        regime = compute_volatility_regime(low_vol_atr)
        assert regime == VolatilityRegime.LOW

    def test_high_regime(self, high_vol_atr):
        regime = compute_volatility_regime(high_vol_atr)
        assert regime == VolatilityRegime.HIGH

    def test_short_series_defaults_to_normal(self):
        short_series = pd.Series([1.0, 1.1, 1.2])
        regime = compute_volatility_regime(short_series, lookback=100)
        assert regime == VolatilityRegime.NORMAL

    def test_flat_series(self):
        flat = pd.Series(np.ones(100))
        regime = compute_volatility_regime(flat)
        # All values equal → percentile = 0 → LOW
        assert regime in (VolatilityRegime.LOW, VolatilityRegime.NORMAL)


# ---------------------------------------------------------------------------
# Test: compute_volatility_ratio
# ---------------------------------------------------------------------------


class TestComputeVolatilityRatio:
    def test_ratio_near_one(self, normal_vol_atr):
        ratio = compute_volatility_ratio(normal_vol_atr)
        # With moderate random data, ratio should be reasonable
        assert 0.5 <= ratio <= 1.5

    def test_ratio_low(self, low_vol_atr):
        # Low vol at end → ratio should be < 1
        ratio = compute_volatility_ratio(low_vol_atr)
        # The low vol series has mean of ~0.3, should be lower than long-term
        assert ratio < 1.5

    def test_short_series_returns_one(self):
        short = pd.Series([1.0, 1.0])
        ratio = compute_volatility_ratio(short, long_period=100)
        assert ratio == 1.0


# ---------------------------------------------------------------------------
# Test: suggest_dynamic_barriers — LONG
# ---------------------------------------------------------------------------


class TestSuggestDynamicBarriersLong:
    def test_basic_long(self, normal_vol_atr, sample_ohlcv):
        result = suggest_dynamic_barriers(
            entry_price=100.0, side="LONG", atr_val=1.0,
            atr_series=normal_vol_atr, df=sample_ohlcv, idx=150,
        )
        assert result.sl_price < 100.0, "SL must be below entry for LONG"
        assert result.tp_price > 100.0, "TP must be above entry for LONG"
        assert result.sl_price < result.tp_price, "SL must be below TP"
        assert result.atr_at_entry == 1.0
        assert result.sl_distance_price > 0
        assert result.tp_distance_price > 0

    def test_long_no_structure(self, normal_vol_atr):
        """Without OHLCV data, should still produce valid barriers."""
        result = suggest_dynamic_barriers(
            entry_price=100.0, side="LONG", atr_val=1.0,
            atr_series=normal_vol_atr, use_structure=False,
        )
        assert result.sl_price < 100.0
        assert result.tp_price > 100.0
        assert result.sl_price < result.tp_price
        assert result.structure_anchored is False

    def test_long_tiny_atr(self):
        """Near-zero ATR should trigger minimum distance enforcement."""
        tiny_atr = pd.Series(np.ones(200) * 0.0001)
        result = suggest_dynamic_barriers(
            entry_price=1.16, side="LONG", atr_val=0.0001,
            atr_series=tiny_atr, use_structure=False,
        )
        assert result.sl_price < 1.16
        assert result.tp_price > 1.16
        assert result.sl_price < result.tp_price
        # Minimum distance should be enforced
        gap = result.tp_price - result.sl_price
        min_expected = 1.16 * MIN_GAP_PCT
        assert gap >= min_expected * 0.999, f"Gap {gap} < min {min_expected}"
        assert result.min_distance_applied or result.min_gap_applied, (
            "Should have applied minimum distance enforcement"
        )

    def test_long_multipliers_by_regime(self, low_vol_atr, high_vol_atr, normal_vol_atr):
        """Different regimes should produce different multiplier values."""
        # LOW vol → tighter multipliers
        r_low = suggest_dynamic_barriers(
            100.0, "LONG", 1.0, low_vol_atr, use_structure=False,
        )
        # HIGH vol → wider multipliers
        r_high = suggest_dynamic_barriers(
            100.0, "LONG", 1.0, high_vol_atr, use_structure=False,
        )
        # HIGH vol should have wider (larger) multipliers
        assert r_high.sl_multiplier >= r_low.sl_multiplier, (
            f"High vol SL mult {r_high.sl_multiplier} should be >= low vol {r_low.sl_multiplier}"
        )
        assert r_high.tp_multiplier >= r_low.tp_multiplier, (
            f"High vol TP mult {r_high.tp_multiplier} should be >= low vol {r_low.tp_multiplier}"
        )


# ---------------------------------------------------------------------------
# Test: suggest_dynamic_barriers — SHORT
# ---------------------------------------------------------------------------


class TestSuggestDynamicBarriersShort:
    def test_basic_short(self, normal_vol_atr, sample_ohlcv):
        result = suggest_dynamic_barriers(
            entry_price=100.0, side="SHORT", atr_val=1.0,
            atr_series=normal_vol_atr, df=sample_ohlcv, idx=150,
        )
        assert result.sl_price > 100.0, "SL must be above entry for SHORT"
        assert result.tp_price < 100.0, "TP must be below entry for SHORT"
        assert result.sl_price > result.tp_price, "SL must be above TP for SHORT"

    def test_short_no_structure(self, normal_vol_atr):
        result = suggest_dynamic_barriers(
            entry_price=100.0, side="SHORT", atr_val=1.0,
            atr_series=normal_vol_atr, use_structure=False,
        )
        assert result.sl_price > 100.0
        assert result.tp_price < 100.0
        assert result.sl_price > result.tp_price

    def test_short_tiny_atr(self):
        """Near-zero ATR: SHORT should also maintain minimum gap."""
        tiny_atr = pd.Series(np.ones(200) * 0.0001)
        result = suggest_dynamic_barriers(
            entry_price=1.16, side="SHORT", atr_val=0.0001,
            atr_series=tiny_atr, use_structure=False,
        )
        assert result.sl_price > 1.16
        assert result.tp_price < 1.16
        gap = result.sl_price - result.tp_price
        assert gap > 0, f"Gap must be positive, got {gap}"
        assert result.min_gap_applied or result.min_distance_applied


# ---------------------------------------------------------------------------
# Test: validate_barrier_levels
# ---------------------------------------------------------------------------


class TestValidateBarrierLevels:
    def test_long_valid_no_change(self):
        """Valid LONG barriers — RR enforcement scales TP to meet MIN_RR=1.5."""
        sl, tp = validate_barrier_levels(99.0, 101.0, 100.0, "LONG", 1.0)
        assert sl == 99.0
        # RR=1.0 (SL=1.0, TP=1.0) < MIN_RR=1.5 → TP scaled to 1.5 → 101.5
        assert tp == 101.5

    def test_short_valid_no_change(self):
        sl, tp = validate_barrier_levels(101.0, 99.0, 100.0, "SHORT", 1.0)
        assert sl == 101.0
        # RR=1.0 (SL=1.0, TP=1.0) < MIN_RR=1.5 → TP scaled to 1.5 → 98.5
        assert tp == 98.5

    def test_long_degenerate_equal(self):
        """SL==TP should be corrected."""
        sl, tp = validate_barrier_levels(100.0, 100.0, 100.0, "LONG", 1.0)
        assert sl != tp
        assert sl < 100.0 < tp

    def test_short_degenerate_equal(self):
        sl, tp = validate_barrier_levels(100.0, 100.0, 100.0, "SHORT", 1.0)
        assert sl != tp
        assert sl > 100.0 > tp

    def test_long_swapped(self):
        """SL > TP for LONG should be corrected."""
        sl, tp = validate_barrier_levels(101.0, 99.0, 100.0, "LONG", 1.0)
        assert sl < tp
        assert sl < 100.0 < tp

    def test_short_swapped(self):
        sl, tp = validate_barrier_levels(99.0, 101.0, 100.0, "SHORT", 1.0)
        assert sl > tp
        assert sl > 100.0 > tp

    def test_eurusd_bug_fix(self):
        """Exact case from logs: EURUSD SL=TP=1.16 should be fixed."""
        sl, tp = validate_barrier_levels(1.16, 1.16, 1.16, "SHORT", 0.0001)
        assert sl != tp, "SL and TP must not be equal!"
        assert sl > 1.16, "SL must be above entry for SHORT"
        assert tp < 1.16, "TP must be below entry for SHORT"
        gap = sl - tp
        assert gap > 0, f"Gap must be positive, got {gap}"


# ---------------------------------------------------------------------------
# Test: barriers_to_dict
# ---------------------------------------------------------------------------


class TestBarriersToDict:
    def test_dict_conversion(self, normal_vol_atr):
        result = suggest_dynamic_barriers(
            entry_price=100.0, side="LONG", atr_val=1.0,
            atr_series=normal_vol_atr, use_structure=False,
        )
        d = barriers_to_dict(result)
        assert isinstance(d, dict)
        assert "sl_price" in d
        assert "tp_price" in d
        assert "sl_multiplier" in d
        assert "tp_multiplier" in d
        assert "volatility_regime" in d
        assert "volatility_ratio" in d
        assert "atr_at_entry" in d
        assert len(d) == 12

    def test_round_trip_types(self, normal_vol_atr):
        result = suggest_dynamic_barriers(
            entry_price=100.0, side="SHORT", atr_val=1.0,
            atr_series=normal_vol_atr, use_structure=False,
        )
        d = barriers_to_dict(result)
        assert isinstance(d["sl_price"], float)
        assert isinstance(d["sl_multiplier"], float)
        assert isinstance(d["volatility_regime"], str)
        assert isinstance(d["structure_anchored"], bool)


# ---------------------------------------------------------------------------
# Test: structure-based anchoring
# ---------------------------------------------------------------------------


class TestStructureBasedBarriers:
    def test_structure_long_uses_swing_low(self, sample_ohlcv, normal_vol_atr):
        """Structure-based LONG should place SL at or below swing low."""
        idx = 150
        # Ensure there's a low point before idx
        swing_low = sample_ohlcv["Low"].iloc[idx - 20: idx + 1].min()
        
        result = suggest_dynamic_barriers(
            entry_price=sample_ohlcv["Close"].iloc[idx],
            side="LONG",
            atr_val=1.0,
            atr_series=normal_vol_atr,
            df=sample_ohlcv,
            idx=idx,
            use_structure=True,
        )
        assert result.sl_price < result.tp_price
        assert result.structure_anchored

    def test_structure_short_uses_swing_high(self, sample_ohlcv, normal_vol_atr):
        idx = 150
        result = suggest_dynamic_barriers(
            entry_price=sample_ohlcv["Close"].iloc[idx],
            side="SHORT",
            atr_val=1.0,
            atr_series=normal_vol_atr,
            df=sample_ohlcv,
            idx=idx,
            use_structure=True,
        )
        assert result.sl_price > result.tp_price
        assert result.structure_anchored

    def test_no_structure_without_df(self, normal_vol_atr):
        """Without OHLCV, structure_anchored should be False."""
        result = suggest_dynamic_barriers(
            entry_price=100.0, side="LONG", atr_val=1.0,
            atr_series=normal_vol_atr, use_structure=True,
        )
        assert result.structure_anchored is False
