"""Tests for core strategy logic: trend detection, entry signals, risk management."""

from __future__ import annotations

import pytest
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from core.trend_analysis import (
    Candle,
    Level,
    TrendDirection,
    detect_trend,
    detect_trend_reversal,
    detect_support_resistance,
    _sma,
    _ema,
    _rsi,
    _linear_regression_slope,
)
from core.candle_patterns import (
    PatternType,
    detect_all_patterns,
    detect_bullish_engulfing,
    detect_bearish_engulfing,
    detect_morning_star,
    detect_evening_star,
    detect_hammer,
    detect_shooting_star,
    detect_doji,
    detect_three_white_soldiers,
    detect_three_black_crows,
)
from core.entry_detection import (
    EntrySignal,
    SignalType,
    detect_entry,
    detect_entry_buy,
    detect_entry_sell,
)
from core.risk_calculator import (
    RiskParameters,
    RiskResult,
    calculate_stop_loss_take_profit,
    calculate_position_size,
    calculate_atr,
    validate_trade,
    full_risk_analysis,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
def make_candle(ts: int, o: float, h: float, l: float, c: float, v: float = 1000.0) -> Candle:
    """Helper to create a Candle."""
    return Candle(timestamp=ts, open=o, high=h, low=l, close=c, volume=v)


def uptrend_candles(count: int = 30) -> list:
    """Generate a simple uptrend series."""
    candles = []
    price = 100.0
    for i in range(count):
        o = price
        c = price + 0.5 + (i * 0.01)
        h = c + 0.3
        l = o - 0.2
        candles.append(make_candle(1000000 + i * 3600, o, h, l, c))
        price = c
    return candles


def downtrend_candles(count: int = 30) -> list:
    """Generate a simple downtrend series."""
    candles = []
    price = 200.0
    for i in range(count):
        o = price
        c = price - 0.5 - (i * 0.01)
        h = o + 0.2
        l = c - 0.3
        candles.append(make_candle(1000000 + i * 3600, o, h, l, c))
        price = c
    return candles


def sideways_candles(count: int = 30) -> list:
    """Generate a sideways/neutral series."""
    candles = []
    import random
    random.seed(42)
    price = 150.0
    for i in range(count):
        o = price
        c = price + random.uniform(-0.5, 0.5)
        h = max(o, c) + random.uniform(0, 0.3)
        l = min(o, c) - random.uniform(0, 0.3)
        candles.append(make_candle(1000000 + i * 3600, o, h, l, c))
        price = c
    return candles


# ---------------------------------------------------------------------------
# Trend Analysis Tests
# ---------------------------------------------------------------------------
class TestSMACalculations:
    def test_sma_basic(self):
        values = [1, 2, 3, 4, 5]
        result = _sma(values, 3)
        assert len(result) == 3
        assert result[0] == 2.0  # (1+2+3)/3
        assert result[1] == 3.0  # (2+3+4)/3
        assert result[2] == 4.0  # (3+4+5)/3

    def test_sma_insufficient_data(self):
        values = [1, 2]
        result = _sma(values, 3)
        assert result == []

    def test_sma_period_equals_length(self):
        values = [1, 2, 3]
        result = _sma(values, 3)
        assert len(result) == 1
        assert result[0] == 2.0


class TestEMACalculations:
    def test_ema_basic(self):
        values = [1, 2, 3, 4, 5]
        result = _ema(values, 3)
        assert len(result) == 5
        assert result[0] == 1.0

    def test_ema_insufficient_data(self):
        values = [1]
        result = _ema(values, 3)
        assert len(result) == 0


class TestRSI:
    def test_rsi_rising(self):
        closes = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114]
        rsi = _rsi(closes, 14)
        assert rsi is not None
        assert rsi > 70  # Strong uptrend

    def test_rsi_falling(self):
        closes = [114, 113, 112, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100]
        rsi = _rsi(closes, 14)
        assert rsi is not None
        assert rsi < 30  # Strong downtrend

    def test_rsi_flat(self):
        closes = [100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100]
        rsi = _rsi(closes, 14)
        assert rsi is not None
        assert rsi == 100.0 or abs(rsi - 100.0) < 0.01  # No losses

    def test_rsi_insufficient_data(self):
        closes = [100, 101]
        rsi = _rsi(closes, 14)
        assert rsi is None

    def test_rsi_all_losses(self):
        closes = [100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86]
        rsi = _rsi(closes, 14)
        assert rsi is not None
        assert rsi == 0.0


class TestLinearRegressionSlope:
    def test_positive_slope(self):
        values = [1, 2, 3, 4, 5]
        slope = _linear_regression_slope(values)
        assert slope > 0

    def test_negative_slope(self):
        values = [5, 4, 3, 2, 1]
        slope = _linear_regression_slope(values)
        assert slope < 0

    def test_zero_slope(self):
        values = [3, 3, 3, 3, 3]
        slope = _linear_regression_slope(values)
        assert slope == 0.0

    def test_insufficient_data(self):
        values = [1]
        slope = _linear_regression_slope(values)
        assert slope == 0.0


