"""
Tests for LiveBroker implementation.

These tests use mocked exchange responses to verify LiveBroker behavior
without requiring actual exchange connectivity.
"""
import pytest
from unittest.mock import Mock, MagicMock, patch
from datetime import datetime, timezone
import json

import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))

from brokers.live_broker import (
    LiveBroker, LiveOrder, RateLimiter, retry_on_network_error
)
from brokers.base_broker import OrderSide, OrderType, OrderStatus, Position
import ccxt


class TestRateLimiter:
    """Tests for the RateLimiter class."""

    def test_rate_limiter_creation(self):
        """Test rate limiter initialization."""
        limiter = RateLimiter(calls_per_second=2.0)
        assert limiter.min_interval == 0.5

    def test_rate_limiter_wait(self):
        """Test that rate limiter enforces delays."""
        limiter = RateLimiter(calls_per_second=100.0)  # Fast for testing
        limiter.wait()
        assert limiter.last_call > 0


class TestRetryDecorator:
    """Tests for the retry_on_network_error decorator."""

    def test_retry_succeeds_on_first_attempt(self):
        """Test function returns immediately when no error."""
        @retry_on_network_error(max_retries=3, delay=0.01)
        def always_works():
            return "success"

        assert always_works() == "success"

    def test_retry_on_network_error(self):
        """Test function retries on network error."""
        call_count = 0

        @retry_on_network_error(max_retries=3, delay=0.01)
        def fails_then_works():
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                raise ccxt.NetworkError("Connection failed")
            return "success"

        result = fails_then_works()
        assert result == "success"
        assert call_count == 3

    def test_retry_exhausted(self):
        """Test exception raised when retries exhausted."""
        @retry_on_network_error(max_retries=2, delay=0.01)
        def always_fails():
            raise ccxt.NetworkError("Connection failed")

        with pytest.raises(ccxt.NetworkError):
            always_fails()


class TestLiveOrder:
    """Tests for the LiveOrder class."""

    def test_live_order_creation(self):
        """Test creating a live order."""
        order = LiveOrder(
            order_id="test123",
            exchange_order_id="EX123",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1
        )
        assert order.order_id == "test123"
        assert order.symbol == "BTC/USDT"
        assert order.side == OrderSide.BUY
        assert order.status == OrderStatus.PENDING

    def test_live_order_to_dict(self):
        """Test serializing order to dict."""
        order = LiveOrder(
            order_id="test123",
            exchange_order_id="EX123",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            order_type=OrderType.LIMIT,
            size=0.1,
            price=50000.0
        )
        order.status = OrderStatus.FILLED
        order.filled_qty = 0.1
        order.filled_price = 50000.0

        data = order.to_dict()
        assert data['order_id'] == "test123"
        assert data['symbol'] == "BTC/USDT"
        assert data['side'] == "buy"
        assert data['status'] == "filled"
        assert data['filled_qty'] == 0.1

    def test_live_order_from_dict(self):
        """Test deserializing order from dict."""
        data = {
            'order_id': 'test123',
            'exchange_order_id': 'EX123',
            'symbol': 'BTC/USDT',
            'side': 'buy',
            'order_type': 'limit',
            'size': 0.1,
            'price': 50000.0,
            'status': 'filled',
            'filled_qty': 0.1,
            'filled_price': 50000.0,
            'commission': 5.0,
            'created_at': datetime.now(timezone.utc).isoformat()
        }

        order = LiveOrder.from_dict(data)
        assert order.order_id == 'test123'
        assert order.side == OrderSide.BUY
        assert order.status == OrderStatus.FILLED


