"""Unit tests for exchange module."""

import asyncio
import pytest
from datetime import datetime

from openclaw.exchange.models import (
    Balance,
    Order,
    OrderSide,
    OrderStatus,
    OrderType,
    Position,
    Ticker,
)
from openclaw.exchange.base import Exchange, ExchangeError, InsufficientFundsError
from openclaw.exchange.mock import MockExchange
from openclaw.exchange.binance import BinanceExchange


class TestOrder:
    """Tests for Order model."""

    def test_order_creation(self):
        """Test basic order creation."""
        order = Order(
            order_id="test-123",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=1.0,
            price=50000.0,
        )
        assert order.order_id == "test-123"
        assert order.symbol == "BTC/USDT"
        assert order.side == OrderSide.BUY
        assert order.amount == 1.0
        assert order.price == 50000.0
        assert order.status == OrderStatus.PENDING

    def test_order_is_filled(self):
        """Test order fill detection."""
        order = Order(
            order_id="test-123",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=1.0,
            status=OrderStatus.FILLED,
            filled_amount=1.0,
        )
        assert order.is_filled is True

    def test_order_remaining_amount(self):
        """Test remaining amount calculation."""
        order = Order(
            order_id="test-123",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=2.0,
            filled_amount=1.5,
        )
        assert order.remaining_amount == 0.5

    def test_order_fill_percentage(self):
        """Test fill percentage calculation."""
        order = Order(
            order_id="test-123",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=2.0,
            filled_amount=1.0,
        )
        assert order.fill_percentage == 50.0


class TestBalance:
    """Tests for Balance model."""

    def test_balance_total(self):
        """Test total balance calculation."""
        balance = Balance(asset="BTC", free=1.0, locked=0.5)
        assert balance.total == 1.5

    def test_balance_zero(self):
        """Test zero balance."""
        balance = Balance(asset="USDT", free=0.0, locked=0.0)
        assert balance.total == 0.0


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

    def test_position_long_pnl(self):
        """Test long position PnL calculation."""
        position = Position(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=1.0,
            entry_price=50000.0,
            current_price=55000.0,
        )
        assert position.unrealized_pnl == 5000.0

    def test_position_short_pnl(self):
        """Test short position PnL calculation."""
        position = Position(
            symbol="BTC/USDT",
            side=OrderSide.SELL,
            amount=1.0,
            entry_price=50000.0,
            current_price=45000.0,
        )
        assert position.unrealized_pnl == 5000.0

    def test_position_market_value(self):
        """Test market value calculation."""
        position = Position(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=2.0,
            entry_price=50000.0,
            current_price=55000.0,
        )
        assert position.market_value == 110000.0


class TestTicker:
    """Tests for Ticker model."""

    def test_ticker_spread(self):
        """Test spread calculation."""
        ticker = Ticker(
            symbol="BTC/USDT",
            bid=64000.0,
            ask=64100.0,
            last=64050.0,
        )
        assert ticker.spread == 100.0

    def test_ticker_mid_price(self):
        """Test mid price calculation."""
        ticker = Ticker(
            symbol="BTC/USDT",
            bid=64000.0,
            ask=64100.0,
            last=64050.0,
        )
        assert ticker.mid_price == 64050.0


