"""Tests for neural network inference pipeline."""

from unittest.mock import MagicMock, patch

import numpy as np
import pandas as pd
import pytest

from ai.inference_v2 import (
    NeuralPredictor,
    NeuralSignal,
    predict_for_ticker,
)


def make_ohlcv_df(n_bars: int = 100, base_price: float = 100.0) -> pd.DataFrame:
    """Create synthetic OHLCV data for testing."""
    np.random.seed(42)
    
    data = []
    price = base_price
    
    for i in range(n_bars):
        change = np.random.normal(0.0005, 0.015)
        price = price * (1 + change)
        
        spread = price * 0.02
        high = price + spread * np.random.uniform(0.3, 1.0)
        low = price - spread * np.random.uniform(0.3, 1.0)
        open_price = price * (1 + np.random.normal(0, 0.005))
        volume = int(np.random.uniform(100000, 1000000))
        
        data.append({
            "timestamp": 1700000000 + i * 3600,
            "Open": open_price,
            "High": max(open_price, price, high),
            "Low": min(open_price, price, low),
            "Close": price,
            "Volume": volume,
        })
    
    return pd.DataFrame(data)


class TestNeuralSignal:
    def test_signal_creation(self):
        signal = NeuralSignal(
            timestamp=1700000000,
            side="LONG",
            entry_price=100.0,
            sl_price=98.0,
            tp_price=104.0,
            sl_distance_atr=1.0,
            tp_distance_atr=2.0,
            entry_probability=0.85,
            confidence=0.75,
            atr_at_signal=2.0,
        )
        
        assert signal.timestamp == 1700000000
        assert signal.side == "LONG"
        assert signal.entry_price == 100.0
        assert signal.sl_price == 98.0
        assert signal.tp_price == 104.0
        assert signal.entry_probability == 0.85

    def test_signal_short(self):
        signal = NeuralSignal(
            timestamp=1700000000,
            side="SHORT",
            entry_price=100.0,
            sl_price=102.0,
            tp_price=96.0,
            sl_distance_atr=1.0,
            tp_distance_atr=2.0,
            entry_probability=0.75,
            confidence=0.65,
            atr_at_signal=2.0,
        )
        
        assert signal.side == "SHORT"
        assert signal.sl_price > signal.entry_price
        assert signal.tp_price < signal.entry_price


class TestNeuralPredictor:
    def test_predictor_creation_no_model(self):
        predictor = NeuralPredictor(model_path=None)
        assert predictor.trainer is None
        assert predictor.entry_threshold == 0.5

    def test_predictor_custom_thresholds(self):
        predictor = NeuralPredictor(
            model_path=None,
            entry_threshold=0.7,
            min_confidence=0.5,
        )
        assert predictor.entry_threshold == 0.7
        assert predictor.min_confidence == 0.5

    def test_predict_signals_no_model(self):
        predictor = NeuralPredictor(model_path=None)
        df = make_ohlcv_df(n_bars=100)
        
        signals = predictor.predict_signals(df, "LONG")
        assert signals == []

    def test_predict_signals_insufficient_data(self):
        predictor = NeuralPredictor(model_path=None, context_window=24)
        df = make_ohlcv_df(n_bars=10)
        
        signals = predictor.predict_signals(df, "LONG")
        assert signals == []

    def test_predict_signals_empty_df(self):
        predictor = NeuralPredictor(model_path=None)
        signals = predictor.predict_signals(pd.DataFrame(), "LONG")
        assert signals == []

    def test_predict_latest_no_model(self):
        predictor = NeuralPredictor(model_path=None)
        df = make_ohlcv_df(n_bars=100)
        
        signal = predictor.predict_latest(df, "LONG")
        assert signal is None

    def test_signals_to_dataframe_empty(self):
        predictor = NeuralPredictor(model_path=None)
        result = predictor.signals_to_dataframe([])
        assert result.empty

    def test_signals_to_dataframe(self):
        predictor = NeuralPredictor(model_path=None)
        
        signals = [
            NeuralSignal(
                timestamp=1700000000,
                side="LONG",
                entry_price=100.0,
                sl_price=98.0,
                tp_price=104.0,
                sl_distance_atr=1.0,
                tp_distance_atr=2.0,
                entry_probability=0.85,
                confidence=0.75,
                atr_at_signal=2.0,
            ),
            NeuralSignal(
                timestamp=1700003600,
                side="SHORT",
                entry_price=101.0,
                sl_price=103.0,
                tp_price=97.0,
                sl_distance_atr=1.0,
                tp_distance_atr=2.0,
                entry_probability=0.75,
                confidence=0.65,
                atr_at_signal=2.0,
            ),
        ]
        
        result = predictor.signals_to_dataframe(signals)
        
        assert len(result) == 2
        assert "timestamp" in result.columns
        assert "signal_type" in result.columns
        assert "entry_price" in result.columns
        assert "sl_price" in result.columns
        assert "tp_price" in result.columns
        assert "ai_probability" in result.columns
        assert "ai_confidence" in result.columns


