from copy import deepcopy
from datetime import UTC, datetime
from unittest.mock import MagicMock, PropertyMock

import pytest

from freqtrade.exceptions import ConfigurationError
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re


@pytest.fixture
def markets_hip3():
    markets = {
        "BTC/USDC:USDC": {
            "quote": "USDC",
            "base": "BTC",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 50}},
            "info": {},
        },
        "ETH/USDC:USDC": {
            "quote": "USDC",
            "base": "ETH",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 50}},
            "info": {},
        },
        "SOL/USDC:USDC": {
            "quote": "USDC",
            "base": "SOL",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 20}},
            "info": {},
        },
        "DOGE/USDC:USDC": {
            "quote": "USDC",
            "base": "DOGE",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 20}},
            "info": {},
        },
        "XYZ-AAPL/USDC:USDC": {
            "quote": "USDC",
            "base": "XYZ-AAPL",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 10}},
            "info": {"hip3": True, "dex": "xyz"},
        },
        "XYZ-TSLA/USDC:USDC": {
            "quote": "USDC",
            "base": "XYZ-TSLA",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 10}},
            "info": {"hip3": True, "dex": "xyz"},
        },
        "XYZ-GOOGL/USDC:USDC": {
            "quote": "USDC",
            "base": "XYZ-GOOGL",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 10}},
            "info": {"hip3": True, "dex": "xyz"},
        },
        "XYZ-NVDA/USDC:USDC": {
            "quote": "USDC",
            "base": "XYZ-NVDA",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 10}},
            "info": {"hip3": True, "dex": "xyz"},
        },
        "VNTL-SPACEX/USDH:USDH": {
            "quote": "USDH",
            "base": "VNTL-SPACEX",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 3}},
            "info": {"hip3": True, "dex": "vntl"},
        },
        "VNTL-ANTHROPIC/USDH:USDH": {
            "quote": "USDH",
            "base": "VNTL-ANTHROPIC",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 3}},
            "info": {"hip3": True, "dex": "vntl"},
        },
        "FLX-TOKEN/USDC:USDC": {
            "quote": "USDC",
            "base": "FLX-TOKEN",
            "type": "swap",
            "swap": True,
            "linear": True,
            "limits": {"leverage": {"max": 3}},
            "info": {"hip3": True, "dex": "flx"},
        },
    }

    return markets