class TestLiveBroker:
    """Tests for the LiveBroker class."""

    def _create_mock_exchange(self, configured=True, testnet=True):
        """Create a mock exchange for testing."""
        exchange = Mock()
        exchange.configured = configured
        exchange.testnet = testnet
        exchange.client = Mock()
        exchange.client.fetch_balance = Mock(return_value={
            'USDT': {'total': 10000, 'free': 9000, 'used': 1000},
            'BTC': {'total': 0.5, 'free': 0.5, 'used': 0}
        })
        exchange.get_active_trades = Mock(return_value=[])
        exchange.get_open_orders = Mock(return_value=[])
        exchange.get_price = Mock(return_value=50000.0)
        exchange.place_order = Mock(return_value=(
            'Success',
            {'id': 'EX123', 'status': 'open', 'filled': 0, 'average': 0}
        ))
        exchange.get_order = Mock(return_value={
            'id': 'EX123',
            'status': 'closed',
            'filled': 0.1,
            'average': 50000.0,
            'fee': {'cost': 5.0}
        })
        return exchange

    def test_live_broker_creation_testnet(self):
        """Test creating a live broker in testnet mode."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)

        assert broker._testnet is True
        assert broker._connected is False

    def test_live_broker_creation_production_warning(self, caplog):
        """Test that production mode logs a warning."""
        exchange = self._create_mock_exchange()

        with caplog.at_level('WARNING'):
            broker = LiveBroker(exchange=exchange, testnet=False)

        assert "PRODUCTION trading" in caplog.text

    def test_live_broker_connect(self):
        """Test connecting to exchange."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)

        result = broker.connect()

        assert result is True
        assert broker._connected is True
        assert 'USDT' in broker._balances
        assert broker._balances['USDT'] == 10000

    def test_live_broker_connect_no_exchange(self):
        """Test connect fails without exchange."""
        broker = LiveBroker(exchange=None, testnet=True)

        result = broker.connect()

        assert result is False
        assert broker._connected is False

    def test_live_broker_connect_not_configured(self):
        """Test connect fails when exchange not configured."""
        exchange = self._create_mock_exchange(configured=False)
        broker = LiveBroker(exchange=exchange, testnet=True)

        result = broker.connect()

        assert result is False

    def test_live_broker_sync_balance(self):
        """Test syncing balance from exchange."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)

        balances = broker.sync_balance()

        assert 'USDT' in balances
        assert balances['USDT'] == 10000

    def test_live_broker_get_balance(self):
        """Test getting balance."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        balance = broker.get_balance()
        assert balance == 10000  # USDT balance

    def test_live_broker_get_available_balance(self):
        """Test getting available balance."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        available = broker.get_available_balance()
        # 10000 total - 1000 locked = 9000
        assert available == 9000

    def test_live_broker_place_market_order(self):
        """Test placing a market order."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        result = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1
        )

        assert result.success
        assert result.order_id is not None
        exchange.place_order.assert_called_once()

    def test_live_broker_place_limit_order(self):
        """Test placing a limit order."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        result = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.LIMIT,
            size=0.1,
            price=49000.0
        )

        assert result.success
        assert result.status == OrderStatus.OPEN

    def test_live_broker_auto_client_id_reused_within_retry_window(self):
        """Auto-generated client IDs should be reused briefly for retry idempotency."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        first = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1
        )
        second = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1
        )

        assert first.success is True
        assert second.success is True
        assert "duplicate" in (second.message or "").lower()
        # Only one actual exchange call due to duplicate detection.
        assert exchange.place_order.call_count == 1

    def test_live_broker_auto_client_id_expires_after_window(self):
        """After retry window expiry, identical orders should get a fresh client ID."""
        import time
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()
        broker._auto_client_id_window_seconds = 0.01

        first = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1
        )
        time.sleep(0.02)
        second = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1
        )

        assert first.success is True
        assert second.success is True
        # Two exchange calls because the auto ID rotated after expiry.
        assert exchange.place_order.call_count == 2

    def test_live_broker_place_order_invalid_size(self):
        """Test that invalid order size is rejected."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        result = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0  # Invalid
        )

        assert not result.success
        assert "positive" in result.message

    def test_live_broker_place_order_limit_no_price(self):
        """Test that limit order without price is rejected."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        result = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.LIMIT,
            size=0.1,
            price=None
        )

        assert not result.success
        assert "price" in result.message.lower()

    def test_live_broker_cancel_order(self):
        """Test cancelling an order."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        # Place an order first
        result = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.LIMIT,
            size=0.1,
            price=49000.0
        )

        # Cancel it
        cancelled = broker.cancel_order(result.order_id)
        assert cancelled

        # Check order status
        order = broker._orders[result.order_id]
        assert order.status == OrderStatus.CANCELLED

    def test_live_broker_get_open_orders(self):
        """Test getting open orders."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        # Place a limit order (stays open)
        broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.LIMIT,
            size=0.1,
            price=49000.0
        )

        open_orders = broker.get_open_orders()
        assert len(open_orders) == 1

    def test_live_broker_update_detects_fills(self):
        """Test that update() detects order fills."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        # Place a limit order
        result = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.LIMIT,
            size=0.1,
            price=49000.0
        )

        # Exchange returns filled order
        exchange.get_order.return_value = {
            'id': 'EX123',
            'status': 'closed',
            'filled': 0.1,
            'average': 49000.0,
            'fee': {'cost': 4.9}
        }

        # Update should detect fill
        events = broker.update()

        assert len(events) == 1
        assert events[0]['type'] == 'fill'
        assert events[0]['filled_qty'] == 0.1
        assert events[0]['filled_price'] == 49000.0

    def test_live_broker_get_current_price(self):
        """Test getting current price."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker._connected = True

        price = broker.get_current_price('BTC/USDT')

        assert price == 50000.0
        exchange.get_price.assert_called_with('BTC/USDT')


