"""Tests for the TradeModule."""

from __future__ import annotations

from unittest.mock import AsyncMock, patch

import pytest

from openhedge.exceptions import AuthenticationError, InvalidPairError
from openhedge.models.trade import TradeResult, TradeType
from tests.conftest import (
    FAKE_PRIVATE_KEY,
    FAKE_TX_HASH,
    FAKE_VAULT,
    make_rpc_response,
    mock_receipt,
)


def _setup_trade_mocks(agent, mock_receipt_data):
    """Set up the standard mock chain for a write transaction:
    eth_getBlockByNumber, eth_getTransactionCount, eth_estimateGas,
    eth_gasPrice, eth_sendRawTransaction, eth_getTransactionReceipt
    """
    responses = [
        # eth_getBlockByNumber (for deadline in swap)
        make_rpc_response({"timestamp": hex(1700000000), "number": "0x10"}),
        # eth_getTransactionCount
        make_rpc_response("0x5"),
        # eth_estimateGas
        make_rpc_response("0x493e0"),  # 300,000
        # eth_gasPrice
        make_rpc_response("0x3b9aca00"),  # 1 Gwei
        # eth_sendRawTransaction
        make_rpc_response(FAKE_TX_HASH),
        # eth_getTransactionReceipt
        make_rpc_response(mock_receipt_data),
    ]

    call_count = 0

    async def mock_post(*args, **kwargs):
        nonlocal call_count
        body = kwargs.get("json") or (args[1] if len(args) > 1 else {})
        resp = responses[min(call_count, len(responses) - 1)]
        call_count += 1
        return resp

    return mock_post


class TestTradeSwap:
    async def test_swap_success(self, agent, mock_receipt):
        mock_post = _setup_trade_mocks(agent, mock_receipt)
        with patch.object(agent._http, "post", side_effect=mock_post):
            result = await agent.trade.swap(
                vault=FAKE_VAULT,
                token_in="USDC",
                token_out="WETH",
                amount_in=100_000_000,
            )
            assert isinstance(result, TradeResult)
            assert result.trade_type == TradeType.SWAP
            assert result.tx_hash == FAKE_TX_HASH

    async def test_swap_requires_wallet(self, readonly_agent):
        with pytest.raises(AuthenticationError, match="wallet_private_key"):
            await readonly_agent.trade.swap(
                vault=FAKE_VAULT,
                token_in="USDC",
                token_out="WETH",
                amount_in=100_000_000,
            )


class TestTradeOpenPosition:
    async def test_open_position_success(self, agent, mock_receipt):
        # open_position doesn't need eth_getBlockByNumber
        responses = [
            make_rpc_response("0x5"),           # nonce
            make_rpc_response("0x493e0"),        # gas estimate
            make_rpc_response("0x3b9aca00"),     # gas price
            make_rpc_response(FAKE_TX_HASH),     # sendRawTransaction
            make_rpc_response(mock_receipt),      # receipt
        ]
        call_count = 0

        async def mock_post(*args, **kwargs):
            nonlocal call_count
            resp = responses[min(call_count, len(responses) - 1)]
            call_count += 1
            return resp

        with patch.object(agent._http, "post", side_effect=mock_post):
            result = await agent.trade.open_position(
                vault=FAKE_VAULT,
                pair="BTC/USD",
                is_long=True,
                collateral=500_000_000,
                leverage=10,
            )
            assert result.trade_type == TradeType.PERP_OPEN

    async def test_open_position_invalid_pair(self, agent):
        with pytest.raises(InvalidPairError):
            await agent.trade.open_position(
                vault=FAKE_VAULT,
                pair="INVALID/PAIR",
                is_long=True,
                collateral=100,
                leverage=2,
            )


class TestTradeCloseTrade:
    async def test_close_trade_success(self, agent, mock_receipt):
        responses = [
            make_rpc_response("0x5"),
            make_rpc_response("0x493e0"),
            make_rpc_response("0x3b9aca00"),
            make_rpc_response(FAKE_TX_HASH),
            make_rpc_response(mock_receipt),
        ]
        call_count = 0

        async def mock_post(*args, **kwargs):
            nonlocal call_count
            resp = responses[min(call_count, len(responses) - 1)]
            call_count += 1
            return resp

        with patch.object(agent._http, "post", side_effect=mock_post):
            result = await agent.trade.close_trade(vault=FAKE_VAULT, trade_index=3)
            assert result.trade_type == TradeType.PERP_CLOSE