@pytest.mark.parametrize("margin_mode", ["isolated", "cross"])
def test_hyperliquid_dry_run_liquidation_price(default_conf, markets_hip3, mocker, margin_mode):
    # test if liq price calculated by dry_run_liquidation_price() is close to ccxt liq price
    # testing different pairs with large/small prices, different leverages, long, short

    positions = [
        {
            "symbol": "ETH/USDC:USDC",
            "entryPrice": 2458.5,
            "side": "long",
            "contracts": 0.015,
            "collateral": 36.864593,
            "leverage": 1.0,
            "liquidationPrice": 0.86915825,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 63287.0,
            "side": "long",
            "contracts": 0.00039,
            "collateral": 24.673292,
            "leverage": 1.0,
            "liquidationPrice": 22.37166537,
        },
        {
            "symbol": "SOL/USDC:USDC",
            "entryPrice": 146.82,
            "side": "long",
            "contracts": 0.16,
            "collateral": 23.482979,
            "leverage": 1.0,
            "liquidationPrice": 0.05269872,
        },
        {
            "symbol": "SOL/USDC:USDC",
            "entryPrice": 145.83,
            "side": "long",
            "contracts": 0.33,
            "collateral": 24.045107,
            "leverage": 2.0,
            "liquidationPrice": 74.83696193,
        },
        {
            "symbol": "ETH/USDC:USDC",
            "entryPrice": 2459.5,
            "side": "long",
            "contracts": 0.0199,
            "collateral": 24.454895,
            "leverage": 2.0,
            "liquidationPrice": 1243.0411908,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 62739.0,
            "side": "long",
            "contracts": 0.00077,
            "collateral": 24.137992,
            "leverage": 2.0,
            "liquidationPrice": 31708.03843631,
        },
        {
            "symbol": "DOGE/USDC:USDC",
            "entryPrice": 0.11586,
            "side": "long",
            "contracts": 437.0,
            "collateral": 25.29769,
            "leverage": 2.0,
            "liquidationPrice": 0.05945697,
        },
        {
            "symbol": "ETH/USDC:USDC",
            "entryPrice": 2642.8,
            "side": "short",
            "contracts": 0.019,
            "collateral": 25.091876,
            "leverage": 2.0,
            "liquidationPrice": 3924.18322043,
        },
        {
            "symbol": "SOL/USDC:USDC",
            "entryPrice": 155.89,
            "side": "short",
            "contracts": 0.32,
            "collateral": 24.924941,
            "leverage": 2.0,
            "liquidationPrice": 228.07847866,
        },
        {
            "symbol": "DOGE/USDC:USDC",
            "entryPrice": 0.14333,
            "side": "short",
            "contracts": 351.0,
            "collateral": 25.136807,
            "leverage": 2.0,
            "liquidationPrice": 0.20970228,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 68595.0,
            "side": "short",
            "contracts": 0.00069,
            "collateral": 23.64871,
            "leverage": 2.0,
            "liquidationPrice": 101849.99354283,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 65536.0,
            "side": "short",
            "contracts": 0.00099,
            "collateral": 21.604172,
            "leverage": 3.0,
            "liquidationPrice": 86493.46174617,
        },
        {
            "symbol": "SOL/USDC:USDC",
            "entryPrice": 173.06,
            "side": "long",
            "contracts": 0.6,
            "collateral": 20.735658,
            "leverage": 5.0,
            "liquidationPrice": 142.05186667,
        },
        {
            "symbol": "ETH/USDC:USDC",
            "entryPrice": 2545.5,
            "side": "long",
            "contracts": 0.0329,
            "collateral": 20.909894,
            "leverage": 4.0,
            "liquidationPrice": 1929.23322895,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 67400.0,
            "side": "short",
            "contracts": 0.00031,
            "collateral": 20.887308,
            "leverage": 1.0,
            "liquidationPrice": 133443.97317151,
        },
        {
            "symbol": "ETH/USDC:USDC",
            "entryPrice": 2552.0,
            "side": "short",
            "contracts": 0.0327,
            "collateral": 20.833393,
            "leverage": 4.0,
            "liquidationPrice": 3157.53150453,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 66930.0,
            "side": "long",
            "contracts": 0.0015,
            "collateral": 20.043862,
            "leverage": 5.0,
            "liquidationPrice": 54108.51043771,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 67033.0,
            "side": "long",
            "contracts": 0.00121,
            "collateral": 20.251817,
            "leverage": 4.0,
            "liquidationPrice": 50804.00091827,
        },
        {
            "symbol": "ETH/USDC:USDC",
            "entryPrice": 2521.9,
            "side": "long",
            "contracts": 0.0237,
            "collateral": 19.902091,
            "leverage": 3.0,
            "liquidationPrice": 1699.14071943,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 68139.0,
            "side": "short",
            "contracts": 0.00145,
            "collateral": 19.72573,
            "leverage": 5.0,
            "liquidationPrice": 80933.61590987,
        },
        {
            "symbol": "SOL/USDC:USDC",
            "entryPrice": 178.29,
            "side": "short",
            "contracts": 0.11,
            "collateral": 19.605036,
            "leverage": 1.0,
            "liquidationPrice": 347.82205322,
        },
        {
            "symbol": "SOL/USDC:USDC",
            "entryPrice": 176.23,
            "side": "long",
            "contracts": 0.33,
            "collateral": 19.364946,
            "leverage": 3.0,
            "liquidationPrice": 120.56240404,
        },
        {
            "symbol": "SOL/USDC:USDC",
            "entryPrice": 173.08,
            "side": "short",
            "contracts": 0.33,
            "collateral": 19.01881,
            "leverage": 3.0,
            "liquidationPrice": 225.08561715,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 68240.0,
            "side": "short",
            "contracts": 0.00105,
            "collateral": 17.887922,
            "leverage": 4.0,
            "liquidationPrice": 84431.79820839,
        },
        {
            "symbol": "ETH/USDC:USDC",
            "entryPrice": 2518.4,
            "side": "short",
            "contracts": 0.007,
            "collateral": 17.62263,
            "leverage": 1.0,
            "liquidationPrice": 4986.05799151,
        },
        {
            "symbol": "ETH/USDC:USDC",
            "entryPrice": 2533.2,
            "side": "long",
            "contracts": 0.0347,
            "collateral": 17.555195,
            "leverage": 5.0,
            "liquidationPrice": 2047.7642302,
        },
        {
            "symbol": "DOGE/USDC:USDC",
            "entryPrice": 0.13284,
            "side": "long",
            "contracts": 360.0,
            "collateral": 15.943218,
            "leverage": 3.0,
            "liquidationPrice": 0.09082388,
        },
        {
            "symbol": "SOL/USDC:USDC",
            "entryPrice": 163.11,
            "side": "short",
            "contracts": 0.48,
            "collateral": 15.650731,
            "leverage": 5.0,
            "liquidationPrice": 190.94213618,
        },
        {
            "symbol": "BTC/USDC:USDC",
            "entryPrice": 67141.0,
            "side": "long",
            "contracts": 0.00067,
            "collateral": 14.979079,
            "leverage": 3.0,
            "liquidationPrice": 45236.52992613,
        },
        {
            "symbol": "XYZ-AAPL/USDC:USDC",
            "entryPrice": 250.0,
            "side": "long",
            "contracts": 0.5,
            "collateral": 25.0,
            "leverage": 5.0,
            "liquidationPrice": 210.5263157894737,
        },
        {
            "symbol": "XYZ-GOOGL/USDC:USDC",
            "entryPrice": 190.0,
            "side": "short",
            "contracts": 0.5,
            "collateral": 9.5,
            "leverage": 10.0,
            "liquidationPrice": 199.04761904761904,
        },
        {
            "symbol": "XYZ-TSLA/USDC:USDC",
            "entryPrice": 350.0,
            "side": "long",
            "contracts": 1.0,
            "collateral": 50.0,
            "leverage": 7.0,
            "liquidationPrice": 315.7894736842105,
        },
    ]

    api_mock = MagicMock()
    default_conf["trading_mode"] = "futures"
    default_conf["margin_mode"] = margin_mode
    default_conf["stake_currency"] = "USDC"
    api_mock.load_markets = get_mock_coro()
    api_mock.markets = markets_hip3
    exchange = get_patched_exchange(
        mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=False
    )

    for position in positions:
        is_short = True if position["side"] == "short" else False
        liq_price_returned = position["liquidationPrice"]
        liq_price_calculated = exchange.dry_run_liquidation_price(
            position["symbol"],
            position["entryPrice"],
            is_short,
            position["contracts"],
            position["collateral"],
            position["leverage"],
            # isolated doesn't use wallet-balance
            wallet_balance=0.0 if margin_mode == "isolated" else position["collateral"],
            open_trades=[],
        )
        # Assume full position size is the wallet balance
        assert pytest.approx(liq_price_returned, rel=0.0001) == liq_price_calculated

        if margin_mode == "cross":
            # test with larger wallet balance
            liq_price_calculated_cross = exchange.dry_run_liquidation_price(
                position["symbol"],
                position["entryPrice"],
                is_short,
                position["contracts"],
                position["collateral"],
                position["leverage"],
                wallet_balance=position["collateral"] * 2,
                open_trades=[],
            )
            # Assume full position size is the wallet balance
            # This
            if position["side"] == "long":
                assert liq_price_returned > liq_price_calculated_cross < position["entryPrice"]
            else:
                assert liq_price_returned < liq_price_calculated_cross > position["entryPrice"]


