"""
Tests for the broker abstraction layer.
"""
import pytest
from brokers import (
    BaseBroker, BacktestBroker, PaperBroker, LiveBroker,
    OrderSide, OrderType, OrderStatus, OrderResult, Position,
    create_broker, TradingMode
)
from ExchangeInterface import ExchangeInterface
from trade import Trade, Trades


class TestPaperBroker:
    """Tests for PaperBroker."""

    def test_create_paper_broker(self):
        """Test creating a paper broker."""
        broker = PaperBroker(initial_balance=10000)
        assert broker.get_balance() == 10000
        assert broker.get_available_balance() == 10000

    def test_paper_broker_market_buy(self):
        """Test market buy order."""
        broker = PaperBroker(initial_balance=10000, commission=0.001)
        broker.update_price('BTC/USDT', 50000)

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

        assert result.success
        assert result.status == OrderStatus.FILLED

        position = broker.get_position('BTC/USDT')
        assert position is not None
        assert position.size == 0.1

        # Check balance deducted (price * size + commission)
        expected_cost = 50000 * 0.1 * (1 + 0.001)  # with slippage
        assert broker.get_available_balance() < 10000

    def test_paper_broker_market_sell(self):
        """Test market sell order."""
        broker = PaperBroker(initial_balance=10000, commission=0.001)
        broker.update_price('BTC/USDT', 50000)

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

        # Update price and sell
        broker.update_price('BTC/USDT', 55000)
        result = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.SELL,
            order_type=OrderType.MARKET,
            size=0.1
        )

        assert result.success
        assert result.status == OrderStatus.FILLED

        # Position should be closed
        position = broker.get_position('BTC/USDT')
        assert position is None

    def test_paper_broker_insufficient_funds(self):
        """Test order rejection due to insufficient funds."""
        broker = PaperBroker(initial_balance=1000)
        broker.update_price('BTC/USDT', 50000)

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

        assert not result.success
        assert "Insufficient funds" in result.message

    def test_paper_broker_limit_order(self):
        """Test limit order placement and fill."""
        broker = PaperBroker(initial_balance=10000, commission=0.001)
        broker.update_price('BTC/USDT', 50000)

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

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

        # Order should be pending
        open_orders = broker.get_open_orders()
        assert len(open_orders) == 1

        # Update price below limit - should fill
        broker.update_price('BTC/USDT', 48000)
        events = broker.update()

        assert len(events) == 1
        assert events[0]['type'] == 'fill'

        # Now position should exist
        position = broker.get_position('BTC/USDT')
        assert position is not None
        assert position.size == 0.1

    def test_paper_broker_cancel_order(self):
        """Test order cancellation."""
        broker = PaperBroker(initial_balance=10000, commission=0, slippage=0)
        broker.update_price('BTC/USDT', 50000)

        initial_balance = broker.get_available_balance()

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

        assert result.success
        assert broker.get_available_balance() < initial_balance  # Funds locked

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

        # Funds should be released
        assert broker.get_available_balance() == initial_balance

    def test_paper_broker_pnl_tracking(self):
        """Test P&L tracking."""
        broker = PaperBroker(initial_balance=10000, commission=0, slippage=0)
        broker.update_price('BTC/USDT', 50000)

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

        # Price goes up
        broker.update_price('BTC/USDT', 52000)
        broker.update()

        position = broker.get_position('BTC/USDT')
        assert position is not None
        # Unrealized P&L: (52000 - 50000) * 0.1 = 200
        assert position.unrealized_pnl == 200

    def test_paper_broker_equity_calculation(self):
        """Test that equity = cash + position value (not just unrealized PnL)."""
        broker = PaperBroker(initial_balance=10000, commission=0, slippage=0)
        broker.update_price('BTC/USDT', 100)

        # Initial equity should equal initial balance
        assert broker.get_balance() == 10000

        # Buy 1 unit at $100 = spend $100
        broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=1.0
        )

        # Cash is now 9900, position value is 100
        # Total equity should still be 10000 (9900 + 100)
        assert broker.get_available_balance() == 9900  # Cash
        assert broker.get_balance() == 10000  # Total equity

        # Price doubles
        broker.update_price('BTC/USDT', 200)
        broker.update()

        # Cash still 9900, position value now 200
        # Total equity should be 10100 (9900 + 200)
        assert broker.get_available_balance() == 9900
        assert broker.get_balance() == 10100

    def test_paper_broker_reset(self):
        """Test broker reset."""
        broker = PaperBroker(initial_balance=10000)
        broker.update_price('BTC/USDT', 50000)
        broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1
        )

        broker.reset()

        assert broker.get_balance() == 10000
        assert broker.get_all_positions() == []
        assert broker.get_open_orders() == []


