# app/tests/unit/market_data/acquisition/adapters/dzengi/test_mapper.py

from __future__ import annotations

from dataclasses import FrozenInstanceError, replace
from decimal import Decimal

import pytest

from src.market_data.acquisition.adapters.dzengi.mapper import (
    map_dzengi_exchange_info_to_instruments,
    map_dzengi_symbol_to_instrument,
)
from src.market_data.acquisition.adapters.dzengi.models import (
    DzengiExchangeInfoPayload,
    DzengiExchangeInfoResponse,
    DzengiExchangeInfoSymbol,
    DzengiLotSizeFilter,
    DzengiMinNotionalFilter,
    DzengiUnknownFilter,
)
from src.market_data.acquisition.exceptions import (
    InstrumentReferenceMappingError,
)


def _complete_symbol() -> DzengiExchangeInfoSymbol:
    return DzengiExchangeInfoSymbol(
        symbol="ETH/EUR_LEVERAGE",
        name="ETH/EUR",
        status="TRADING",
        asset_type="CRYPTOCURRENCY",
        base_asset="ETH",
        base_asset_precision=3,
        quote_asset="EUR",
        quote_asset_id="EUR_LEVERAGE",
        quote_precision=3,
        order_types=("LIMIT", "MARKET", "STOP"),
        filters=(
            DzengiLotSizeFilter(
                filter_type="LOT_SIZE",
                min_qty="0.001",
                max_qty="1000",
                step_size="0.001",
            ),
            DzengiMinNotionalFilter(
                filter_type="MIN_NOTIONAL",
                min_notional="2",
            ),
        ),
        market_modes=("REGULAR",),
        market_type="LEVERAGE",
        country="",
        sector="",
        industry="",
        trading_hours="UTC; Mon - 21:00, 21:05 -",
        tick_size=0.01,
        tick_value=18.3415,
        trading_fee=0.06,
        exchange_fee=None,
        long_rate=-0.01,
        short_rate=0.01,
        swap_charge_interval=480,
        min_sl_gap=0,
        max_sl_gap=50.0,
        min_tp_gap=0,
        max_tp_gap=50.0,
    )


def _response(
    *symbols: DzengiExchangeInfoSymbol,
) -> DzengiExchangeInfoResponse:
    return DzengiExchangeInfoResponse(
        payload=DzengiExchangeInfoPayload(
            timezone="UTC",
            server_time=1783537921471,
            rate_limits=(),
            exchange_filters=(),
            symbols=tuple(symbols),
        ),
    )


def test_map_complete_dzengi_symbol_to_instrument() -> None:
    instrument = map_dzengi_symbol_to_instrument(
        _complete_symbol()
    )

    assert instrument.symbol == "ETH/EUR_LEVERAGE"
    assert instrument.name == "ETH/EUR"
    assert instrument.status == "TRADING"

    assert instrument.base_asset == "ETH"
    assert instrument.quote_asset == "EUR"
    assert instrument.asset_type == "CRYPTOCURRENCY"

    assert instrument.market_type == "LEVERAGE"
    assert instrument.market_modes == ("REGULAR",)
    assert instrument.order_types == ("LIMIT", "MARKET", "STOP")

    assert instrument.base_asset_precision == 3
    assert instrument.quote_asset_precision == 3

    assert instrument.tick_size == Decimal("0.01")
    assert instrument.tick_value == Decimal("18.3415")

    assert instrument.step_size == Decimal("0.001")
    assert instrument.min_qty == Decimal("0.001")
    assert instrument.max_qty == Decimal("1000")
    assert instrument.min_notional == Decimal("2")

    assert instrument.country is None
    assert instrument.sector is None
    assert instrument.industry is None
    assert instrument.trading_hours == "UTC; Mon - 21:00, 21:05 -"


def test_map_exchange_info_to_instruments() -> None:
    first = _complete_symbol()
    second = replace(
        _complete_symbol(),
        symbol="BTC/USD_LEVERAGE",
        name="BTC/USD",
        base_asset="BTC",
        quote_asset="USD",
    )

    instruments = map_dzengi_exchange_info_to_instruments(
        _response(first, second)
    )

    assert isinstance(instruments, tuple)
    assert len(instruments) == 2
    assert instruments[0].symbol == "ETH/EUR_LEVERAGE"
    assert instruments[1].symbol == "BTC/USD_LEVERAGE"


def test_map_numeric_values_to_decimal_exactly() -> None:
    symbol = replace(
        _complete_symbol(),
        tick_size=0.00000001,
        tick_value=0,
        filters=(
            DzengiLotSizeFilter(
                filter_type="LOT_SIZE",
                min_qty="0.00000001",
                max_qty=10000000,
                step_size="0.00000001",
            ),
            DzengiMinNotionalFilter(
                filter_type="MIN_NOTIONAL",
                min_notional="0.00000069",
            ),
        ),
    )

    instrument = map_dzengi_symbol_to_instrument(symbol)

    assert instrument.tick_size == Decimal("1E-8")
    assert instrument.tick_value == Decimal("0")
    assert instrument.min_qty == Decimal("1E-8")
    assert instrument.max_qty == Decimal("10000000")
    assert instrument.step_size == Decimal("1E-8")
    assert instrument.min_notional == Decimal("6.9E-7")


def test_map_symbol_without_filters() -> None:
    symbol = replace(
        _complete_symbol(),
        filters=(),
    )

    instrument = map_dzengi_symbol_to_instrument(symbol)

    assert instrument.step_size is None
    assert instrument.min_qty is None
    assert instrument.max_qty is None
    assert instrument.min_notional is None