class TestDetectTrend:
    def test_uptrend_detection(self):
        candles = uptrend_candles(30)
        result = detect_trend(candles, short_period=8, long_period=21)
        assert result.direction == TrendDirection.UPTREND
        assert result.strength > 0

    def test_downtrend_detection(self):
        candles = downtrend_candles(30)
        result = detect_trend(candles, short_period=8, long_period=21)
        assert result.direction == TrendDirection.DOWNTREND
        assert result.strength > 0

    def test_insufficient_candles(self):
        candles = uptrend_candles(5)
        result = detect_trend(candles, short_period=8, long_period=21)
        assert result.direction == TrendDirection.NEUTRAL
        assert result.confidence == 0.0

    def test_trend_with_rsi_disabled(self):
        candles = uptrend_candles(30)
        result = detect_trend(candles, short_period=8, long_period=21, use_rsi=False)
        assert result.rsi is None

    def test_trend_confidence_range(self):
        candles = uptrend_candles(30)
        result = detect_trend(candles)
        assert 0.0 <= result.confidence <= 1.0


class TestDetectTrendReversal:
    def test_no_reversal_in_uptrend(self):
        candles = uptrend_candles(20)
        result = detect_trend_reversal(candles)
        assert result is None  # No reversal in pure uptrend

    def test_bullish_reversal(self):
        """Create RSI oversold then bounce."""
        candles = downtrend_candles(15)
        # Add a bullish candle at the end
        last = candles[-1]
        candles.append(make_candle(
            last.timestamp + 3600,
            last.close - 2,  # Gap down
            last.close - 1,
            last.close - 3,
            last.close - 0.5  # Close up from open
        ))
        result = detect_trend_reversal(candles)
        # May detect bullish reversal
        assert result is None or result == "bullish_reversal"


class TestSupportResistance:
    def test_support_detection(self):
        candles = uptrend_candles(30)
        supports, resistances = detect_support_resistance(candles)
        assert isinstance(supports, list)
        assert isinstance(resistances, list)

    def test_empty_candles(self):
        supports, resistances = detect_support_resistance([])
        assert supports == []
        assert resistances == []

    def test_returns_level_objects(self):
        """Levels should be Level dataclass instances with metadata."""
        candles = _wavy_candles(50)
        supports, resistances = detect_support_resistance(candles, lookback=50, min_touches=1)
        assert all(isinstance(s, Level) for s in supports)
        assert all(isinstance(r, Level) for r in resistances)

    def test_clustering_merges_nearby_levels(self):
        """Levels within 0.5% should be merged into one cluster."""
        candles = _wavy_candles(60)
        supports, resistances = detect_support_resistance(candles, lookback=60)
        for s in supports:
            assert s.touches >= 2, f"Support {s.price} has {s.touches} touches, expected >= 2"

    def test_min_touches_filters_weak(self):
        candles = _wavy_candles(40)
        _, resistances = detect_support_resistance(candles, lookback=40, min_touches=3)
        for r in resistances:
            assert r.touches >= 3

    def test_level_has_timestamps(self):
        candles = _wavy_candles(50)
        supports, _ = detect_support_resistance(candles, lookback=50, min_touches=1)
        for s in supports:
            assert s.first_ts > 0
            assert s.last_ts >= s.first_ts


def _wavy_candles(count: int = 50) -> list:
    """Generate oscillating candles with clear local minima and maxima."""
    import math
    candles = []
    base = 100.0
    for i in range(count):
        wave = 5.0 * math.sin(i * 0.15)
        o = base + wave
        c = base + wave + 0.5 * math.cos(i * 0.3)
        h = max(o, c) + 0.3 + abs(wave * 0.1)
        l = min(o, c) - 0.3 - abs(wave * 0.1)
        candles.append(make_candle(1000000 + i * 3600, o, h, l, c))
    return candles


# ---------------------------------------------------------------------------
# Candle Pattern Tests
# ---------------------------------------------------------------------------
class TestBullishEngulfing:
    def test_detect_bullish_engulfing(self):
        # Bearish candle followed by larger bullish candle
        candles = [
            make_candle(1, 100, 101, 99, 99.5),   # Bearish
            make_candle(2, 98, 105, 97, 104),     # Bullish, engulfs previous
        ]
        result = detect_bullish_engulfing(candles)
        assert result is not None
        assert result.pattern == PatternType.BULLISH_ENGULFING
        assert result.confidence > 0

    def test_no_bullish_engulfing_wrong_order(self):
        candles = [
            make_candle(1, 100, 105, 99, 104),     # Bullish
            make_candle(2, 98, 101, 97, 99.5),     # Bearish
        ]
        result = detect_bullish_engulfing(candles)
        assert result is None

    def test_no_engulfing_small_second(self):
        candles = [
            make_candle(1, 100, 101, 99, 99.5),
            make_candle(2, 99, 100, 98.5, 99.8),   # Too small
        ]
        result = detect_bullish_engulfing(candles)
        assert result is None


