"""
Tests for the strategy execution loop.

These tests verify that:
1. Strategies.update() iterates through active instances
2. StrategyInstance.tick() processes candle data correctly
3. Price updates flow to paper trading instances
4. Errors are handled gracefully
"""
import pytest
from unittest.mock import MagicMock, patch
import json


class TestStrategiesUpdate:
    """Tests for the Strategies.update() method."""

    @pytest.fixture
    def mock_strategies(self):
        """Create a Strategies instance with mock dependencies."""
        with patch.dict('sys.modules', {'eventlet': MagicMock()}):
            from Strategies import Strategies

            # Mock DataCache
            mock_cache = MagicMock()
            mock_cache.create_cache = MagicMock()
            mock_cache.get_rows_from_datacache = MagicMock(return_value=MagicMock(empty=True))

            # Mock Trades and Indicators
            mock_trades = MagicMock()
            mock_indicators = MagicMock()

            strategies = Strategies(mock_cache, mock_trades, mock_indicators)
            return strategies

    def test_update_no_active_instances(self, mock_strategies):
        """Test update with no active strategies returns empty list."""
        result = mock_strategies.update()
        assert result == []

    def test_update_calls_tick_on_instances(self, mock_strategies):
        """Test that update calls tick() on all active instances."""
        # Create mock instances
        mock_instance1 = MagicMock()
        mock_instance1.tick.return_value = [{'type': 'tick_complete'}]
        mock_instance1.exit = False

        mock_instance2 = MagicMock()
        mock_instance2.tick.return_value = [{'type': 'tick_complete'}]
        mock_instance2.exit = False

        mock_strategies.active_instances = {
            (1, 'strat-1', 'paper'): mock_instance1,
            (2, 'strat-2', 'paper'): mock_instance2,
        }

        candle_data = {'symbol': 'BTC/USDT', 'close': 50000}
        result = mock_strategies.update(candle_data)

        # Both instances should have tick called
        mock_instance1.tick.assert_called_once_with(candle_data)
        mock_instance2.tick.assert_called_once_with(candle_data)

        # Should have events from both
        assert len(result) == 2

    def test_update_delegates_price_handling_to_tick(self, mock_strategies):
        """Test that update delegates candle handling to instance.tick()."""
        mock_instance = MagicMock()
        mock_instance.tick.return_value = []
        mock_instance.exit = False
        mock_instance.update_prices = MagicMock()

        mock_strategies.active_instances = {
            (1, 'strat-1', 'paper'): mock_instance,
        }

        candle_data = {'symbol': 'ETH/USDT', 'close': 3000.5}
        mock_strategies.update(candle_data)

        mock_instance.tick.assert_called_once_with(candle_data)
        mock_instance.update_prices.assert_not_called()

    def test_update_tags_events_with_instance_info(self, mock_strategies):
        """Test that events are tagged with user_id, strategy_id, mode."""
        mock_instance = MagicMock()
        mock_instance.tick.return_value = [{'type': 'tick_complete'}]
        mock_instance.exit = False

        mock_strategies.active_instances = {
            (42, 'my-strategy', 'paper'): mock_instance,
        }

        result = mock_strategies.update()

        assert len(result) == 1
        event = result[0]
        assert event['user_id'] == 42
        assert event['strategy_id'] == 'my-strategy'
        assert event['mode'] == 'paper'

    def test_update_handles_instance_errors(self, mock_strategies):
        """Test that errors in one instance don't stop others."""
        # First instance raises error
        mock_instance1 = MagicMock()
        mock_instance1.tick.side_effect = Exception("Strategy crashed")
        mock_instance1.exit = False

        # Second instance works fine
        mock_instance2 = MagicMock()
        mock_instance2.tick.return_value = [{'type': 'tick_complete'}]
        mock_instance2.exit = False

        mock_strategies.active_instances = {
            (1, 'bad-strat', 'paper'): mock_instance1,
            (2, 'good-strat', 'paper'): mock_instance2,
        }

        result = mock_strategies.update()

        # Should have error event from first, success from second
        assert len(result) == 2

        error_events = [e for e in result if e['type'] == 'error']
        success_events = [e for e in result if e['type'] == 'tick_complete']

        assert len(error_events) == 1
        assert len(success_events) == 1
        assert 'Strategy crashed' in error_events[0]['message']

    def test_update_removes_exited_strategies(self, mock_strategies):
        """Test that strategies with exit=True and no positions are removed."""
        mock_instance = MagicMock()
        mock_instance.tick.return_value = []
        mock_instance.exit = True
        mock_instance.broker = MagicMock()
        mock_instance.broker.get_all_positions.return_value = []  # No positions

        mock_strategies.active_instances = {
            (1, 'exiting-strat', 'paper'): mock_instance,
        }

        result = mock_strategies.update()

        # Strategy should be removed
        assert len(mock_strategies.active_instances) == 0

        # Should have strategy_exited event
        exited_events = [e for e in result if e['type'] == 'strategy_exited']
        assert len(exited_events) == 1