def test_map_symbol_with_missing_optional_numeric_values() -> None:
    symbol = replace(
        _complete_symbol(),
        tick_size=None,
        tick_value=None,
        filters=(
            DzengiLotSizeFilter(
                filter_type="LOT_SIZE",
                min_qty=None,
                max_qty=None,
                step_size=None,
            ),
            DzengiMinNotionalFilter(
                filter_type="MIN_NOTIONAL",
                min_notional=None,
            ),
        ),
    )

    instrument = map_dzengi_symbol_to_instrument(symbol)

    assert instrument.tick_size is None
    assert instrument.tick_value is None
    assert instrument.step_size is None
    assert instrument.min_qty is None
    assert instrument.max_qty is None
    assert instrument.min_notional is None


def test_mapper_ignores_unknown_filters() -> None:
    symbol = replace(
        _complete_symbol(),
        filters=(
            DzengiUnknownFilter(
                filter_type="FUTURE_FILTER",
                fields=(
                    ("enabled", True),
                    ("limit", 10),
                ),
            ),
            DzengiLotSizeFilter(
                filter_type="LOT_SIZE",
                min_qty="0.001",
                max_qty="1000",
                step_size="0.001",
            ),
        ),
    )

    instrument = map_dzengi_symbol_to_instrument(symbol)

    assert instrument.min_qty == Decimal("0.001")
    assert instrument.max_qty == Decimal("1000")
    assert instrument.step_size == Decimal("0.001")
    assert instrument.min_notional is None


@pytest.mark.parametrize(
    ("field", "value"),
    [
        ("asset_type", ""),
        ("asset_type", "   "),
        ("country", ""),
        ("country", "   "),
        ("sector", ""),
        ("industry", " "),
        ("trading_hours", ""),
    ],
)
def test_mapper_converts_empty_optional_text_to_none(
    field: str,
    value: str,
) -> None:
    symbol = replace(
        _complete_symbol(),
        **{field: value},
    )

    instrument = map_dzengi_symbol_to_instrument(symbol)

    assert getattr(instrument, field) is None


def test_mapper_strips_non_empty_optional_text() -> None:
    symbol = replace(
        _complete_symbol(),
        asset_type=" CRYPTOCURRENCY ",
        country=" DE ",
        sector=" Technology ",
        industry=" Software ",
        trading_hours=" UTC; Mon 07:00 - 15:30 ",
    )

    instrument = map_dzengi_symbol_to_instrument(symbol)

    assert instrument.asset_type == "CRYPTOCURRENCY"
    assert instrument.country == "DE"
    assert instrument.sector == "Technology"
    assert instrument.industry == "Software"
    assert instrument.trading_hours == "UTC; Mon 07:00 - 15:30"


def test_mapper_preserves_market_modes_order() -> None:
    symbol = replace(
        _complete_symbol(),
        market_modes=("REGULAR", "CLOSE_ONLY", "EXTENDED"),
    )

    instrument = map_dzengi_symbol_to_instrument(symbol)

    assert instrument.market_modes == (
        "REGULAR",
        "CLOSE_ONLY",
        "EXTENDED",
    )


def test_mapper_preserves_order_types_order() -> None:
    symbol = replace(
        _complete_symbol(),
        order_types=("MARKET", "LIMIT", "STOP"),
    )

    instrument = map_dzengi_symbol_to_instrument(symbol)

    assert instrument.order_types == (
        "MARKET",
        "LIMIT",
        "STOP",
    )


def test_mapper_rejects_duplicate_lot_size_filters() -> None:
    lot_size = DzengiLotSizeFilter(
        filter_type="LOT_SIZE",
        min_qty="0.001",
        max_qty="1000",
        step_size="0.001",
    )

    symbol = replace(
        _complete_symbol(),
        filters=(
            lot_size,
            lot_size,
        ),
    )

    with pytest.raises(
        InstrumentReferenceMappingError,
        match=r"несколько фильтров LOT_SIZE",
    ):
        map_dzengi_symbol_to_instrument(symbol)


def test_mapper_rejects_duplicate_min_notional_filters() -> None:
    min_notional = DzengiMinNotionalFilter(
        filter_type="MIN_NOTIONAL",
        min_notional="2",
    )

    symbol = replace(
        _complete_symbol(),
        filters=(
            min_notional,
            min_notional,
        ),
    )

    with pytest.raises(
        InstrumentReferenceMappingError,
        match=r"несколько фильтров MIN_NOTIONAL",
    ):
        map_dzengi_symbol_to_instrument(symbol)


@pytest.mark.parametrize(
    ("field_name", "invalid_value"),
    [
        ("tick_size", float("nan")),
        ("tick_value", float("inf")),
    ],
)
def test_mapper_rejects_non_finite_direct_numeric_value(
    field_name: str,
    invalid_value: float,
) -> None:
    symbol = replace(
        _complete_symbol(),
        **{field_name: invalid_value},
    )

    with pytest.raises(
        InstrumentReferenceMappingError,
        match=r"должно быть конечным числом",
    ):
        map_dzengi_symbol_to_instrument(symbol)


def test_mapper_rejects_invalid_filter_numeric_value() -> None:
    symbol = replace(
        _complete_symbol(),
        filters=(
            DzengiLotSizeFilter(
                filter_type="LOT_SIZE",
                min_qty="not-a-number",
                max_qty="1000",
                step_size="0.001",
            ),
        ),
    )

    with pytest.raises(
        InstrumentReferenceMappingError,
        match=r"minQty.*невозможно преобразовать в Decimal",
    ):
        map_dzengi_symbol_to_instrument(symbol)


def test_mapped_instrument_is_immutable() -> None:
    instrument = map_dzengi_symbol_to_instrument(
        _complete_symbol()
    )

    with pytest.raises(FrozenInstanceError):
        instrument.status = "BREAK"  # type: ignore[misc]