class TestBrokerFactory:
    """Tests for the broker factory."""

    def test_create_paper_broker(self):
        """Test creating paper broker via factory."""
        broker = create_broker(
            mode=TradingMode.PAPER,
            initial_balance=5000
        )
        assert isinstance(broker, PaperBroker)
        assert broker.get_balance() == 5000

    def test_create_backtest_broker(self):
        """Test creating backtest broker via factory."""
        broker = create_broker(
            mode=TradingMode.BACKTEST,
            initial_balance=10000
        )
        assert isinstance(broker, BacktestBroker)

    def test_create_live_broker_requires_exchange(self):
        """Test that live broker requires an exchange parameter."""
        with pytest.raises(ValueError, match="exchange"):
            create_broker(mode=TradingMode.LIVE, initial_balance=5000)

    def test_invalid_mode(self):
        """Test that invalid mode raises ValueError."""
        with pytest.raises(ValueError):
            create_broker(mode='invalid_mode')


class TestOrderResult:
    """Tests for OrderResult dataclass."""

    def test_order_result_success(self):
        """Test successful order result."""
        result = OrderResult(
            success=True,
            order_id='12345',
            status=OrderStatus.FILLED,
            filled_qty=0.1,
            filled_price=50000
        )
        assert result.success
        assert result.order_id == '12345'
        assert result.status == OrderStatus.FILLED

    def test_order_result_failure(self):
        """Test failed order result."""
        result = OrderResult(
            success=False,
            message="Insufficient funds"
        )
        assert not result.success
        assert "Insufficient" in result.message


class TestPosition:
    """Tests for Position dataclass."""

    def test_position_creation(self):
        """Test position creation."""
        position = Position(
            symbol='BTC/USDT',
            size=0.1,
            entry_price=50000,
            current_price=51000,
            unrealized_pnl=100
        )
        assert position.symbol == 'BTC/USDT'
        assert position.size == 0.1
        assert position.unrealized_pnl == 100


class TestExecutionPriceFallbacks:
    """Tests for execution-price safety fallbacks."""

    def test_exchange_interface_uses_caller_fallback_price(self):
        """When order_price is unavailable, caller fallback should be used."""
        exchange_interface = ExchangeInterface.__new__(ExchangeInterface)

        # If this gets called, fallback was not used correctly.
        exchange_interface.get_price = lambda symbol: (_ for _ in ()).throw(RuntimeError("exchange unavailable"))

        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=0.0,  # Market order style
            base_order_qty=1.0,
            order_type='MARKET'
        )
        trade.order = object()

        price = exchange_interface.get_trade_executed_price(trade, fallback_price=123.45)
        assert price == pytest.approx(123.45)

    def test_trades_update_fills_using_tick_price_fallback(self):
        """Trades.update should pass current tick price as execution fallback."""

        class _DummyUsers:
            @staticmethod
            def get_username(user_id):
                return "test_user"

        class _MockExchangeInterface:
            @staticmethod
            def get_trade_status(trade):
                return 'FILLED'

            @staticmethod
            def get_trade_executed_qty(trade):
                return trade.base_order_qty

            @staticmethod
            def get_trade_executed_price(trade, fallback_price=None):
                return fallback_price

        trades = Trades(users=_DummyUsers())
        trades.exchange_interface = _MockExchangeInterface()

        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=0.0,  # No explicit fill price available
            base_order_qty=1.0,
            order_type='MARKET'
        )
        trade.order_placed(order=object())
        trades.active_trades[trade.unique_id] = trade

        updates = trades.update({'BTC/USDT': 321.0})

        assert updates
        assert trade.status == 'filled'
        assert trade.stats['opening_price'] == pytest.approx(321.0)


