# app/tests/unit/telegram/ui/test_currency_ui.py

from __future__ import annotations

from datetime import datetime, timezone
from decimal import Decimal

from decimal import Decimal
from types import SimpleNamespace
from typing import cast

import pytest

from src.integrations.exchange.exceptions import ExchangeError
from src.integrations.exchange.models import (
    BalanceSummary,
)
from src.integrations.exchange.service import ExchangeService
from src.market_data.acquisition.models.instrument import Instrument
from src.market_data.acquisition.models.quote import Quote
from src.telegram.ui.currency_ui import (
    _resolve_asset_quote_instrument,
    estimate_balance_usd,
    get_asset_usd_rate,
)


def _instrument(
    *,
    symbol: str,
    base_asset: str,
    quote_asset: str = "USD",
    status: str = "TRADING",
    market_type: str = "SPOT",
) -> Instrument:
    return Instrument(
        symbol=symbol,
        name=symbol,
        status=status,
        base_asset=base_asset,
        quote_asset=quote_asset,
        asset_type="CRYPTOCURRENCY",
        market_type=market_type,
        market_modes=("REGULAR",),
        order_types=("LIMIT", "MARKET"),
        base_asset_precision=8,
        quote_asset_precision=8,
        tick_size=Decimal("0.01"),
        tick_value=None,
        step_size=Decimal("0.0001"),
        min_qty=Decimal("0.0001"),
        max_qty=None,
        min_notional=Decimal("1"),
        country=None,
        sector=None,
        industry=None,
        trading_hours=None,
    )


def _service() -> ExchangeService:
    return object.__new__(
        ExchangeService
    )


