"""Tests for exchange interface and mock exchange.

This module contains tests for MockExchange, Order, OrderType, and related models.
"""

import asyncio
import pytest

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


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

    @pytest.fixture
    async def exchange(self):
        """Fixture to create a MockExchange instance."""
        exchange = MockExchange(
            name="test_exchange",
            initial_balances={"USDT": 10000.0, "BTC": 1.0},
            latency_ms=0,  # No latency for faster tests
            slippage_pct=0.1,
        )
        await exchange.connect()
        yield exchange
        await exchange.disconnect()

    @pytest.mark.asyncio
    async def test_exchange_initialization(self):
        """Test MockExchange initialization."""
        exchange = MockExchange(
            name="test_exchange",
            initial_balances={"USDT": 10000.0},
        )

        assert exchange.name == "test_exchange"
        assert exchange.latency_ms == 10.0  # Default
        assert exchange.slippage_pct == 0.1  # Default

    @pytest.mark.asyncio
    async def test_default_initial_balances(self):
        """Test default initial balances."""
        exchange = MockExchange()

        balances = await exchange.get_balance()

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

    @pytest.mark.asyncio
    async def test_connect_disconnect(self):
        """Test connect and disconnect."""
        exchange = MockExchange()

        connected = await exchange.connect()
        assert connected is True

        await exchange.disconnect()
        # No assertion needed, just verify no exception

    @pytest.mark.asyncio
    async def test_get_balance_all(self):
        """Test getting all balances."""
        exchange = MockExchange(initial_balances={"USDT": 10000.0, "BTC": 1.0})
        await exchange.connect()

        balances = await exchange.get_balance()

        assert len(balances) == 2
        assets = {b.asset for b in balances}
        assert assets == {"USDT", "BTC"}

    @pytest.mark.asyncio
    async def test_get_balance_specific(self):
        """Test getting specific asset balance."""
        exchange = MockExchange(initial_balances={"USDT": 10000.0, "BTC": 1.0})
        await exchange.connect()

        balances = await exchange.get_balance("BTC")

        assert len(balances) == 1
        assert balances[0].asset == "BTC"
        assert balances[0].free == 1.0

    @pytest.mark.asyncio
    async def test_get_balance_nonexistent(self):
        """Test getting nonexistent asset balance."""
        exchange = MockExchange(initial_balances={"USDT": 10000.0})
        await exchange.connect()

        balances = await exchange.get_balance("ETH")

        assert balances == []

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

        order = await exchange.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.filled_amount == 0.1

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

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

        assert order.symbol == "BTC/USDT"
        assert order.side == OrderSide.SELL
        assert order.amount == 0.5
        assert order.status == OrderStatus.FILLED

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

        with pytest.raises(InsufficientFundsError):
            await exchange.place_order(
                symbol="BTC/USDT",
                side=OrderSide.BUY,
                amount=1.0,  # Costs ~$65k
            )

    @pytest.mark.asyncio
    async def test_place_order_insufficient_funds_sell(self):
        """Test sell order fails with insufficient funds."""
        exchange = MockExchange(initial_balances={"USDT": 10000.0, "BTC": 0.1})
        await exchange.connect()

        with pytest.raises(InsufficientFundsError):
            await exchange.place_order(
                symbol="BTC/USDT",
                side=OrderSide.SELL,
                amount=1.0,  # Only have 0.1 BTC
            )

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

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

        retrieved = await exchange.get_order(order.order_id)

        assert retrieved is not None
        assert retrieved.order_id == order.order_id

    @pytest.mark.asyncio
    async def test_get_order_nonexistent(self):
        """Test getting nonexistent order returns None."""
        exchange = MockExchange()
        await exchange.connect()

        retrieved = await exchange.get_order("nonexistent")

        assert retrieved is None

    @pytest.mark.asyncio
    async def test_cancel_order(self):
        """Test cancelling an order."""
        # Note: In mock exchange, orders are filled immediately
        # so cancellation typically won't work for market orders
        exchange = MockExchange(initial_balances={"USDT": 10000.0})
        await exchange.connect()

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

        # Already filled, so cancel should fail
        cancelled = await exchange.cancel_order(order.order_id)
        assert cancelled is False

    @pytest.mark.asyncio
    async def test_cancel_order_nonexistent(self):
        """Test cancelling nonexistent order returns False."""
        exchange = MockExchange()
        await exchange.connect()

        cancelled = await exchange.cancel_order("nonexistent")

        assert cancelled is False

    @pytest.mark.asyncio
    async def test_get_open_orders(self):
        """Test getting open orders."""
        exchange = MockExchange(initial_balances={"USDT": 10000.0})
        await exchange.connect()

        # Place an order (fills immediately in mock)
        await exchange.place_order(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=0.1,
        )

        # Since orders fill immediately, there should be no open orders
        open_orders = await exchange.get_open_orders()

        assert len(open_orders) == 0

    @pytest.mark.asyncio
    async def test_get_open_orders_filtered(self):
        """Test getting open orders filtered by symbol."""
        exchange = MockExchange(initial_balances={"USDT": 10000.0})
        await exchange.connect()

        # All orders fill immediately, so test the filter works
        open_orders = await exchange.get_open_orders("BTC/USDT")

        assert open_orders == []

    @pytest.mark.asyncio
    async def test_get_positions(self):
        """Test getting positions."""
        exchange = MockExchange(
            initial_balances={"USDT": 50000.0, "BTC": 1.0}  # Need more USDT for BTC purchase
        )
        await exchange.connect()

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

        # After buy, should have position
        await exchange.place_order(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=0.1,  # Smaller amount for test
        )

        positions = await exchange.get_positions()
        assert len(positions) == 1
        assert positions[0].symbol == "BTC/USDT"

    @pytest.mark.asyncio
    async def test_get_positions_filtered(self):
        """Test getting positions filtered by symbol."""
        exchange = MockExchange(
            initial_balances={"USDT": 50000.0, "BTC": 1.0, "ETH": 10.0}  # More USDT
        )
        await exchange.connect()

        await exchange.place_order(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=0.1,
        )
        await exchange.place_order(
            symbol="ETH/USDT",
            side=OrderSide.BUY,
            amount=2.0,
        )

        btc_positions = await exchange.get_positions("BTC/USDT")

        assert len(btc_positions) == 1
        assert btc_positions[0].symbol == "BTC/USDT"

    @pytest.mark.asyncio
    async def test_get_ticker(self):
        """Test getting ticker data."""
        exchange = MockExchange()
        await exchange.connect()

        ticker = await exchange.get_ticker("BTC/USDT")

        assert ticker.symbol == "BTC/USDT"
        assert ticker.bid > 0
        assert ticker.ask > 0
        assert ticker.last > 0
        assert ticker.ask > ticker.bid  # Ask should be higher than bid

    @pytest.mark.asyncio
    async def test_get_ticker_unknown_symbol(self):
        """Test getting ticker for unknown symbol."""
        exchange = MockExchange()
        await exchange.connect()

        ticker = await exchange.get_ticker("UNKNOWN/PAIR")

        assert ticker.symbol == "UNKNOWN/PAIR"
        assert ticker.last > 0  # Should generate a default price

    @pytest.mark.asyncio
    async def test_update_ticker(self):
        """Test manually updating ticker price."""
        exchange = MockExchange()
        await exchange.connect()

        exchange.update_ticker("BTC/USDT", 70000.0)

        ticker = await exchange.get_ticker("BTC/USDT")

        # get_ticker applies random price movement, so check approximate values
        assert abs(ticker.last - 70000.0) < 100  # Within $100 of expected
        assert ticker.bid < ticker.last  # Bid should be less than last
        assert ticker.ask > ticker.last  # Ask should be more than last
        assert abs(ticker.bid - 70000.0 * 0.9995) < 100
        assert abs(ticker.ask - 70000.0 * 1.0005) < 100

    @pytest.mark.asyncio
    async def test_set_balance(self):
        """Test manually setting balance."""
        exchange = MockExchange()
        await exchange.connect()

        exchange.set_balance("ETH", 5.0)

        balances = await exchange.get_balance("ETH")

        assert len(balances) == 1
        assert balances[0].asset == "ETH"
        assert balances[0].free == 5.0

    @pytest.mark.asyncio
    async def test_buy_order_updates_balances(self):
        """Test that buy order correctly updates balances."""
        exchange = MockExchange(initial_balances={"USDT": 10000.0})
        await exchange.connect()

        initial_usdt = (await exchange.get_balance("USDT"))[0].free

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

        final_usdt = (await exchange.get_balance("USDT"))[0].free
        btc_balance = (await exchange.get_balance("BTC"))[0].free

        assert final_usdt < initial_usdt  # USDT decreased
        assert btc_balance == 0.1  # BTC increased

    @pytest.mark.asyncio
    async def test_sell_order_updates_balances(self):
        """Test that sell order correctly updates balances."""
        exchange = MockExchange(
            initial_balances={"USDT": 10000.0, "BTC": 1.0}
        )
        await exchange.connect()

        initial_btc = (await exchange.get_balance("BTC"))[0].free

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

        final_btc = (await exchange.get_balance("BTC"))[0].free
        usdt_balance = (await exchange.get_balance("USDT"))[0].free

        assert final_btc == initial_btc - 0.5  # BTC decreased
        assert usdt_balance > 10000.0  # USDT increased


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

    def test_order_creation(self):
        """Test creating an Order."""
        order = Order(
            order_id="order123",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            order_type=OrderType.MARKET,
            amount=1.0,
            price=50000.0,
            status=OrderStatus.FILLED,
            filled_amount=1.0,
        )

        assert order.order_id == "order123"
        assert order.symbol == "BTC/USDT"
        assert order.side == OrderSide.BUY
        assert order.order_type == OrderType.MARKET
        assert order.amount == 1.0
        assert order.price == 50000.0
        assert order.status == OrderStatus.FILLED

    def test_order_is_filled(self):
        """Test is_filled property."""
        filled_order = Order(
            order_id="order1",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=1.0,
            status=OrderStatus.FILLED,
            filled_amount=1.0,
        )

        pending_order = Order(
            order_id="order2",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=1.0,
            status=OrderStatus.PENDING,
            filled_amount=0.0,
        )

        assert filled_order.is_filled is True
        assert pending_order.is_filled is False

    def test_order_remaining_amount(self):
        """Test remaining_amount property."""
        order = Order(
            order_id="order1",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=1.0,
            status=OrderStatus.PARTIALLY_FILLED,
            filled_amount=0.5,
        )

        assert order.remaining_amount == 0.5

    def test_order_fill_percentage(self):
        """Test fill_percentage property."""
        order = Order(
            order_id="order1",
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=1.0,
            status=OrderStatus.PARTIALLY_FILLED,
            filled_amount=0.75,
        )

        assert order.fill_percentage == 75.0