def test_hyperliquid_get_funding_fees(default_conf, mocker):
    now = datetime.now(UTC)
    exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
    exchange._fetch_and_calculate_funding_fees = MagicMock()

    # Spot mode - no funding fees
    exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
    assert exchange._fetch_and_calculate_funding_fees.call_count == 0

    default_conf["trading_mode"] = "futures"
    default_conf["margin_mode"] = "isolated"
    default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl"]

    # Mock validate_config to skip validation
    mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")

    exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
    exchange._fetch_and_calculate_funding_fees = MagicMock()

    # Normal market
    exchange.get_funding_fees("BTC/USDC:USDC", 1, False, now)
    assert exchange._fetch_and_calculate_funding_fees.call_count == 1

    # HIP-3 XYZ market
    exchange._fetch_and_calculate_funding_fees.reset_mock()
    exchange.get_funding_fees("XYZ-TSLA/USDC:USDC", 1, False, now)
    assert exchange._fetch_and_calculate_funding_fees.call_count == 1

    # HIP-3 VNTL market
    exchange._fetch_and_calculate_funding_fees.reset_mock()
    exchange.get_funding_fees("VNTL-SPACEX/USDH:USDH", 1, True, now)
    assert exchange._fetch_and_calculate_funding_fees.call_count == 1