class TestMockExchange:
    """Tests for MockExchange implementation."""

    @pytest.fixture
    async def exchange(self):
        """Create a mock exchange for testing."""
        ex = MockExchange(
            name="test_mock",
            initial_balances={"USDT": 10000.0, "BTC": 1.0},
            latency_ms=0,  # No latency for tests
        )
        await ex.connect()
        yield ex
        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_connect_disconnect(self):
        """Test connection lifecycle."""
        ex = MockExchange()
        result = await ex.connect()
        assert result is True
        assert ex.is_connected is True
        await ex.disconnect()
        assert ex.is_connected is False

    @pytest.mark.asyncio
    async def test_get_balance(self):
        """Test balance retrieval."""
        ex = MockExchange(initial_balances={"USDT": 10000.0})
        await ex.connect()

        balances = await ex.get_balance()
        assert len(balances) == 1
        assert balances[0].asset == "USDT"
        assert balances[0].free == 10000.0

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_get_ticker(self):
        """Test ticker retrieval."""
        ex = MockExchange()
        await ex.connect()

        ticker = await ex.get_ticker("BTC/USDT")
        assert ticker.symbol == "BTC/USDT"
        assert ticker.bid > 0
        assert ticker.ask > 0
        assert ticker.last > 0

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_place_market_order_buy(self):
        """Test placing a buy market order."""
        ex = MockExchange(initial_balances={"USDT": 10000.0})
        await ex.connect()

        order = await ex.place_order(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=0.1,
        )

        assert order.symbol == "BTC/USDT"
        assert order.side == OrderSide.BUY
        assert order.amount == 0.1
        assert order.status == OrderStatus.FILLED
        assert order.is_filled is True

        # Check balance was deducted
        balances = await ex.get_balance("USDT")
        assert balances[0].free < 10000.0

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_place_market_order_sell(self):
        """Test placing a sell market order."""
        ex = MockExchange(initial_balances={"BTC": 1.0, "USDT": 10000.0})
        await ex.connect()

        order = await ex.place_order(
            symbol="BTC/USDT",
            side=OrderSide.SELL,
            amount=0.5,
        )

        assert order.side == OrderSide.SELL
        assert order.amount == 0.5
        assert order.status == OrderStatus.FILLED

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_insufficient_funds(self):
        """Test order with insufficient funds."""
        ex = MockExchange(initial_balances={"USDT": 100.0})
        await ex.connect()

        with pytest.raises(InsufficientFundsError):
            await ex.place_order(
                symbol="BTC/USDT",
                side=OrderSide.BUY,
                amount=1.0,  # Too much for $100 balance
            )

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_cancel_order(self):
        """Test order cancellation."""
        ex = MockExchange(initial_balances={"USDT": 10000.0})
        await ex.connect()

        # Place an order
        order = await ex.place_order(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=0.1,
        )

        # Market orders fill immediately in mock, so can't cancel
        # Test cancel returns False for already filled orders
        result = await ex.cancel_order(order.order_id)
        assert result is False

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_get_order(self):
        """Test retrieving order details."""
        ex = MockExchange(initial_balances={"USDT": 10000.0})
        await ex.connect()

        order = await ex.place_order(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=0.1,
        )

        retrieved = await ex.get_order(order.order_id)
        assert retrieved is not None
        assert retrieved.order_id == order.order_id

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_get_positions(self):
        """Test position retrieval."""
        ex = MockExchange(initial_balances={"USDT": 10000.0})
        await ex.connect()

        # Initially no positions
        positions = await ex.get_positions()
        assert len(positions) == 0

        # Place an order to create position
        await ex.place_order(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=0.1,
        )

        # Now should have a position
        positions = await ex.get_positions()
        assert len(positions) == 1
        assert positions[0].symbol == "BTC/USDT"

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_update_ticker(self):
        """Test manual ticker update."""
        ex = MockExchange()
        await ex.connect()

        ex.update_ticker("BTC/USDT", 70000.0)
        ticker = await ex.get_ticker("BTC/USDT")
        # Allow for small price movement simulation (within 1%)
        assert abs(ticker.last - 70000.0) < 700.0

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_set_balance(self):
        """Test manual balance update."""
        ex = MockExchange()
        await ex.connect()

        ex.set_balance("ETH", 10.0)
        balances = await ex.get_balance("ETH")
        assert len(balances) == 1
        assert balances[0].free == 10.0

        await ex.disconnect()


class TestBinanceExchange:
    """Tests for BinanceExchange implementation."""

    @pytest.mark.asyncio
    async def test_simulated_mode(self):
        """Test that simulated mode uses mock."""
        ex = BinanceExchange(is_simulated=True)
        assert ex.is_simulated is True
        assert ex._mock is not None

        result = await ex.connect()
        assert result is True

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_live_mode_requires_credentials(self):
        """Test that live mode requires API credentials."""
        with pytest.raises(Exception):  # AuthenticationError
            BinanceExchange(is_simulated=False, api_key=None, api_secret=None)

    @pytest.mark.asyncio
    async def test_place_order_simulated(self):
        """Test placing order in simulated mode."""
        ex = BinanceExchange(is_simulated=True)
        await ex.connect()

        order = await ex.place_order(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=0.1,
        )

        assert order.symbol == "BTC/USDT"
        assert order.side == OrderSide.BUY

        await ex.disconnect()

    @pytest.mark.asyncio
    async def test_get_ticker_simulated(self):
        """Test getting ticker in simulated mode."""
        ex = BinanceExchange(is_simulated=True)
        await ex.connect()

        ticker = await ex.get_ticker("ETH/USDT")
        assert ticker.symbol == "ETH/USDT"
        assert ticker.last > 0

        await ex.disconnect()


class TestExchangeError:
    """Tests for exchange exceptions."""

    def test_exchange_error_basic(self):
        """Test basic error creation."""
        err = ExchangeError("Something went wrong")
        assert str(err) == "Something went wrong"
        assert err.message == "Something went wrong"
        assert err.error_code is None

    def test_exchange_error_with_code(self):
        """Test error with code."""
        err = ExchangeError("Rate limit exceeded", error_code="RATE_LIMIT")
        assert str(err) == "[RATE_LIMIT] Rate limit exceeded"
        assert err.error_code == "RATE_LIMIT"

    def test_insufficient_funds_error(self):
        """Test insufficient funds error."""
        err = InsufficientFundsError("Not enough BTC")
        assert isinstance(err, ExchangeError)
        assert "BTC" in str(err)
