"""Tests for the Trade and Trades classes."""
import pytest
from unittest.mock import MagicMock, patch

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

from trade import Trade, Trades


class TestTrade:
    """Tests for the Trade class."""

    def test_trade_creation(self):
        """Test basic trade creation."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=50000.0,
            base_order_qty=0.1
        )

        assert trade.symbol == 'BTC/USDT'
        assert trade.side == 'BUY'
        assert trade.order_price == 50000.0
        assert trade.base_order_qty == 0.1
        assert trade.status == 'inactive'
        assert trade.is_paper is False
        assert trade.unique_id is not None

    def test_trade_paper_flag(self):
        """Test is_paper flag."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=50000.0,
            base_order_qty=0.1,
            is_paper=True
        )

        assert trade.is_paper is True

    def test_trade_to_json(self):
        """Test trade serialization."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=50000.0,
            base_order_qty=0.1,
            is_paper=True,
            creator=1
        )

        json_data = trade.to_json()

        assert json_data['symbol'] == 'BTC/USDT'
        assert json_data['side'] == 'BUY'
        assert json_data['is_paper'] is True
        assert json_data['creator'] == 1
        assert 'stats' in json_data

    def test_trade_with_stop_loss_take_profit(self):
        """Test trade with SL/TP fields."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=50000.0,
            base_order_qty=0.1,
            stop_loss=45000.0,
            take_profit=60000.0
        )

        assert trade.stop_loss == 45000.0
        assert trade.take_profit == 60000.0

        # Verify serialization
        json_data = trade.to_json()
        assert json_data['stop_loss'] == 45000.0
        assert json_data['take_profit'] == 60000.0

    def test_trade_sltp_defaults_to_none(self):
        """Test that SL/TP default to None when not provided."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=50000.0,
            base_order_qty=0.1
        )

        assert trade.stop_loss is None
        assert trade.take_profit is None

    def test_trade_update_values(self):
        """Test P/L calculation."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=50000.0,
            base_order_qty=0.1,
            fee=0.001  # 0.1% fee
        )
        trade.status = 'filled'
        trade.stats['qty_filled'] = 0.1

        # Price goes up
        trade.update_values(55000.0)

        assert trade.stats['current_price'] == 55000.0
        assert trade.stats['current_value'] == 5500.0
        # Profit should be positive (minus fees)
        assert trade.stats['profit'] > 0

    def test_trade_sell_side_pl(self):
        """Test P/L calculation for sell side."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='SELL',
            order_price=50000.0,
            base_order_qty=0.1,
            fee=0.001
        )
        trade.status = 'filled'
        trade.stats['qty_filled'] = 0.1

        # Price goes down - should be profit for sell
        trade.update_values(45000.0)

        assert trade.stats['profit'] > 0

    def test_trade_filled(self):
        """Test trade fill logic."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=50000.0,
            base_order_qty=0.1
        )

        trade.trade_filled(qty=0.1, price=50000.0)

        assert trade.status == 'filled'
        assert trade.stats['qty_filled'] == 0.1
        assert trade.stats['opening_price'] == 50000.0

    def test_trade_partial_fill(self):
        """Test partial fill logic."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=50000.0,
            base_order_qty=0.1
        )

        trade.trade_filled(qty=0.05, price=50000.0)

        assert trade.status == 'part-filled'
        assert trade.stats['qty_filled'] == 0.05

    def test_trade_settle(self):
        """Test trade settlement."""
        trade = Trade(
            target='test_exchange',
            symbol='BTC/USDT',
            side='BUY',
            order_price=50000.0,
            base_order_qty=0.1
        )
        trade.trade_filled(qty=0.1, price=50000.0)

        trade.settle(qty=0.1, price=55000.0)

        assert trade.status == 'closed'
        assert trade.stats['settled_price'] == 55000.0


class TestTrades:
    """Tests for the Trades class."""

    @pytest.fixture
    def mock_users(self):
        """Create a mock Users object."""
        users = MagicMock()
        users.get_username.return_value = 'test_user'
        return users

    @pytest.fixture
    def mock_data_cache(self):
        """Create a mock DataCache object."""
        dc = MagicMock()
        dc.db.table_exists.return_value = True
        dc.get_all_rows_from_datacache.return_value = None
        dc.get_rows_from_datacache.return_value = MagicMock(empty=True)
        return dc

    def test_trades_creation_no_cache(self, mock_users):
        """Test Trades creation without data cache."""
        trades = Trades(mock_users)

        assert trades.users == mock_users
        assert trades.data_cache is None
        assert len(trades.active_trades) == 0

    def test_trades_creation_with_cache(self, mock_users, mock_data_cache):
        """Test Trades creation with data cache."""
        trades = Trades(mock_users, data_cache=mock_data_cache)

        assert trades.data_cache == mock_data_cache

    def test_connect_exchanges(self, mock_users):
        """Test exchange connection."""
        trades = Trades(mock_users)
        mock_exchange = MagicMock()

        trades.connect_exchanges(mock_exchange)

        assert trades.exchange_interface == mock_exchange
        assert trades.exchange_connected() is True

    def test_new_paper_trade(self, mock_users):
        """Test creating a paper trade."""
        trades = Trades(mock_users)

        status, trade_id = trades.new_trade(
            target='test_exchange',
            symbol='BTC/USDT',
            price=50000.0,
            side='buy',
            order_type='MARKET',
            qty=0.1,
            user_id=1
        )

        assert status == 'Success'
        assert trade_id is not None
        assert trade_id in trades.active_trades

        # Check trade properties
        trade = trades.get_trade_by_id(trade_id)
        assert trade.is_paper is True
        assert trade.status == 'filled'
        assert trade.creator == 1

    def test_new_live_trade_no_exchange(self, mock_users):
        """Test creating a live trade without exchange connected."""
        trades = Trades(mock_users)

        # Use testnet=True to bypass production safety gate and test exchange check
        status, msg = trades.new_trade(
            target='binance',
            symbol='BTC/USDT',
            price=50000.0,
            side='buy',
            order_type='MARKET',
            qty=0.1,
            user_id=1,
            testnet=True
        )

        assert status == 'Error'
        assert 'No exchange' in msg.lower() or 'no exchange' in msg.lower()

    def test_new_production_trade_blocked_without_env_var(self, mock_users):
        """Test that production trades are blocked without ALLOW_LIVE_PRODUCTION."""
        import config
        original_value = getattr(config, 'ALLOW_LIVE_PRODUCTION', False)

        try:
            # Ensure production is NOT allowed
            config.ALLOW_LIVE_PRODUCTION = False

            trades = Trades(mock_users)
            status, msg = trades.new_trade(
                target='binance',
                symbol='BTC/USDT',
                price=50000.0,
                side='buy',
                order_type='MARKET',
                qty=0.1,
                user_id=1,
                testnet=False  # Production mode
            )

            assert status == 'Error'
            assert 'production trading is disabled' in msg.lower()

        finally:
            # Restore original value
            config.ALLOW_LIVE_PRODUCTION = original_value

    def test_get_trades_json(self, mock_users):
        """Test getting trades in JSON format."""
        trades = Trades(mock_users)

        # Create a paper trade
        trades.new_trade(
            target='test_exchange',
            symbol='BTC/USDT',
            price=50000.0,
            side='buy',
            order_type='MARKET',
            qty=0.1
        )

        result = trades.get_trades('json')

        assert len(result) == 1
        assert result[0]['symbol'] == 'BTC/USDT'

    def test_get_trades_for_user(self, mock_users):
        """Test filtering trades by user."""
        trades = Trades(mock_users)

        # Create trades for different users
        trades.new_trade(
            target='test_exchange',
            symbol='BTC/USDT',
            price=50000.0,
            side='buy',
            order_type='MARKET',
            qty=0.1,
            user_id=1
        )
        trades.new_trade(
            target='test_exchange',
            symbol='ETH/USDT',
            price=3000.0,
            side='buy',
            order_type='MARKET',
            qty=1.0,
            user_id=2
        )

        # Filter for user 1
        user1_trades = trades.get_trades_for_user(1, 'json')
        assert len(user1_trades) == 1
        assert user1_trades[0]['symbol'] == 'BTC/USDT'

    def test_close_paper_trade(self, mock_users):
        """Test closing a paper trade."""
        trades = Trades(mock_users)

        # Create a paper trade
        status, trade_id = trades.new_trade(
            target='test_exchange',
            symbol='BTC/USDT',
            price=50000.0,
            side='buy',
            order_type='MARKET',
            qty=0.1
        )

        assert trade_id in trades.active_trades

        # Close the trade
        result = trades.close_trade(trade_id, current_price=55000.0)

        assert result['success'] is True
        assert trade_id not in trades.active_trades
        assert trade_id in trades.settled_trades

    def test_close_nonexistent_trade(self, mock_users):
        """Test closing a trade that doesn't exist."""
        trades = Trades(mock_users)

        result = trades.close_trade('nonexistent_id')

        assert result['success'] is False
        assert 'not found' in result['message']

    def test_is_valid_trade_id(self, mock_users):
        """Test trade ID validation."""
        trades = Trades(mock_users)

        _, trade_id = trades.new_trade(
            target='test_exchange',
            symbol='BTC/USDT',
            price=50000.0,
            side='buy',
            order_type='MARKET',
            qty=0.1
        )

        assert trades.is_valid_trade_id(trade_id) is True
        assert trades.is_valid_trade_id('invalid_id') is False

    def test_update_trades(self, mock_users):
        """Test updating trade P/L with price changes."""
        trades = Trades(mock_users)

        # Create and fill a trade
        _, trade_id = trades.new_trade(
            target='test_exchange',
            symbol='BTC/USDT',
            price=50000.0,
            side='buy',
            order_type='MARKET',
            qty=0.1
        )

        # Update with new price
        price_updates = {'BTC/USDT': 55000.0}
        updates = trades.update(price_updates)

        assert len(updates) > 0
        # Find our trade in updates
        trade_update = next((u for u in updates if u['id'] == trade_id), None)
        assert trade_update is not None
        assert trade_update['pl'] != 0  # Should have some P/L

    def test_buy_method_paper(self, mock_users):
        """Test buy method creates a BUY paper trade using new_trade."""
        trades = Trades(mock_users)

        # Use new_trade for paper trades (buy/sell methods are for live trading)
        status, trade_id = trades.new_trade(
            target='test_exchange',
            symbol='BTC/USDT',
            price=50000.0,
            side='buy',
            order_type='MARKET',
            qty=0.1,
            user_id=1
        )

        assert status == 'Success'
        trade = trades.get_trade_by_id(trade_id)
        assert trade.side == 'BUY'

    def test_sell_method_paper(self, mock_users):
        """Test sell method creates a SELL paper trade using new_trade."""
        trades = Trades(mock_users)

        # Use new_trade for paper trades (buy/sell methods are for live trading)
        status, trade_id = trades.new_trade(
            target='test_exchange',
            symbol='BTC/USDT',
            price=50000.0,
            side='sell',
            order_type='MARKET',
            qty=0.1,
            user_id=1
        )

        assert status == 'Success'
        trade = trades.get_trade_by_id(trade_id)
        assert trade.side == 'SELL'