class TestBearishEngulfing:
    def test_detect_bearish_engulfing(self):
        candles = [
            make_candle(1, 100, 101, 99, 100.5),   # Bullish
            make_candle(2, 102, 103, 97, 98),      # Bearish, engulfs
        ]
        result = detect_bearish_engulfing(candles)
        assert result is not None
        assert result.pattern == PatternType.BEARISH_ENGULFING

    def test_no_bearish_engulfing(self):
        candles = [
            make_candle(1, 100, 101, 99, 99.5),
            make_candle(2, 98, 103, 97, 102),
        ]
        result = detect_bearish_engulfing(candles)
        assert result is None


class TestMorningStar:
    def test_detect_morning_star(self):
        candles = [
            make_candle(1, 110, 112, 108, 109),    # Long bearish
            make_candle(2, 108, 109, 107.5, 108),  # Small body (gap down)
            make_candle(3, 109, 115, 108.5, 114),  # Long bullish (gap up)
        ]
        result = detect_morning_star(candles)
        assert result is not None
        assert result.pattern == PatternType.MORNING_STAR

    def test_no_morning_star(self):
        candles = [
            make_candle(1, 100, 102, 98, 101),
            make_candle(2, 101, 103, 99, 102),
            make_candle(3, 102, 104, 100, 103),
        ]
        result = detect_morning_star(candles)
        assert result is None


class TestEveningStar:
    def test_detect_evening_star(self):
        candles = [
            make_candle(1, 100, 102, 98, 101),     # Long bullish
            make_candle(2, 102, 103, 101.5, 102),  # Small body (gap up)
            make_candle(3, 101, 103, 97, 98),      # Long bearish (gap down)
        ]
        result = detect_evening_star(candles)
        assert result is not None
        assert result.pattern == PatternType.EVENING_STAR


class TestHammer:
    def test_detect_hammer(self):
        candle = [
            make_candle(1, 100, 100.5, 94, 100.5),   # Small body, long lower shadow, no upper shadow
        ]
        result = detect_hammer(candle)
        assert result is not None
        assert result.pattern == PatternType.HAMMER

    def test_no_hammer_no_shadow(self):
        candle = [
            make_candle(1, 100, 103, 97, 102),  # Normal bullish candle
        ]
        result = detect_hammer(candle)
        assert result is None


class TestShootingStar:
    def test_detect_shooting_star(self):
        candle = [
            make_candle(1, 98.2, 104, 98.15, 98.3),  # Small body, long upper shadow, tiny lower
        ]
        result = detect_shooting_star(candle)
        assert result is not None
        assert result.pattern == PatternType.SHOOTING_STAR


class TestDoji:
    def test_detect_doji(self):
        candle = [
            make_candle(1, 100, 100.5, 99.5, 100),  # Open == close
        ]
        result = detect_doji(candle)
        assert result is not None
        assert result.pattern == PatternType.DOJI

    def test_no_doji_normal_candle(self):
        candle = [
            make_candle(1, 100, 103, 97, 102),
        ]
        result = detect_doji(candle)
        assert result is None


class TestThreeWhiteSoldiers:
    def test_detect_three_white_soldiers(self):
        candles = [
            make_candle(1, 100, 102, 99, 101.5),
            make_candle(2, 101.5, 103, 101, 102.5),
            make_candle(3, 102.5, 104, 102, 103.5),
        ]
        result = detect_three_white_soldiers(candles)
        assert result is not None
        assert result.pattern == PatternType.THREE_WHITE_SOLDIERS


class TestThreeBlackCrows:
    def test_detect_three_black_crows(self):
        candles = [
            make_candle(1, 103, 104, 101, 102),
            make_candle(2, 102, 103, 100, 101),
            make_candle(3, 101, 102, 99, 100),
        ]
        result = detect_three_black_crows(candles)
        assert result is not None
        assert result.pattern == PatternType.THREE_BLACK_CROWS


class TestDetectAllPatterns:
    def test_all_patterns_empty(self):
        candles = [make_candle(1, 100, 101, 99, 100.5)]
        results = detect_all_patterns(candles)
        # May or may not detect single-candle patterns
        assert isinstance(results, list)

    def test_all_patterns_with_many(self):
        candles = uptrend_candles(30)
        results = detect_all_patterns(candles)
        assert isinstance(results, list)


