from __future__ import annotations

import pytest

from hlbot.execution import Executor, is_valid_cloid, parse_usdc_balance, require_valid_cloid
from hlbot.cli import _classify_exit_source


class _FakeInfo:
    name_to_coin = {"SOL": "SOL", "BTC": "BTC", "LIT": "LIT"}
    coin_to_asset = {"SOL": 5, "BTC": 0, "LIT": 111}
    asset_to_sz_decimals = {5: 2, 0: 5, 111: 1}


class _FakeExchange:
    info = _FakeInfo()


def _executor_without_init() -> Executor:
    executor = object.__new__(Executor)
    executor.exchange = _FakeExchange()
    return executor


def test_usdc_balance_prefers_available_after_maintenance() -> None:
    state = {
        "balances": [{"coin": "USDC", "total": "134.88", "hold": "21.14"}],
        "tokenToAvailableAfterMaintenance": [["0", "113.74"]],
    }
    assert parse_usdc_balance(state) == {"total": 134.88, "hold": 21.14, "available": 113.74}


def test_usdc_balance_falls_back_when_token_map_malformed() -> None:
    state = {
        "balances": [{"coin": "USDC", "total": "50", "hold": "7.5"}],
        "tokenToAvailableAfterMaintenance": [["bad", "999"], ["1", "12"]],
    }
    assert parse_usdc_balance(state) == {"total": 50.0, "hold": 7.5, "available": 42.5}


def test_cloid_validation_accepts_only_128_bit_hex() -> None:
    good = "0x" + "a1" * 16
    assert is_valid_cloid(good)
    assert require_valid_cloid(good) == good

    for bad in ("", "abc", "0x1234", "0x" + "g" * 32, "0x" + "1" * 34):
        assert not is_valid_cloid(bad)
        with pytest.raises(ValueError):
            require_valid_cloid(bad)


def test_round_price_avoids_known_sol_tick_rejection_value() -> None:
    executor = _executor_without_init()
    assert executor._round_price("SOL", 85.464) == 85.46


def test_round_price_keeps_btc_integer_prices() -> None:
    executor = _executor_without_init()
    assert executor._round_price("BTC", 75544.5) == 75545.0


def test_round_price_keeps_small_alt_precision_conservative() -> None:
    executor = _executor_without_init()
    assert executor._round_price("LIT", 1.2584) == 1.258


def test_exit_source_classification_for_bot_levels() -> None:
    long_pos = {"side": "long", "entry": 85.775, "stop": 85.0, "target": 87.488}
    assert _classify_exit_source(long_pos, 87.49) == "bot_tp"
    assert _classify_exit_source(long_pos, 85.01) == "bot_sl"

    short_pos = {"side": "short", "entry": 58.66, "stop": 59.2, "target": 57.78}
    assert _classify_exit_source(short_pos, 57.78) == "bot_tp"
    assert _classify_exit_source(short_pos, 59.19) == "bot_sl"


def test_exit_source_classification_for_unknown_manual() -> None:
    pos = {"side": "long", "entry": 2.0, "stop": 1.8, "target": 2.4}
    assert _classify_exit_source(pos, 2.12) == "manual_or_unknown"


def test_protective_order_state_requires_one_stop_and_one_target() -> None:
    executor = _executor_without_init()
    executor.frontend_open_orders = lambda: [
        {"coin": "SOL", "reduceOnly": True, "orderType": "Stop Market", "cloid": "0x" + "1" * 32},
        {"coin": "SOL", "reduceOnly": True, "orderType": "Take Profit Market", "cloid": "0x" + "2" * 32},
        {"coin": "BTC", "reduceOnly": True, "orderType": "Stop Market", "cloid": "0x" + "3" * 32},
    ]
    state = executor.protective_order_state("SOL")
    assert state["ok"] is True
    assert state["stop_count"] == 1
    assert state["target_count"] == 1


def test_protective_order_state_rejects_duplicate_stops() -> None:
    executor = _executor_without_init()
    executor.frontend_open_orders = lambda: [
        {"coin": "SOL", "reduceOnly": True, "orderType": "Stop Market", "cloid": "0x" + "1" * 32},
        {"coin": "SOL", "reduceOnly": True, "orderType": "Stop Market", "cloid": "0x" + "2" * 32},
        {"coin": "SOL", "reduceOnly": True, "orderType": "Take Profit Market", "cloid": "0x" + "3" * 32},
    ]
    state = executor.protective_order_state("SOL")
    assert state["ok"] is False
    assert state["stop_count"] == 2