class TestLiveBrokerPersistence:
    """Tests for LiveBroker state persistence."""

    def _create_mock_exchange(self):
        """Create a mock exchange for testing."""
        exchange = Mock()
        exchange.configured = True
        exchange.client = Mock()
        exchange.client.fetch_balance = Mock(return_value={
            'USDT': {'total': 10000, 'free': 10000, 'used': 0}
        })
        exchange.get_active_trades = Mock(return_value=[])
        exchange.get_open_orders = Mock(return_value=[])
        exchange.get_price = Mock(return_value=50000.0)
        return exchange

    def _create_mock_data_cache(self):
        """Create a mock data cache for testing."""
        data_cache = Mock()
        data_cache.db = Mock()
        data_cache.db.execute_sql = Mock()
        data_cache.create_cache = Mock()

        # Empty result by default
        import pandas as pd
        data_cache.get_rows_from_datacache = Mock(return_value=pd.DataFrame())
        data_cache.insert_row_into_datacache = Mock()
        data_cache.modify_datacache_item = Mock()

        return data_cache

    def test_live_broker_to_state_dict(self):
        """Test serializing broker state."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        # Add some state
        broker._balances = {'USDT': 10000}
        broker._current_prices = {'BTC/USDT': 50000}

        state = broker.to_state_dict()

        assert state['testnet'] is True
        assert 'USDT' in state['balances']
        assert 'BTC/USDT' in state['current_prices']

    def test_live_broker_from_state_dict(self):
        """Test restoring broker state."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)

        state = {
            'testnet': True,
            'balances': {'USDT': 9500},
            'locked_balances': {'USDT': 500},
            'orders': {},
            'positions': {},
            'current_prices': {'BTC/USDT': 51000}
        }

        broker.from_state_dict(state)

        assert broker._balances['USDT'] == 9500
        assert broker._current_prices['BTC/USDT'] == 51000

    def test_live_broker_save_state(self):
        """Test saving state to data cache."""
        exchange = self._create_mock_exchange()
        data_cache = self._create_mock_data_cache()
        broker = LiveBroker(exchange=exchange, testnet=True, data_cache=data_cache)
        broker.connect()

        result = broker.save_state('test-strategy-123')

        assert result is True
        data_cache.insert_row_into_datacache.assert_called()

    def test_live_broker_load_state(self):
        """Test loading state from data cache."""
        exchange = self._create_mock_exchange()
        data_cache = self._create_mock_data_cache()

        # Set up mock to return saved state
        import pandas as pd
        state_dict = {
            'testnet': True,
            'balances': {'USDT': 9500},
            'locked_balances': {},
            'orders': {},
            'positions': {},
            'current_prices': {}
        }
        data_cache.get_rows_from_datacache.return_value = pd.DataFrame([{
            'broker_state': json.dumps(state_dict)
        }])

        broker = LiveBroker(exchange=exchange, testnet=True, data_cache=data_cache)

        result = broker.load_state('test-strategy-123')

        assert result is True
        assert broker._balances['USDT'] == 9500


class TestLiveBrokerReconciliation:
    """Tests for exchange reconciliation."""

    def _create_mock_exchange(self):
        """Create a mock exchange for testing."""
        exchange = Mock()
        exchange.configured = True
        exchange.client = Mock()
        exchange.client.fetch_balance = Mock(return_value={
            'USDT': {'total': 10000, 'free': 9000, 'used': 1000}
        })
        exchange.get_active_trades = Mock(return_value=[])
        exchange.get_open_orders = Mock(return_value=[])
        exchange.get_price = Mock(return_value=50000.0)
        exchange.get_order = Mock(return_value={
            'id': 'EX123',
            'status': 'closed',
            'filled': 0.1,
            'average': 50000.0
        })
        return exchange

    def test_reconcile_detects_balance_changes(self):
        """Test that reconciliation detects balance changes."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)
        broker.connect()

        # Manually set a different balance
        broker._balances['USDT'] = 8000

        # Reconcile
        results = broker.reconcile_with_exchange()

        assert results['success'] is True
        assert len(results['balance_changes']) > 0
        # Balance should now be updated to 10000
        assert broker._balances['USDT'] == 10000

    def test_reconcile_not_connected(self):
        """Test that reconciliation fails when not connected."""
        exchange = self._create_mock_exchange()
        broker = LiveBroker(exchange=exchange, testnet=True)

        results = broker.reconcile_with_exchange()

        assert results['success'] is False
        assert 'error' in results