# ---------------------------------------------------------------------------
# Entry Detection Tests
# ---------------------------------------------------------------------------
class TestDetectEntryBuy:
    def test_buy_in_uptrend(self):
        candles = uptrend_candles(30)
        signal = detect_entry_buy(candles)
        assert signal is not None
        assert signal.signal_type == SignalType.BUY
        assert signal.price > 0
        assert 0.0 <= signal.confidence <= 1.0

    def test_no_buy_in_downtrend(self):
        candles = downtrend_candles(30)
        signal = detect_entry_buy(candles)
        assert signal is None

    def test_no_buy_insufficient_data(self):
        candles = uptrend_candles(5)
        signal = detect_entry_buy(candles)
        assert signal is None


class TestDetectEntrySell:
    def test_sell_in_downtrend(self):
        candles = downtrend_candles(30)
        signal = detect_entry_sell(candles)
        assert signal is not None
        assert signal.signal_type == SignalType.SELL

    def test_no_sell_in_uptrend(self):
        candles = uptrend_candles(30)
        signal = detect_entry_sell(candles)
        assert signal is None


class TestDetectEntry:
    def test_detect_entry_returns_best(self):
        candles = uptrend_candles(30)
        signal = detect_entry(candles)
        assert signal is not None
        assert signal.signal_type in (SignalType.BUY, SignalType.SELL)


# ---------------------------------------------------------------------------
# Risk Management Tests
# ---------------------------------------------------------------------------
class TestCalculateStopLossTakeProfit:
    def test_buy_sl_tp(self):
        entry = 100.0
        sl, tp = calculate_stop_loss_take_profit(
            entry, SignalType.BUY,
            second_to_last_low=95.0,
            second_to_last_high=98.0,
        )
        assert sl < entry
        assert tp > entry
        assert tp == entry + (entry - sl) * 3.0

    def test_sell_sl_tp(self):
        entry = 100.0
        sl, tp = calculate_stop_loss_take_profit(
            entry, SignalType.SELL,
            second_to_last_low=95.0,
            second_to_last_high=105.0,
        )
        assert sl > entry
        assert tp < entry

    def test_fallback_when_sld_wider(self):
        entry = 100.0
        sl, tp = calculate_stop_loss_take_profit(
            entry, SignalType.BUY,
            second_to_last_low=99.9,  # Very close
            second_to_last_high=101.0,
        )
        assert sl < entry


class TestCalculatePositionSize:
    def test_basic_position_sizing(self):
        risk_params = RiskParameters(
            account_balance=10000,
            risk_percent=1.0,
        )
        size, amount, pct = calculate_position_size(100, 95, risk_params)
        assert size > 0
        assert amount == 100.0  # 1% of 10000
        price_risk = abs(100 - 95)
        assert price_risk == 5.0

    def test_zero_risk(self):
        risk_params = RiskParameters(account_balance=10000)
        size, amount, pct = calculate_position_size(100, 100, risk_params)
        assert size == 0.0


class TestATR:
    def test_atr_basic(self):
        candles = uptrend_candles(20)
        atr = calculate_atr(candles)
        assert atr >= 0

    def test_atr_insufficient_data(self):
        candles = [make_candle(1, 100, 101, 99, 100)]
        atr = calculate_atr(candles)
        assert atr == 0.0


class TestValidateTrade:
    def test_valid_trade(self):
        signal = EntrySignal(
            signal_type=SignalType.BUY,
            price=100.0,
            confidence=0.8,
            reason="test",
            stop_loss=97.0,  # 3% risk, within the 3% max
            take_profit=109.0,
        )
        params = RiskParameters(account_balance=10000)
        result = validate_trade(signal, params)
        assert result.invalid is False
        assert result.position_size > 0

    def test_invalid_no_stop_loss(self):
        signal = EntrySignal(
            signal_type=SignalType.BUY,
            price=100.0,
            confidence=0.8,
            reason="test",
        )
        params = RiskParameters()
        result = validate_trade(signal, params)
        assert result.invalid is True


class TestFullRiskAnalysis:
    def test_full_analysis_uptrend(self):
        candles = uptrend_candles(30)
        params = RiskParameters(account_balance=10000, risk_percent=1.0)
        result = full_risk_analysis(candles, params)
        assert result is not None
        assert result.invalid is False
        assert result.position_size > 0

    def test_full_analysis_downtrend_no_buy(self):
        candles = downtrend_candles(30)
        params = RiskParameters()
        result = full_risk_analysis(candles, params)
        # May be None (no buy signal in downtrend) or valid sell signal
        if result is not None:
            assert result.position_size > 0 or result.invalid is True

    def test_full_analysis_insufficient_data(self):
        candles = uptrend_candles(2)
        result = full_risk_analysis(candles)
        assert result is None