def test_hyperliquid_get_max_leverage(default_conf, mocker, markets_hip3):
    exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
    assert exchange.get_max_leverage("BTC/USDC:USDC", 1) == 1.0

    default_conf["trading_mode"] = "futures"
    default_conf["margin_mode"] = "isolated"
    default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl"]

    # Mock validate_config to skip validation
    mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")

    exchange = get_patched_exchange(mocker, default_conf, exchange="hyperliquid")
    mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets_hip3))

    # Normal markets
    assert exchange.get_max_leverage("BTC/USDC:USDC", 1) == 50
    assert exchange.get_max_leverage("ETH/USDC:USDC", 20) == 50
    assert exchange.get_max_leverage("SOL/USDC:USDC", 50) == 20
    assert exchange.get_max_leverage("DOGE/USDC:USDC", 3) == 20

    # HIP-3 markets
    assert exchange.get_max_leverage("XYZ-TSLA/USDC:USDC", 1) == 10
    assert exchange.get_max_leverage("XYZ-NVDA/USDC:USDC", 5) == 10
    assert exchange.get_max_leverage("VNTL-SPACEX/USDH:USDH", 2) == 3
    assert exchange.get_max_leverage("VNTL-ANTHROPIC/USDH:USDH", 1) == 3


def test_hyperliquid__lev_prep(default_conf, mocker):
    api_mock = MagicMock()
    api_mock.set_margin_mode = MagicMock()
    type(api_mock).has = PropertyMock(return_value={"setMarginMode": True})
    exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="hyperliquid")
    exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")

    assert api_mock.set_margin_mode.call_count == 0

    # test in futures mode
    api_mock.set_margin_mode.reset_mock()
    default_conf["dry_run"] = False

    default_conf["trading_mode"] = "futures"
    default_conf["margin_mode"] = "isolated"
    default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl"]

    # Mock validate_config to skip validation
    mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")

    exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange="hyperliquid")

    # Normal market
    exchange._lev_prep("BTC/USDC:USDC", 3.2, "buy")
    assert api_mock.set_margin_mode.call_count == 1
    api_mock.set_margin_mode.assert_called_with("isolated", "BTC/USDC:USDC", {"leverage": 3})

    api_mock.reset_mock()
    exchange._lev_prep("BTC/USDC:USDC", 19.99, "sell")
    assert api_mock.set_margin_mode.call_count == 1
    api_mock.set_margin_mode.assert_called_with("isolated", "BTC/USDC:USDC", {"leverage": 19})

    # HIP-3 XYZ market
    api_mock.reset_mock()
    exchange._lev_prep("XYZ-TSLA/USDC:USDC", 5.7, "buy")
    assert api_mock.set_margin_mode.call_count == 1
    api_mock.set_margin_mode.assert_called_with("isolated", "XYZ-TSLA/USDC:USDC", {"leverage": 5})

    api_mock.reset_mock()
    exchange._lev_prep("XYZ-TSLA/USDC:USDC", 10.0, "sell")
    assert api_mock.set_margin_mode.call_count == 1
    api_mock.set_margin_mode.assert_called_with("isolated", "XYZ-TSLA/USDC:USDC", {"leverage": 10})

    # HIP-3 VNTL market
    api_mock.reset_mock()
    exchange._lev_prep("VNTL-SPACEX/USDH:USDH", 2.5, "buy")
    assert api_mock.set_margin_mode.call_count == 1
    api_mock.set_margin_mode.assert_called_with(
        "isolated", "VNTL-SPACEX/USDH:USDH", {"leverage": 2}
    )

    api_mock.reset_mock()
    exchange._lev_prep("VNTL-ANTHROPIC/USDH:USDH", 3.0, "sell")
    assert api_mock.set_margin_mode.call_count == 1
    api_mock.set_margin_mode.assert_called_with(
        "isolated", "VNTL-ANTHROPIC/USDH:USDH", {"leverage": 3}
    )


