# app/tests/unit/integrations/exchange/test_service_klines.py

from __future__ import annotations

from datetime import datetime, timezone
from decimal import Decimal
from types import SimpleNamespace
from typing import Any, cast

import pytest

import src.integrations.exchange.service as service_module
from src.integrations.exchange.exceptions import ExchangeError
from src.integrations.exchange.models import SymbolValidationResult
from src.integrations.exchange.service import ExchangeService
from src.market_data.acquisition.models.candle import Candle


def _candle(
    *,
    symbol: str = "BTC/USD_LEVERAGE",
    interval: str = "1m",
    open_time_ms: int = 1_750_000_000_000,
    open_price: str = "100.10",
    high_price: str = "110.20",
    low_price: str = "90.30",
    close_price: str = "105.40",
    volume: str = "12.50",
    source: str = "rest_klines:bid",
) -> Candle:
    return Candle(
        symbol=symbol,
        interval=interval,
        open_time=datetime.fromtimestamp(
            open_time_ms / 1000,
            tz=timezone.utc,
        ),
        open_price=Decimal(open_price),
        high_price=Decimal(high_price),
        low_price=Decimal(low_price),
        close_price=Decimal(close_price),
        volume=Decimal(volume),
        source=source,
    )



def _set_test_attribute(
    target: object,
    name: str,
    value: object,
) -> None:
    setattr(cast(Any, target), name, value)


def _service(
    *,
    default_symbol: str = "BTC/USD_LEVERAGE",
    exchange_enabled: bool = True,
) -> ExchangeService:
    service = ExchangeService.__new__(ExchangeService)
    _set_test_attribute(
        service,
        "settings",
        SimpleNamespace(
            default_symbol=default_symbol,
            exchange_enabled=exchange_enabled,
        ),
    )
    return service


def _valid_symbol(
    symbol: str = "BTC/USD_LEVERAGE",
) -> SymbolValidationResult:
    return SymbolValidationResult(
        requested_symbol=symbol,
        normalized_symbol=symbol,
        is_valid=True,
        message="OK",
        symbol_info=None,
    )


def _invalid_symbol(
    symbol: str = "UNKNOWN",
) -> SymbolValidationResult:
    return SymbolValidationResult(
        requested_symbol=symbol,
        normalized_symbol=symbol,
        is_valid=False,
        message="Invalid symbol.",
        symbol_info=None,
    )


def test_get_klines_uses_default_symbol() -> None:
    service = _service(default_symbol="ETH/USD_LEVERAGE")
    requested_symbols: list[str] = []
    acquisition_calls: list[dict[str, object]] = []

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: (
            requested_symbols.append(symbol) or _valid_symbol(symbol)
        ),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: acquisition_calls.append(kwargs) or (),
    )

    result = service.get_klines()

    assert requested_symbols == ["ETH/USD_LEVERAGE"]
    assert acquisition_calls[0]["symbol"] == "ETH/USD_LEVERAGE"
    assert result.symbol == "ETH/USD_LEVERAGE"


@pytest.mark.parametrize("limit", [0, -1, -100])
def test_get_klines_normalizes_non_positive_limit(limit: int) -> None:
    service = _service()
    captured: list[dict[str, object]] = []

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol(symbol),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: captured.append(kwargs) or (),
    )

    service.get_klines(limit=limit)

    assert captured[0]["limit"] == 200


def test_get_klines_caps_limit_at_200() -> None:
    service = _service()
    captured: list[dict[str, object]] = []

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol(symbol),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: captured.append(kwargs) or (),
    )

    service.get_klines(limit=500)

    assert captured[0]["limit"] == 200


@pytest.mark.parametrize("interval", ["1m", "5m", "15m", "1h"])
def test_get_klines_accepts_supported_intervals(interval: str) -> None:
    service = _service()
    captured: list[dict[str, object]] = []

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol(symbol),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: captured.append(kwargs) or (),
    )

    result = service.get_klines(interval=interval)

    assert captured[0]["interval"] == interval
    assert result.interval == interval


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

    with pytest.raises(
        ExchangeError,
        match="Unsupported kline interval",
    ):
        service.get_klines(interval="4h")


@pytest.mark.parametrize(
    ("price_type", "expected"),
    [
        ("bid", "bid"),
        ("ask", "ask"),
        (" BID ", "bid"),
        (" AsK ", "ask"),
        ("unknown", "bid"),
        ("", "bid"),
    ],
)
def test_get_klines_normalizes_price_type(
    price_type: str,
    expected: str,
) -> None:
    service = _service()
    captured: list[dict[str, object]] = []

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol(symbol),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: captured.append(kwargs) or (),
    )

    result = service.get_klines(price_type=price_type)

    assert captured[0]["price_type"] == expected
    assert result.source == f"rest_klines:{expected}"


def test_get_klines_rejects_mock_mode() -> None:
    service = _service(exchange_enabled=False)

    with pytest.raises(
        ExchangeError,
        match="Klines are not available in mock exchange mode",
    ):
        service.get_klines()


def test_get_klines_rejects_invalid_symbol() -> None:
    service = _service()
    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _invalid_symbol(symbol),
    )

    with pytest.raises(ExchangeError, match="Invalid symbol"):
        service.get_klines("UNKNOWN")


def test_get_klines_calls_acquisition_with_exact_arguments() -> None:
    service = _service()
    captured: list[dict[str, object]] = []

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol("BTC/USD_LEVERAGE"),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: captured.append(kwargs) or (),
    )

    service.get_klines(
        " btc/usd ",
        interval="5m",
        limit=123,
        price_type="ask",
    )

    assert captured == [
        {
            "symbol": "BTC/USD_LEVERAGE",
            "interval": "5m",
            "limit": 123,
            "price_type": "ask",
        }
    ]