class TestTradeIntegration:
    """Integration tests for Trade system."""

    @pytest.fixture
    def mock_users(self):
        users = MagicMock()
        users.get_username.return_value = 'test_user'
        return users

    def test_full_trade_lifecycle(self, mock_users):
        """Test complete lifecycle: create -> update -> close."""
        trades = Trades(mock_users)

        # Create trade
        status, trade_id = trades.new_trade(
            target='test_exchange',
            symbol='BTC/USDT',
            price=50000.0,
            side='buy',
            order_type='MARKET',
            qty=0.1,
            user_id=1
        )
        assert status == 'Success'

        trade = trades.get_trade_by_id(trade_id)
        # Set a more realistic fee (0.1% instead of default 10%)
        trade.fee = 0.001
        trade.stats['fee_paid'] = trade.stats['opening_value'] * trade.fee

        # Update with higher price (20% increase should exceed fees)
        trades.update({'BTC/USDT': 60000.0})

        trade = trades.get_trade_by_id(trade_id)
        assert trade.stats['profit'] > 0  # Should be in profit

        # Close trade
        result = trades.close_trade(trade_id, current_price=60000.0)
        assert result['success'] is True
        assert result['final_pl'] > 0

    def test_multiple_trades(self, mock_users):
        """Test managing multiple trades."""
        trades = Trades(mock_users)

        # Create multiple trades
        trade_ids = []
        for i in range(3):
            _, trade_id = trades.new_trade(
                target='test_exchange',
                symbol='BTC/USDT',
                price=50000.0 + (i * 100),
                side='buy',
                order_type='MARKET',
                qty=0.1
            )
            trade_ids.append(trade_id)

        assert len(trades.active_trades) == 3

        # Close one trade
        trades.close_trade(trade_ids[1])

        assert len(trades.active_trades) == 2
        assert trade_ids[1] not in trades.active_trades