def test_resolver_uses_get_instruments(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    instruments = (
        _instrument(
            symbol="BTC/USD",
            base_asset="BTC",
        ),
    )
    call_count = 0

    def get_instruments() -> tuple[Instrument, ...]:
        nonlocal call_count

        call_count += 1
        return instruments

    monkeypatch.setattr(
        service,
        "get_instruments",
        get_instruments,
    )

    result = _resolve_asset_quote_instrument(
        service,
        "BTC",
    )

    assert result is instruments[0]
    assert call_count == 1


def test_resolver_uses_canonical_instrument_reference(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    instrument = _instrument(
        symbol="BTC/USD",
        base_asset="BTC",
    )

    calls = 0

    def get_instruments() -> tuple[Instrument, ...]:
        nonlocal calls

        calls += 1
        return (
            instrument,
        )

    monkeypatch.setattr(
        service,
        "get_instruments",
        get_instruments,
    )

    result = _resolve_asset_quote_instrument(
        service,
        "BTC",
    )

    assert result is instrument
    assert calls == 1


def test_resolver_prefers_usd_over_usdt(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    usd = _instrument(
        symbol="BTC/USD",
        base_asset="BTC",
        quote_asset="USD",
        market_type="LEVERAGE",
    )
    usdt = _instrument(
        symbol="BTC/USDT",
        base_asset="BTC",
        quote_asset="USDT",
        market_type="SPOT",
    )

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (
            usdt,
            usd,
        ),
    )

    result = _resolve_asset_quote_instrument(
        service,
        "BTC",
    )

    assert result is usd


def test_resolver_prefers_trading_status(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    active = _instrument(
        symbol="BTC/USD_ACTIVE",
        base_asset="BTC",
        status="TRADING",
        market_type="LEVERAGE",
    )
    inactive = _instrument(
        symbol="BTC/USD_INACTIVE",
        base_asset="BTC",
        status="UNKNOWN",
        market_type="SPOT",
    )

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (
            inactive,
            active,
        ),
    )

    result = _resolve_asset_quote_instrument(
        service,
        "BTC",
    )

    assert result is active


def test_resolver_prefers_spot_when_other_priority_is_equal(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    spot = _instrument(
        symbol="BTC/USD_SPOT",
        base_asset="BTC",
        market_type="SPOT",
    )
    leverage = _instrument(
        symbol="BTC/USD_LEVERAGE",
        base_asset="BTC",
        market_type="LEVERAGE",
    )

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (
            leverage,
            spot,
        ),
    )

    result = _resolve_asset_quote_instrument(
        service,
        "BTC",
    )

    assert result is spot


def test_resolver_ignores_other_base_assets(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (
            _instrument(
                symbol="ETH/USD",
                base_asset="ETH",
            ),
        ),
    )

    result = _resolve_asset_quote_instrument(
        service,
        "BTC",
    )

    assert result is None


def test_resolver_ignores_unsupported_quote_assets(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (
            _instrument(
                symbol="BTC/EUR",
                base_asset="BTC",
                quote_asset="EUR",
            ),
        ),
    )

    result = _resolve_asset_quote_instrument(
        service,
        "BTC",
    )

    assert result is None


def test_resolver_returns_none_when_get_instruments_fails(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    def raise_error() -> tuple[Instrument, ...]:
        raise ExchangeError(
            "exchangeInfo unavailable"
        )

    monkeypatch.setattr(
        service,
        "get_instruments",
        raise_error,
    )

    result = _resolve_asset_quote_instrument(
        service,
        "BTC",
    )

    assert result is None


@pytest.mark.parametrize(
    "currency",
    [
        "USD",
        "usd",
        "USDT",
        "usdt",
    ],
)
def test_usd_and_usdt_rates_are_one(
    currency: str,
) -> None:
    service = _service()
    cache: dict[str, float | None] = {}

    result = get_asset_usd_rate(
        service,
        currency,
        cache,
    )

    assert result == 1.0
    assert cache == {}


def test_get_asset_usd_rate_uses_existing_price_cache(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    cache: dict[str, float | None] = {
        "BTC": 50_000.0,
    }

    def fail_if_called() -> tuple[Instrument, ...]:
        raise AssertionError(
            "Instrument lookup must not run on price cache hit."
        )

    monkeypatch.setattr(
        service,
        "get_instruments",
        fail_if_called,
    )

    result = get_asset_usd_rate(
        service,
        "BTC",
        cache,
    )

    assert result == 50_000.0


def test_get_asset_usd_rate_loads_price_by_instrument_symbol(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    instrument = _instrument(
        symbol="BTC/USD",
        base_asset="BTC",
    )
    requested_symbols: list[str] = []

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (
            instrument,
        ),
    )

    def get_quote(
        symbol: str,
        *,
        runtime_key: str | None = None,
    ) -> Quote:
        del runtime_key

        requested_symbols.append(symbol)

        return Quote(
            symbol=symbol,
            last_price=Decimal("50000"),
            bid_price=Decimal("49999"),
            ask_price=Decimal("50001"),
            exchange_timestamp=None,
            received_at=datetime.now(timezone.utc),
            source="test",
        )

    monkeypatch.setattr(
        service,
        "get_quote",
        get_quote,
    )

    cache: dict[str, float | None] = {}

    result = get_asset_usd_rate(
        service,
        "BTC",
        cache,
    )

    assert result == 50_000.0
    assert requested_symbols == [
        "BTC/USD",
    ]
    assert cache == {
        "BTC": 50_000.0,
    }


def test_get_asset_usd_rate_caches_missing_instrument(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (),
    )

    cache: dict[str, float | None] = {}

    result = get_asset_usd_rate(
        service,
        "BTC",
        cache,
    )

    assert result is None
    assert cache == {
        "BTC": None,
    }


def test_get_asset_usd_rate_caches_price_error(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (
            _instrument(
                symbol="BTC/USD",
                base_asset="BTC",
            ),
        ),
    )

    def raise_error(
        symbol: str,
        *,
        runtime_key: str | None = None,
    ) -> Quote:
        del symbol
        del runtime_key

        raise ExchangeError(
            "ticker unavailable"
        )

    monkeypatch.setattr(
        service,
        "get_quote",
        raise_error,
    )

    cache: dict[str, float | None] = {}

    result = get_asset_usd_rate(
        service,
        "BTC",
        cache,
    )

    assert result is None
    assert cache == {
        "BTC": None,
    }


def test_estimate_balance_usd_for_fiat_balance() -> None:
    service = _service()

    balance = BalanceSummary(
        currency="USD",
        available=100.0,
        locked=25.0,
        source="test",
    )

    result = estimate_balance_usd(
        balance,
        service,
        {},
    )

    assert result == 125.0


def test_estimate_balance_usd_for_crypto_balance(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (
            _instrument(
                symbol="BTC/USD",
                base_asset="BTC",
            ),
        ),
    )

    monkeypatch.setattr(
        service,
        "get_quote",
        lambda symbol, **_: Quote(
            symbol=symbol,
            last_price=Decimal("50000"),
            bid_price=Decimal("49999"),
            ask_price=Decimal("50001"),
            exchange_timestamp=None,
            received_at=datetime.now(timezone.utc),
            source="test",
        ),
    )

    balance = BalanceSummary(
        currency="BTC",
        available=0.01,
        locked=0.005,
        source="test",
    )

    result = estimate_balance_usd(
        balance,
        service,
        {},
    )

    assert result == pytest.approx(
        750.0
    )


def test_estimate_balance_usd_returns_none_for_zero_total() -> None:
    service = _service()

    balance = BalanceSummary(
        currency="BTC",
        available=0.0,
        locked=0.0,
        source="test",
    )

    result = estimate_balance_usd(
        balance,
        service,
        {},
    )

    assert result is None


def test_estimate_balance_usd_returns_none_when_rate_is_missing(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()

    monkeypatch.setattr(
        service,
        "get_instruments",
        lambda: (),
    )

    balance = BalanceSummary(
        currency="BTC",
        available=1.0,
        locked=0.0,
        source="test",
    )

    result = estimate_balance_usd(
        balance,
        service,
        {},
    )

    assert result is None