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

from __future__ import annotations

from types import SimpleNamespace

import pytest

import src.integrations.exchange.market_data_runner as runner_module
from src.integrations.exchange.market_data_runner import MarketDataRunner


class StubExchangeService:
    def __init__(
        self,
        *,
        validation: object | None = None,
        error: Exception | None = None,
    ) -> None:
        self._validation = validation
        self._error = error
        self.requested_symbols: list[str] = []

    def validate_symbol(
        self,
        symbol: str,
    ) -> object:
        self.requested_symbols.append(symbol)

        if self._error is not None:
            raise self._error

        return self._validation


def test_cache_symbol_uses_normalized_symbol_from_validation(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    validation = SimpleNamespace(
        is_valid=True,
        normalized_symbol="BTC/USD_LEVERAGE",
    )
    service = StubExchangeService(
        validation=validation,
    )

    monkeypatch.setattr(
        runner_module,
        "ExchangeService",
        lambda: service,
    )

    result = MarketDataRunner._cache_symbol(
        " btc/usd_leverage "
    )

    assert result == "BTC/USD_LEVERAGE"
    assert service.requested_symbols == [
        " btc/usd_leverage ",
    ]


def test_cache_symbol_does_not_require_symbol_info(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    validation = SimpleNamespace(
        is_valid=True,
        normalized_symbol="ETH/USD_LEVERAGE",
    )
    service = StubExchangeService(
        validation=validation,
    )

    monkeypatch.setattr(
        runner_module,
        "ExchangeService",
        lambda: service,
    )

    result = MarketDataRunner._cache_symbol(
        "ETH/USD_LEVERAGE"
    )

    assert result == "ETH/USD_LEVERAGE"
    assert not hasattr(
        validation,
        "symbol_info",
    )


def test_cache_symbol_returns_original_symbol_when_validation_is_invalid(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    validation = SimpleNamespace(
        is_valid=False,
        normalized_symbol="UNKNOWN/USD",
    )
    service = StubExchangeService(
        validation=validation,
    )

    monkeypatch.setattr(
        runner_module,
        "ExchangeService",
        lambda: service,
    )

    result = MarketDataRunner._cache_symbol(
        "unknown/usd"
    )

    assert result == "unknown/usd"
    assert service.requested_symbols == [
        "unknown/usd",
    ]


def test_cache_symbol_returns_original_symbol_when_validation_fails(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = StubExchangeService(
        error=RuntimeError(
            "Instrument reference data unavailable."
        ),
    )

    monkeypatch.setattr(
        runner_module,
        "ExchangeService",
        lambda: service,
    )

    result = MarketDataRunner._cache_symbol(
        "BTC/USD"
    )

    assert result == "BTC/USD"
    assert service.requested_symbols == [
        "BTC/USD",
    ]


def test_ws_symbol_uses_cache_symbol(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    calls: list[str] = []

    def cache_symbol(
        symbol: str,
    ) -> str:
        calls.append(symbol)
        return "BTC/USD_LEVERAGE"

    monkeypatch.setattr(
        MarketDataRunner,
        "_cache_symbol",
        cache_symbol,
    )

    result = MarketDataRunner._ws_symbol(
        "btc/usd_leverage"
    )

    assert result == "BTC/USD_LEVERAGE"
    assert calls == [
        "btc/usd_leverage",
    ]

def test_run_websocket_uses_canonical_quote_adapter(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    import asyncio
    from datetime import datetime, timezone
    from decimal import Decimal

    from src.integrations.exchange.market_data_runner import MarketRuntimeContext
    from src.market_data.acquisition.models.quote import Quote

    payload = {"payload": {"symbol": "BTC/USD_LEVERAGE"}}
    quote = Quote(
        symbol="BTC/USD_LEVERAGE",
        last_price=Decimal("100"),
        bid_price=Decimal("99"),
        ask_price=Decimal("101"),
        exchange_timestamp=None,
        received_at=datetime.now(timezone.utc),
        source="dzengi",
    )
    mapped_documents: list[object] = []
    stored_quotes: list[tuple[Quote, str]] = []

    class Client:
        async def stream_depth(
            self,
            symbol: str,
            *,
            interval_seconds: int,
        ):
            assert symbol == "BTC/USD_LEVERAGE"
            assert interval_seconds == 1
            yield payload

    class Adapter:
        def map_message(self, document: object) -> Quote:
            mapped_documents.append(document)
            return quote

    class Store:
        def set(
            self,
            source_name: str,
            value: Quote,
            *,
            runtime_key: str = "default",
        ) -> None:
            assert source_name == runner_module.QUOTE_RUNTIME_SOURCE_NAME
            stored_quotes.append((value, runtime_key))

    context = MarketRuntimeContext(
        runtime_key="auto",
        task=None,
        interval_seconds=1,
        symbol_provider=lambda: "BTC/USD_LEVERAGE",
        screen=None,
        action="market_data",
        runtime_label=None,
    )

    monkeypatch.setattr(
        MarketDataRunner,
        "_cache_symbol",
        lambda symbol: "BTC/USD_LEVERAGE",
    )
    monkeypatch.setattr(
        MarketDataRunner,
        "_ws_symbol",
        lambda symbol: "BTC/USD_LEVERAGE",
    )
    monkeypatch.setattr(
        MarketDataRunner,
        "_can_log_runtime_event",
        lambda event_key: False,
    )
    monkeypatch.setattr(
        MarketDataRunner,
        "_log_ws_depth_debug",
        lambda **kwargs: None,
    )
    monkeypatch.setattr(runner_module, "ExchangeWebSocketClient", Client)
    monkeypatch.setattr(runner_module, "DzengiWebSocketQuoteAdapter", Adapter)
    monkeypatch.setattr(runner_module, "get_quote_store", lambda: Store())

    asyncio.run(
        MarketDataRunner._run_websocket(
            context,
            "BTC/USD_LEVERAGE",
        )
    )

    assert mapped_documents == [payload]
    assert stored_quotes == [(quote, "auto")]


def test_run_websocket_raises_after_five_invalid_quotes(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    import asyncio

    from src.integrations.exchange.market_data_runner import MarketRuntimeContext
    from src.market_data.acquisition.exceptions import QuoteSchemaError

    class Client:
        async def stream_depth(
            self,
            symbol: str,
            *,
            interval_seconds: int,
        ):
            del symbol, interval_seconds

            for index in range(5):
                yield {"invalid": index}

    class Adapter:
        def map_message(self, document: object) -> object:
            del document
            raise QuoteSchemaError("Invalid message.")

    context = MarketRuntimeContext(
        runtime_key="auto",
        task=None,
        interval_seconds=1,
        symbol_provider=lambda: "BTC/USD_LEVERAGE",
        screen=None,
        action="market_data",
        runtime_label=None,
    )

    monkeypatch.setattr(
        MarketDataRunner,
        "_cache_symbol",
        lambda symbol: "BTC/USD_LEVERAGE",
    )
    monkeypatch.setattr(
        MarketDataRunner,
        "_ws_symbol",
        lambda symbol: "BTC/USD_LEVERAGE",
    )
    monkeypatch.setattr(runner_module, "ExchangeWebSocketClient", Client)
    monkeypatch.setattr(runner_module, "DzengiWebSocketQuoteAdapter", Adapter)

    with pytest.raises(
        RuntimeError,
        match="does not contain valid quotes",
    ):
        asyncio.run(
            MarketDataRunner._run_websocket(
                context,
                "BTC/USD_LEVERAGE",
            )
        )


def test_rest_fallback_once_refreshes_quote_cache(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    import asyncio

    from src.integrations.exchange.market_data_runner import MarketRuntimeContext

    calls: list[tuple[str, str | None]] = []

    class Service:
        def refresh_quote_cache(
            self,
            symbol: str,
            *,
            runtime_key: str | None = None,
        ) -> None:
            calls.append((symbol, runtime_key))

    context = MarketRuntimeContext(
        runtime_key="debug_auto",
        task=None,
        interval_seconds=1,
        symbol_provider=lambda: "BTC/USD_LEVERAGE",
        screen=None,
        action="market_data",
        runtime_label=None,
        last_rest_state="UNAVAILABLE",
        last_rest_error_key="previous-error",
    )

    monkeypatch.setattr(runner_module, "ExchangeService", Service)
    monkeypatch.setattr(
        MarketDataRunner,
        "_can_log_runtime_event",
        lambda _: False,
    )

    asyncio.run(
        MarketDataRunner._rest_fallback_once(
            context,
            "BTC/USD_LEVERAGE",
        )
    )

    assert calls == [("BTC/USD_LEVERAGE", "debug_auto")]
    assert context.last_rest_state == "AVAILABLE"
    assert context.last_rest_error_key is None


def test_rest_fallback_once_preserves_unavailable_error_state(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    import asyncio

    from src.integrations.exchange.market_data_runner import MarketRuntimeContext

    class Service:
        def refresh_quote_cache(
            self,
            symbol: str,
            *,
            runtime_key: str | None = None,
        ) -> None:
            del symbol, runtime_key
            raise RuntimeError("REST unavailable")

    context = MarketRuntimeContext(
        runtime_key="auto",
        task=None,
        interval_seconds=1,
        symbol_provider=lambda: "BTC/USD_LEVERAGE",
        screen=None,
        action="market_data",
        runtime_label=None,
    )

    monkeypatch.setattr(runner_module, "ExchangeService", Service)
    monkeypatch.setattr(
        MarketDataRunner,
        "_can_log_runtime_event",
        lambda _: False,
    )

    asyncio.run(
        MarketDataRunner._rest_fallback_once(
            context,
            "BTC/USD_LEVERAGE",
        )
    )

    assert context.last_rest_state == "UNAVAILABLE"
    assert context.last_rest_error_key == (
        "BTC/USD_LEVERAGE:RuntimeError:REST unavailable"
    )