def test_hyperliquid_fetch_order(default_conf_usdt, mocker, markets_hip3):
    default_conf_usdt["dry_run"] = False
    default_conf_usdt["trading_mode"] = "futures"
    default_conf_usdt["margin_mode"] = "isolated"
    default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz", "vntl"]

    api_mock = MagicMock()

    # Test with normal market
    api_mock.fetch_order = MagicMock(
        return_value={
            "id": "12345",
            "symbol": "ETH/USDC:USDC",
            "status": "closed",
            "filled": 0.1,
            "average": None,
            "timestamp": 1630000000,
        }
    )

    mocker.patch(f"{EXMS}.exchange_has", return_value=True)
    gtfo_mock = mocker.patch(
        f"{EXMS}.get_trades_for_order",
        return_value=[
            {
                "order_id": "12345",
                "price": 1000,
                "amount": 3,
                "filled": 3,
                "remaining": 0,
            },
            {
                "order_id": "12345",
                "price": 3000,
                "amount": 1,
                "filled": 1,
                "remaining": 0,
            },
        ],
    )
    mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
    exchange = get_patched_exchange(
        mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
    )
    o = exchange.fetch_order("12345", "ETH/USDC:USDC")
    # Uses weighted average
    assert o["average"] == 1500
    assert gtfo_mock.call_count == 1

    # Test with HIP-3 XYZ market
    api_mock.fetch_order = MagicMock(
        return_value={
            "id": "67890",
            "symbol": "XYZ-TSLA/USDC:USDC",
            "status": "closed",
            "filled": 2.5,
            "average": None,
            "timestamp": 1630000100,
        }
    )
    gtfo_mock.reset_mock()
    gtfo_mock.return_value = [
        {
            "order_id": "67890",
            "price": 250,
            "amount": 1.5,
            "filled": 1.5,
            "remaining": 0,
        },
        {
            "order_id": "67890",
            "price": 260,
            "amount": 1.0,
            "filled": 1.0,
            "remaining": 0,
        },
    ]

    o = exchange.fetch_order("67890", "XYZ-TSLA/USDC:USDC")
    # Weighted average: (250*1.5 + 260*1.0) / 2.5 = 254
    assert o["average"] == 254
    assert gtfo_mock.call_count == 1

    # Test with HIP-3 VNTL market
    api_mock.fetch_order = MagicMock(
        return_value={
            "id": "11111",
            "symbol": "VNTL-SPACEX/USDH:USDH",
            "status": "closed",
            "filled": 5.0,
            "average": None,
            "timestamp": 1630000200,
        }
    )
    gtfo_mock.reset_mock()
    gtfo_mock.return_value = [
        {
            "order_id": "11111",
            "price": 100,
            "amount": 3.0,
            "filled": 3.0,
            "remaining": 0,
        },
        {
            "order_id": "11111",
            "price": 105,
            "amount": 2.0,
            "filled": 2.0,
            "remaining": 0,
        },
    ]

    o = exchange.fetch_order("11111", "VNTL-SPACEX/USDH:USDH")
    assert o["average"] == 102
    assert gtfo_mock.call_count == 1