class TestPaperBrokerSLTP:
    """Tests for Stop Loss / Take Profit functionality in PaperBroker."""

    def test_sl_triggers_on_price_drop(self):
        """Test that stop loss triggers when price drops below threshold."""
        broker = PaperBroker(initial_balance=10000, commission=0.001)
        broker.update_price('BTC/USDT', 50000)

        # Buy with SL at 45000
        result = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1,
            stop_loss=45000
        )
        assert result.success

        # Position exists
        position = broker.get_position('BTC/USDT')
        assert position is not None
        assert position.size == 0.1

        # SL is tracked
        assert 'BTC/USDT' in broker._position_sltp
        assert broker._position_sltp['BTC/USDT']['stop_loss'] == 45000

        # Price drops below SL
        broker.update_price('BTC/USDT', 44000)
        events = broker.update()

        # SL should have triggered
        sltp_events = [e for e in events if e.get('type') == 'sltp_triggered']
        assert len(sltp_events) == 1
        assert sltp_events[0]['trigger'] == 'stop_loss'
        assert sltp_events[0]['symbol'] == 'BTC/USDT'

        # Position should be closed
        position = broker.get_position('BTC/USDT')
        assert position is None

        # SL tracking cleared
        assert 'BTC/USDT' not in broker._position_sltp

    def test_tp_triggers_on_price_rise(self):
        """Test that take profit triggers when price rises above threshold."""
        broker = PaperBroker(initial_balance=10000, commission=0.001)
        broker.update_price('BTC/USDT', 50000)

        # Buy with TP at 55000
        result = broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1,
            take_profit=55000
        )
        assert result.success

        # TP is tracked
        assert 'BTC/USDT' in broker._position_sltp
        assert broker._position_sltp['BTC/USDT']['take_profit'] == 55000

        # Price rises above TP
        broker.update_price('BTC/USDT', 56000)
        events = broker.update()

        # TP should have triggered
        sltp_events = [e for e in events if e.get('type') == 'sltp_triggered']
        assert len(sltp_events) == 1
        assert sltp_events[0]['trigger'] == 'take_profit'

        # Position should be closed
        position = broker.get_position('BTC/USDT')
        assert position is None

    def test_sltp_cleared_on_manual_close(self):
        """Test that SL/TP tracking is cleared when position is manually closed."""
        broker = PaperBroker(initial_balance=10000, commission=0.001)
        broker.update_price('BTC/USDT', 50000)

        # Buy with SL and TP
        broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1,
            stop_loss=45000,
            take_profit=55000
        )

        assert 'BTC/USDT' in broker._position_sltp

        # Manually close position
        broker.close_position('BTC/USDT')

        # SL/TP tracking should be cleared
        assert 'BTC/USDT' not in broker._position_sltp

    def test_sltp_persists_across_state_save_load(self):
        """Test that SL/TP tracking persists across state save/load."""
        broker = PaperBroker(initial_balance=10000, commission=0.001)
        broker.update_price('BTC/USDT', 50000)

        # Buy with SL and TP
        broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1,
            stop_loss=45000,
            take_profit=55000
        )

        # Save state
        state = broker.to_state_dict()
        assert 'position_sltp' in state
        assert 'BTC/USDT' in state['position_sltp']

        # Create new broker and restore state
        broker2 = PaperBroker(initial_balance=10000)
        broker2.from_state_dict(state)

        # SL/TP should be restored
        assert 'BTC/USDT' in broker2._position_sltp
        assert broker2._position_sltp['BTC/USDT']['stop_loss'] == 45000
        assert broker2._position_sltp['BTC/USDT']['take_profit'] == 55000

    def test_no_sltp_trigger_when_price_within_range(self):
        """Test that no SL/TP triggers when price stays within range."""
        broker = PaperBroker(initial_balance=10000, commission=0.001)
        broker.update_price('BTC/USDT', 50000)

        # Buy with SL at 45000 and TP at 55000
        broker.place_order(
            symbol='BTC/USDT',
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            size=0.1,
            stop_loss=45000,
            take_profit=55000
        )

        # Price moves but stays within range
        broker.update_price('BTC/USDT', 48000)
        events = broker.update()

        # No SL/TP triggers
        sltp_events = [e for e in events if e.get('type') == 'sltp_triggered']
        assert len(sltp_events) == 0

        # Position still exists
        position = broker.get_position('BTC/USDT')
        assert position is not None
        assert position.size == 0.1