class TestStrategyInstanceTick:
    """Tests for StrategyInstance.tick() method."""

    @pytest.fixture
    def mock_strategy_instance(self):
        """Create a StrategyInstance with mock dependencies."""
        with patch.dict('sys.modules', {'eventlet': MagicMock()}):
            from StrategyInstance import StrategyInstance

            with patch.object(StrategyInstance, '__init__', lambda x: None):
                instance = StrategyInstance()

                # Set up required attributes
                instance.strategy_id = 'test-strategy'
                instance.strategy_instance_id = 'inst-123'
                instance.paused = False
                instance.exit = False
                instance.exec_context = {'_events': []}
                instance.generated_code = "def next(): pass"
                instance.execute = MagicMock(return_value={'success': True, 'profit_loss': 100.0})

                return instance

    def test_tick_skips_when_paused(self, mock_strategy_instance):
        """Test that tick returns skip event when paused."""
        mock_strategy_instance.paused = True

        result = mock_strategy_instance.tick()

        assert len(result) == 1
        assert result[0]['type'] == 'skipped'
        assert result[0]['reason'] == 'paused'
        mock_strategy_instance.execute.assert_not_called()

    def test_tick_skips_when_exiting(self, mock_strategy_instance):
        """Test that tick returns skip event when exit is True."""
        mock_strategy_instance.exit = True

        result = mock_strategy_instance.tick()

        assert len(result) == 1
        assert result[0]['type'] == 'skipped'
        assert result[0]['reason'] == 'exiting'

    def test_tick_updates_exec_context_with_candle(self, mock_strategy_instance):
        """Test that tick updates exec_context with candle data."""
        candle = {'symbol': 'BTC/USDT', 'close': 50000, 'open': 49500, 'high': 51000, 'low': 49000}

        mock_strategy_instance.tick(candle)

        assert mock_strategy_instance.exec_context['current_candle'] == candle
        assert mock_strategy_instance.exec_context['current_price'] == 50000
        assert mock_strategy_instance.exec_context['current_symbol'] == 'BTC/USDT'

    def test_tick_returns_tick_complete_on_success(self, mock_strategy_instance):
        """Test that successful tick returns tick_complete event."""
        result = mock_strategy_instance.tick()

        assert len(result) == 1
        assert result[0]['type'] == 'tick_complete'
        assert result[0]['strategy_id'] == 'test-strategy'
        assert result[0]['profit_loss'] == 100.0

    def test_tick_returns_error_on_execute_failure(self, mock_strategy_instance):
        """Test that failed execute returns error event."""
        mock_strategy_instance.execute.return_value = {
            'success': False,
            'message': 'Syntax error in strategy'
        }

        result = mock_strategy_instance.tick()

        assert len(result) == 1
        assert result[0]['type'] == 'error'
        assert 'Syntax error' in result[0]['message']

    def test_tick_handles_exceptions(self, mock_strategy_instance):
        """Test that exceptions during tick are caught and returned."""
        mock_strategy_instance.execute.side_effect = RuntimeError("Unexpected error")

        result = mock_strategy_instance.tick()

        assert len(result) == 1
        assert result[0]['type'] == 'error'
        assert 'Unexpected error' in result[0]['message']


class TestPaperStrategyInstanceTick:
    """Tests for PaperStrategyInstance.tick() method."""

    @pytest.fixture
    def mock_paper_instance(self):
        """Create a PaperStrategyInstance with mock dependencies."""
        with patch.dict('sys.modules', {'eventlet': MagicMock()}):
            from paper_strategy_instance import PaperStrategyInstance

            with patch.object(PaperStrategyInstance, '__init__', lambda x: None):
                instance = PaperStrategyInstance()

                # Set up required attributes
                instance.strategy_id = 'paper-strategy'
                instance.strategy_instance_id = 'paper-inst-123'
                instance.paused = False
                instance.exit = False
                instance.exec_context = {'_events': []}
                instance.generated_code = "def next(): pass"
                instance.current_balance = 10000.0
                instance.available_balance = 10000.0
                instance.trade_history = []

                # Mock paper broker
                instance.paper_broker = MagicMock()
                instance.paper_broker.update_price = MagicMock()
                instance.paper_broker.update.return_value = []
                instance.paper_broker.get_all_positions.return_value = []

                # Mock execute
                instance.execute = MagicMock(return_value={'success': True})
                instance._update_balances = MagicMock()

                return instance

    def test_tick_updates_broker_prices(self, mock_paper_instance):
        """Test that tick updates paper broker with candle prices."""
        candle = {'symbol': 'ETH/USDT', 'close': 3000}

        mock_paper_instance.tick(candle)

        mock_paper_instance.paper_broker.update_price.assert_called_with('ETH/USDT', 3000)

    def test_tick_processes_broker_fills(self, mock_paper_instance):
        """Test that broker fills are processed and added to events."""
        mock_paper_instance.paper_broker.update.return_value = [
            {'type': 'fill', 'order_id': 'order-1', 'symbol': 'BTC/USDT',
             'filled_qty': 0.1, 'filled_price': 50000}
        ]

        candle = {'symbol': 'BTC/USDT', 'close': 50000}
        result = mock_paper_instance.tick(candle)

        # Should have fill event
        fill_events = [e for e in result if e['type'] == 'order_filled']
        assert len(fill_events) == 1
        assert fill_events[0]['order_id'] == 'order-1'

    def test_tick_includes_balance_in_complete_event(self, mock_paper_instance):
        """Test that tick_complete includes balance info."""
        mock_paper_instance.current_balance = 10500.0
        mock_paper_instance.available_balance = 10000.0

        result = mock_paper_instance.tick()

        complete_events = [e for e in result if e['type'] == 'tick_complete']
        assert len(complete_events) == 1
        assert complete_events[0]['balance'] == 10500.0
        assert complete_events[0]['available_balance'] == 10000.0


class TestExecutionLoopIntegration:
    """Integration tests for the full execution loop."""

    def test_candle_triggers_strategy_update(self):
        """Test that received_cdata triggers strategy updates."""
        # This would need full integration test setup
        # For now, just verify the method exists
        pass

    def test_multiple_strategies_execute_independently(self):
        """Test that multiple strategies don't interfere with each other."""
        pass