class TestNeuralPredictorWithMockedTrainer:
    def test_predict_with_mocked_trainer(self):
        predictor = NeuralPredictor(model_path=None, context_window=24)
        
        mock_trainer = MagicMock()
        mock_trainer.predict.return_value = {
            "entry_long_proba": np.array([0.8]),
            "entry_long_signal": np.array([1]),
            "entry_short_proba": np.array([0.3]),
            "entry_short_signal": np.array([0]),
            "entry_proba": np.array([0.8]),
            "entry_signal": np.array([1]),
            "sl_distance": np.array([1.2]),
            "tp_distance": np.array([2.4]),
            "confidence": np.array([0.7]),
        }
        predictor.trainer = mock_trainer
        
        df = make_ohlcv_df(n_bars=100)
        signals = predictor.predict_signals(df, "LONG")
        
        assert len(signals) > 0
        assert signals[0].side == "LONG"
        assert signals[0].entry_probability == pytest.approx(0.8)
        assert signals[0].sl_distance_atr == pytest.approx(1.2)
        assert signals[0].tp_distance_atr == pytest.approx(2.4)

    def test_predict_filters_low_confidence(self):
        predictor = NeuralPredictor(
            model_path=None,
            context_window=24,
            min_confidence=0.8,
        )
        
        mock_trainer = MagicMock()
        mock_trainer.predict.return_value = {
            "entry_long_proba": np.array([0.8]),
            "entry_long_signal": np.array([1]),
            "entry_short_proba": np.array([0.3]),
            "entry_short_signal": np.array([0]),
            "entry_proba": np.array([0.8]),
            "entry_signal": np.array([1]),
            "sl_distance": np.array([1.2]),
            "tp_distance": np.array([2.4]),
            "confidence": np.array([0.5]),
        }
        predictor.trainer = mock_trainer
        
        df = make_ohlcv_df(n_bars=100)
        signals = predictor.predict_signals(df, "LONG")
        
        assert len(signals) == 0

    def test_predict_filters_no_entry(self):
        predictor = NeuralPredictor(
            model_path=None,
            context_window=24,
            entry_threshold=0.5,
        )
        
        mock_trainer = MagicMock()
        mock_trainer.predict.return_value = {
            "entry_long_proba": np.array([0.3]),
            "entry_long_signal": np.array([0]),
            "entry_short_proba": np.array([0.3]),
            "entry_short_signal": np.array([0]),
            "entry_proba": np.array([0.3]),
            "entry_signal": np.array([0]),
            "sl_distance": np.array([1.2]),
            "tp_distance": np.array([2.4]),
            "confidence": np.array([0.9]),
        }
        predictor.trainer = mock_trainer
        
        df = make_ohlcv_df(n_bars=100)
        signals = predictor.predict_signals(df, "LONG")
        
        assert len(signals) == 0

    def test_predict_long_sl_tp_calculation(self):
        predictor = NeuralPredictor(model_path=None, context_window=24)
        
        mock_trainer = MagicMock()
        mock_trainer.predict.return_value = {
            "entry_long_proba": np.array([0.8]),
            "entry_long_signal": np.array([1]),
            "entry_short_proba": np.array([0.3]),
            "entry_short_signal": np.array([0]),
            "entry_proba": np.array([0.8]),
            "entry_signal": np.array([1]),
            "sl_distance": np.array([1.0]),
            "tp_distance": np.array([2.0]),
            "confidence": np.array([0.7]),
        }
        predictor.trainer = mock_trainer
        
        df = make_ohlcv_df(n_bars=100)
        signals = predictor.predict_signals(df, "LONG")
        
        assert len(signals) > 0
        signal = signals[0]
        assert signal.sl_price < signal.entry_price
        assert signal.tp_price > signal.entry_price

    def test_predict_short_sl_tp_calculation(self):
        predictor = NeuralPredictor(model_path=None, context_window=24)
        
        mock_trainer = MagicMock()
        mock_trainer.predict.return_value = {
            "entry_long_proba": np.array([0.3]),
            "entry_long_signal": np.array([0]),
            "entry_short_proba": np.array([0.8]),
            "entry_short_signal": np.array([1]),
            "entry_proba": np.array([0.3]),
            "entry_signal": np.array([0]),
            "sl_distance": np.array([1.0]),
            "tp_distance": np.array([2.0]),
            "confidence": np.array([0.7]),
        }
        predictor.trainer = mock_trainer
        
        df = make_ohlcv_df(n_bars=100)
        signals = predictor.predict_signals(df, "SHORT")
        
        assert len(signals) > 0
        signal = signals[0]
        assert signal.sl_price > signal.entry_price
        assert signal.tp_price < signal.entry_price


class TestPredictForTicker:
    def test_predict_for_ticker_no_model(self, tmp_path):
        df = make_ohlcv_df(n_bars=100)
        fake_path = tmp_path / "nonexistent.pt"
        
        result = predict_for_ticker("SBER", df, fake_path)
        assert result.empty

    @patch("ai.inference_v2.NeuralPredictor.load_model")
    def test_predict_for_ticker_with_mock(self, mock_load, tmp_path):
        df = make_ohlcv_df(n_bars=100)
        fake_path = tmp_path / "model.pt"
        fake_path.touch()
        
        with patch.object(NeuralPredictor, 'predict_signals') as mock_predict:
            mock_predict.return_value = [
                NeuralSignal(
                    timestamp=1700000000,
                    side="LONG",
                    entry_price=100.0,
                    sl_price=98.0,
                    tp_price=104.0,
                    sl_distance_atr=1.0,
                    tp_distance_atr=2.0,
                    entry_probability=0.85,
                    confidence=0.75,
                    atr_at_signal=2.0,
                ),
            ]
            
            result = predict_for_ticker("SBER", df, fake_path, sides=["LONG"])
            
            assert len(result) == 1
            assert "ticker" in result.columns
            assert result["ticker"].iloc[0] == "SBER"