def test_hyperliquid_hip3_config_validation(default_conf_usdt, mocker, markets_hip3):
    """Test HIP-3 DEX configuration validation."""

    api_mock = MagicMock()
    default_conf_usdt["stake_currency"] = "USDC"

    # Futures mode, no dex configured
    default_conf_copy = deepcopy(default_conf_usdt)
    default_conf_copy["trading_mode"] = "futures"
    default_conf_copy["margin_mode"] = "isolated"
    exchange = get_patched_exchange(
        mocker, default_conf_copy, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
    )
    exchange.validate_config(default_conf_copy)

    # Not in futures mode - no dex configured - no error
    get_patched_exchange(
        mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
    )
    # Not in futures mode
    default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz"]
    with pytest.raises(
        ConfigurationError, match=r"HIP-3 DEXes are only supported in FUTURES trading mode\."
    ):
        get_patched_exchange(
            mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
        )
    # Valid single DEX
    default_conf_usdt["trading_mode"] = "futures"
    default_conf_usdt["margin_mode"] = "isolated"
    default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz"]
    exchange = get_patched_exchange(
        mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
    )
    assert exchange._get_configured_hip3_dexes() == ["xyz"]

    # Invalid DEX
    default_conf_usdt["exchange"]["hip3_dexes"] = ["invalid_dex"]
    with pytest.raises(ConfigurationError, match="Invalid HIP-3 DEXes configured"):
        exchange = get_patched_exchange(
            mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
        )
        exchange.validate_config(default_conf_usdt)

    # Mix of valid and invalid DEX
    default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz", "invalid_dex"]
    with pytest.raises(ConfigurationError, match="Invalid HIP-3 DEXes configured"):
        exchange = get_patched_exchange(
            mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
        )
        exchange.validate_config(default_conf_usdt)

    default_conf_usdt["margin_mode"] = "cross"
    with pytest.raises(ConfigurationError, match="HIP-3 DEXes require 'isolated' margin mode"):
        exchange = get_patched_exchange(
            mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
        )
        exchange.validate_config(default_conf_usdt)


def test_hyperliquid_get_balances_hip3(default_conf, mocker, caplog, markets_hip3):
    """Test balance fetching from HIP-3 DEXes."""
    api_mock = MagicMock()

    api_mock.load_markets = get_mock_coro()

    # Mock balance responses
    default_balance = {"USDC": {"free": 1000, "used": 0, "total": 1000}}
    xyz_balance = {"USDC": {"free": 0, "used": 600, "total": 600}}
    vntl_balance = {"USDH": {"free": 0, "used": 300, "total": 300}}

    def fetch_balance_side_effect(params=None):
        if params and params.get("dex") == "xyz":
            return xyz_balance
        elif params and params.get("dex") == "vntl":
            return vntl_balance
        elif params and params.get("dex") == "flx":
            raise Exception("FLX DEX error")
        return default_balance

    api_mock.fetch_balance = MagicMock(side_effect=fetch_balance_side_effect)

    # Test with two HIP-3 DEXes
    default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl", "flx"]
    default_conf["trading_mode"] = "futures"
    default_conf["margin_mode"] = "isolated"
    mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
    exchange = get_patched_exchange(
        mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
    )

    balances = exchange.get_balances()

    # Should have combined balances
    assert balances["USDC"]["free"] == 1000
    assert balances["USDC"]["used"] == 600
    assert balances["USDC"]["total"] == 1600
    assert balances["USDH"]["free"] == 0
    assert balances["USDH"]["used"] == 300
    assert balances["USDH"]["total"] == 300

    assert api_mock.fetch_balance.call_count == 4
    assert log_has_re("Could not fetch balance for HIP-3 DEX.*", caplog)