class TestTradeLend:
    async def test_lend_success(self, agent, mock_receipt):
        responses = [
            make_rpc_response("0x5"),
            make_rpc_response("0x493e0"),
            make_rpc_response("0x3b9aca00"),
            make_rpc_response(FAKE_TX_HASH),
            make_rpc_response(mock_receipt),
        ]
        call_count = 0

        async def mock_post(*args, **kwargs):
            nonlocal call_count
            resp = responses[min(call_count, len(responses) - 1)]
            call_count += 1
            return resp

        with patch.object(agent._http, "post", side_effect=mock_post):
            result = await agent.trade.lend(
                vault=FAKE_VAULT, asset="USDC", amount=1_000_000
            )
            assert result.trade_type == TradeType.LEND


class TestTradePartialClose:
    async def test_partial_close_success(self, agent, mock_receipt):
        responses = [
            make_rpc_response("0x5"),
            make_rpc_response("0x493e0"),
            make_rpc_response("0x3b9aca00"),
            make_rpc_response(FAKE_TX_HASH),
            make_rpc_response(mock_receipt),
        ]
        call_count = 0

        async def mock_post(*args, **kwargs):
            nonlocal call_count
            resp = responses[min(call_count, len(responses) - 1)]
            call_count += 1
            return resp

        with patch.object(agent._http, "post", side_effect=mock_post):
            result = await agent.trade.partial_close(
                vault=FAKE_VAULT, trade_index=1, percentage_bps=5000
            )
            assert result.trade_type == TradeType.PERP_CLOSE
            assert result.details["percentage_bps"] == 5000


class TestTradeUpdateTpSl:
    async def test_update_tp_sl_success(self, agent, mock_receipt):
        responses = [
            make_rpc_response("0x5"),
            make_rpc_response("0x493e0"),
            make_rpc_response("0x3b9aca00"),
            make_rpc_response(FAKE_TX_HASH),
            make_rpc_response(mock_receipt),
        ]
        call_count = 0

        async def mock_post(*args, **kwargs):
            nonlocal call_count
            resp = responses[min(call_count, len(responses) - 1)]
            call_count += 1
            return resp

        with patch.object(agent._http, "post", side_effect=mock_post):
            result = await agent.trade.update_tp_sl(
                vault=FAKE_VAULT, trade_index=2, take_profit=50000, stop_loss=40000
            )
            assert result.trade_type == TradeType.UPDATE_TP_SL
            assert result.details["take_profit"] == 50000
            assert result.details["stop_loss"] == 40000


class TestTradeUpdateMargin:
    async def test_update_margin_add(self, agent, mock_receipt):
        responses = [
            make_rpc_response("0x5"),
            make_rpc_response("0x493e0"),
            make_rpc_response("0x3b9aca00"),
            make_rpc_response(FAKE_TX_HASH),
            make_rpc_response(mock_receipt),
        ]
        call_count = 0

        async def mock_post(*args, **kwargs):
            nonlocal call_count
            resp = responses[min(call_count, len(responses) - 1)]
            call_count += 1
            return resp

        with patch.object(agent._http, "post", side_effect=mock_post):
            result = await agent.trade.update_margin(
                vault=FAKE_VAULT, trade_index=3, amount=100_000, is_add=True
            )
            assert result.trade_type == TradeType.UPDATE_MARGIN
            assert result.details["is_add"] is True
            assert result.details["amount"] == 100_000


class TestTradeWithdrawLending:
    async def test_withdraw_lending_success(self, agent, mock_receipt):
        responses = [
            make_rpc_response("0x5"),
            make_rpc_response("0x493e0"),
            make_rpc_response("0x3b9aca00"),
            make_rpc_response(FAKE_TX_HASH),
            make_rpc_response(mock_receipt),
        ]
        call_count = 0

        async def mock_post(*args, **kwargs):
            nonlocal call_count
            resp = responses[min(call_count, len(responses) - 1)]
            call_count += 1
            return resp

        with patch.object(agent._http, "post", side_effect=mock_post):
            result = await agent.trade.withdraw_lending(
                vault=FAKE_VAULT, asset="USDC", amount=1_000_000
            )
            assert result.trade_type == TradeType.WITHDRAW_LENDING