class TestOrderType:
    """Tests for OrderType enum."""

    def test_order_type_values(self):
        """Test OrderType enum values."""
        assert OrderType.MARKET.value == "market"
        assert OrderType.LIMIT.value == "limit"
        assert OrderType.STOP_LOSS.value == "stop_loss"
        assert OrderType.TAKE_PROFIT.value == "take_profit"


class TestOrderSide:
    """Tests for OrderSide enum."""

    def test_order_side_values(self):
        """Test OrderSide enum values."""
        assert OrderSide.BUY.value == "buy"
        assert OrderSide.SELL.value == "sell"


class TestOrderStatus:
    """Tests for OrderStatus enum."""

    def test_order_status_values(self):
        """Test OrderStatus enum values."""
        assert OrderStatus.PENDING.value == "pending"
        assert OrderStatus.OPEN.value == "open"
        assert OrderStatus.PARTIALLY_FILLED.value == "partially_filled"
        assert OrderStatus.FILLED.value == "filled"
        assert OrderStatus.CANCELLED.value == "cancelled"
        assert OrderStatus.REJECTED.value == "rejected"
        assert OrderStatus.EXPIRED.value == "expired"


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

    def test_balance_creation(self):
        """Test creating a Balance."""
        balance = Balance(
            asset="BTC",
            free=1.5,
            locked=0.5,
        )

        assert balance.asset == "BTC"
        assert balance.free == 1.5
        assert balance.locked == 0.5

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

        assert balance.total == 2.0


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

    def test_position_creation(self):
        """Test creating a Position."""
        position = Position(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=1.0,
            entry_price=50000.0,
            current_price=55000.0,
        )

        assert position.symbol == "BTC/USDT"
        assert position.side == OrderSide.BUY
        assert position.amount == 1.0
        assert position.entry_price == 50000.0

    def test_position_unrealized_pnl_long(self):
        """Test unrealized PnL for long position."""
        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_unrealized_pnl_short(self):
        """Test unrealized PnL for short position."""
        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_unrealized_pnl_percentage(self):
        """Test unrealized PnL percentage."""
        position = Position(
            symbol="BTC/USDT",
            side=OrderSide.BUY,
            amount=1.0,
            entry_price=50000.0,
            current_price=55000.0,
        )

        assert position.unrealized_pnl_pct == 10.0  # 10% gain

    def test_position_market_value(self):
        """Test position market value."""
        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_creation(self):
        """Test creating a Ticker."""
        ticker = Ticker(
            symbol="BTC/USDT",
            bid=64000.0,
            ask=64100.0,
            last=64050.0,
            high=65000.0,
            low=63000.0,
            volume=1000000.0,
        )

        assert ticker.symbol == "BTC/USDT"
        assert ticker.bid == 64000.0
        assert ticker.ask == 64100.0
        assert ticker.last == 64050.0

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

        assert ticker.spread == 100.0

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

        expected_spread_pct = (100.0 / 64050.0) * 100
        assert abs(ticker.spread_pct - expected_spread_pct) < 0.01

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

        assert ticker.mid_price == 64050.0