def test_hyperliquid_fetch_positions_hip3(default_conf, mocker, caplog, markets_hip3):
    """Test position fetching from HIP-3 DEXes."""
    api_mock = MagicMock()

    # Mock position responses
    default_positions = [{"symbol": "BTC/USDC:USDC", "contracts": 0.5}]
    xyz_positions = [{"symbol": "XYZ-AAPL/USDC:USDC", "contracts": 10}]
    vntl_positions = [{"symbol": "VNTL-SPACEX/USDH:USDH", "contracts": 5}]

    def fetch_positions_side_effect(symbols=None, params=None):
        if params and params.get("dex") == "xyz":
            return xyz_positions
        elif params and params.get("dex") == "vntl":
            return vntl_positions
        elif params and params.get("dex") == "flx":
            raise Exception("FLX DEX error")
        return default_positions

    positions_mock = MagicMock(side_effect=fetch_positions_side_effect)

    default_conf["trading_mode"] = "futures"
    default_conf["margin_mode"] = "isolated"
    default_conf["exchange"]["hip3_dexes"] = ["xyz", "vntl", "flx"]

    mocker.patch("freqtrade.exchange.hyperliquid.Hyperliquid.validate_config")
    exchange = get_patched_exchange(
        mocker, default_conf, api_mock, exchange="hyperliquid", mock_markets=markets_hip3
    )

    # Mock super().fetch_positions() to return default positions
    mocker.patch(f"{EXMS}.fetch_positions", positions_mock)

    positions = exchange.fetch_positions()

    assert log_has_re("Could not fetch positions from HIP-3 .*", caplog)

    # Should have all positions combined (default + HIP-3)
    assert len(positions) == 3
    assert any(p["symbol"] == "BTC/USDC:USDC" for p in positions)
    assert any(p["symbol"] == "XYZ-AAPL/USDC:USDC" for p in positions)
    assert any(p["symbol"] == "VNTL-SPACEX/USDH:USDH" for p in positions)

    # Verify API calls (xyz + vntl, default is mocked separately)
    assert positions_mock.call_count == 4


def test_hyperliquid_market_is_tradable(default_conf_usdt, mocker, markets_hip3):
    """Test market_is_tradable filters HIP-3 markets correctly."""
    default_conf_usdt["stake_currency"] = "USDC"
    default_conf_usdt["trading_mode"] = "futures"
    default_conf_usdt["margin_mode"] = "isolated"
    api_mock = MagicMock()
    api_mock.load_markets = get_mock_coro(return_value=markets_hip3)
    api_mock.markets = markets_hip3
    # Mock parent call - we only want to test hyperliquid specifics here.
    mocker.patch(f"{EXMS}.market_is_tradable", return_value=True)

    # Test 1: No HIP-3 DEXes configured - only default markets tradable
    default_conf_usdt["exchange"]["hip3_dexes"] = []
    exchange = get_patched_exchange(
        mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
    )

    assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is False
    assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is False
    assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is False
    assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is False

    # Test 2: Only 'xyz' configured - default + xyz markets tradable
    default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz"]
    exchange = get_patched_exchange(
        mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
    )

    assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is False
    assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is False

    # Test 3: 'xyz' and 'vntl' configured - default + xyz + vntl markets tradable
    default_conf_usdt["exchange"]["hip3_dexes"] = ["xyz", "flx"]
    exchange = get_patched_exchange(
        mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
    )

    assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is False
    assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is True

    # Use USDH stake currency to enable VNTL markets
    default_conf_usdt["exchange"]["hip3_dexes"] = ["vntl"]
    default_conf_usdt["stake_currency"] = "USDH"
    exchange = get_patched_exchange(
        mocker, default_conf_usdt, api_mock, exchange="hyperliquid", mock_markets=False
    )
    assert exchange.market_is_tradable(markets_hip3["BTC/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["ETH/USDC:USDC"]) is True
    assert exchange.market_is_tradable(markets_hip3["XYZ-AAPL/USDC:USDC"]) is False
    assert exchange.market_is_tradable(markets_hip3["XYZ-TSLA/USDC:USDC"]) is False
    assert exchange.market_is_tradable(markets_hip3["VNTL-SPACEX/USDH:USDH"]) is True
    assert exchange.market_is_tradable(markets_hip3["FLX-TOKEN/USDC:USDC"]) is False