def test_get_klines_maps_canonical_candle_to_legacy_kline() -> None:
    service = _service()
    candle = _candle()

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol(symbol),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: (candle,),
    )

    result = service.get_klines()

    assert len(result.candles) == 1
    kline = result.candles[0]
    assert kline.symbol == candle.symbol
    assert kline.interval == candle.interval
    assert kline.open_time == 1_750_000_000_000
    assert kline.open_price == 100.10
    assert kline.high_price == 110.20
    assert kline.low_price == 90.30
    assert kline.close_price == 105.40
    assert kline.volume == 12.50
    assert kline.source == candle.source


def test_get_klines_preserves_candle_order() -> None:
    service = _service()
    first = _candle(open_time_ms=1000)
    second = _candle(open_time_ms=2000)

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol(symbol),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: (first, second),
    )

    result = service.get_klines()

    assert [item.open_time for item in result.candles] == [1000, 2000]


def test_get_klines_trims_result_to_limit() -> None:
    service = _service()
    candles = tuple(
        _candle(open_time_ms=index * 1000)
        for index in range(1, 6)
    )

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol(symbol),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: candles,
    )

    result = service.get_klines(limit=2)

    assert [item.open_time for item in result.candles] == [4000, 5000]


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

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol(symbol),
    )
    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        lambda **kwargs: (),
    )

    result = service.get_klines()

    assert result.candles == []
    assert result.source == "rest_klines:bid"


def test_get_klines_logs_and_wraps_acquisition_error() -> None:
    service = _service()
    original_error = RuntimeError("candles unavailable")
    log_calls: list[dict[str, object]] = []

    _set_test_attribute(
        service,
        "validate_symbol",
        lambda symbol: _valid_symbol(symbol),
    )

    def raise_error(**kwargs: object) -> tuple[Candle, ...]:
        raise original_error

    _set_test_attribute(
        service,
        "_load_candles_via_acquisition",
        raise_error,
    )
    _set_test_attribute(
        service,
        "_log_exchange_error",
        lambda **kwargs: log_calls.append(kwargs),
    )

    with pytest.raises(
        ExchangeError,
        match="candles unavailable",
    ) as error_info:
        service.get_klines(
            interval="15m",
            limit=25,
            price_type="ask",
        )

    assert error_info.value.__cause__ is original_error
    assert log_calls == [
        {
            "endpoint": "klines",
            "exc": original_error,
            "symbol": "BTC/USD_LEVERAGE",
            "extra_payload": {
                "interval": "15m",
                "limit": 25,
                "price_type": "ask",
            },
        }
    ]


def test_kline_from_candle_returns_new_legacy_model() -> None:
    service = _service()
    candle = _candle()

    result = service._kline_from_candle(candle)

    assert result is not candle
    assert result.symbol == candle.symbol
    assert result.open_time == 1_750_000_000_000


def test_load_candles_via_acquisition_builds_complete_pipeline(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    events: list[tuple[str, object]] = []
    expected = (_candle(),)

    class FakeSource:
        def __init__(self) -> None:
            events.append(("source", self))

    class FakeHandler:
        def __init__(self) -> None:
            events.append(("handler", self))

    class FakeFeed:
        def __init__(self, *, source: object, handler: object) -> None:
            self.source = source
            self.handler = handler
            events.append(("feed", self))

    class FakeRegistry:
        def __init__(self) -> None:
            self.registered: tuple[str, object] | None = None
            events.append(("registry", self))

        def register(self, source_name: str, feed: object) -> None:
            self.registered = (source_name, feed)
            events.append(("register", self.registered))

    class FakeAcquisitionService:
        def __init__(self, *, registry: FakeRegistry) -> None:
            self.registry = registry
            events.append(("service", registry))

        def load_candles(
            self,
            source_name: str,
            symbol: str,
            *,
            interval: str,
            limit: int,
            price_type: str,
        ) -> tuple[Candle, ...]:
            events.append(
                (
                    "load",
                    (
                        source_name,
                        symbol,
                        interval,
                        limit,
                        price_type,
                    ),
                )
            )
            return expected

    monkeypatch.setattr(
        service_module,
        "DzengiCandlesDocumentSource",
        FakeSource,
    )
    monkeypatch.setattr(
        service_module,
        "DzengiCandlesDocumentHandler",
        FakeHandler,
    )
    monkeypatch.setattr(service_module, "CandlesFeed", FakeFeed)
    monkeypatch.setattr(
        service_module,
        "CandlesFeedRegistry",
        FakeRegistry,
    )
    monkeypatch.setattr(
        service_module,
        "CandlesAcquisitionService",
        FakeAcquisitionService,
    )

    service = _service()
    result = service._load_candles_via_acquisition(
        symbol="BTC/USD_LEVERAGE",
        interval="1m",
        limit=100,
        price_type="bid",
    )

    assert result is expected

    source = events[0][1]
    handler = events[1][1]
    feed = events[2][1]
    registry = events[3][1]

    assert isinstance(source, FakeSource)
    assert isinstance(handler, FakeHandler)
    assert isinstance(feed, FakeFeed)
    assert isinstance(registry, FakeRegistry)

    assert feed.source is source
    assert feed.handler is handler
    assert registry.registered == ("dzengi", feed)

    assert (
        "load",
        (
            "dzengi",
            "BTC/USD_LEVERAGE",
            "1m",
            100,
            "bid",
        ),
    ) in events
