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

from __future__ import annotations

import asyncio
from types import SimpleNamespace

import pytest

import src.integrations.exchange.market_stream as stream_module
from src.integrations.exchange.market_stream import start_market_stream


class StopStream(Exception):
    pass


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

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

        return self._validation


class StubWebSocketClient:
    def __init__(
        self,
        *,
        requested_symbols: list[str],
    ) -> None:
        self._requested_symbols = requested_symbols

    async def stream_depth(
        self,
        symbol: str,
    ):
        self._requested_symbols.append(symbol)

        raise StopStream

        if False:
            yield {}


async def _raise_stop_stream(
    delay: float,
) -> None:
    del delay

    raise StopStream


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

    service = StubExchangeService(
        validation=validation,
    )

    websocket_symbols: list[str] = []

    settings = SimpleNamespace(
        exchange_enabled=True,
        default_symbol=" btc/usd_leverage ",
    )

    monkeypatch.setattr(
        stream_module,
        "load_settings",
        lambda: settings,
    )

    monkeypatch.setattr(
        stream_module,
        "JournalService",
        lambda: SimpleNamespace(
            log_info=lambda *args, **kwargs: None,
            log_warning=lambda *args, **kwargs: None,
        ),
    )

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

    monkeypatch.setattr(
        stream_module,
        "ExchangeWebSocketClient",
        lambda: StubWebSocketClient(
            requested_symbols=websocket_symbols,
        ),
    )

    monkeypatch.setattr(
        stream_module.asyncio,
        "sleep",
        _raise_stop_stream,
    )

    with pytest.raises(StopStream):
        asyncio.run(
            start_market_stream()
        )

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

    assert websocket_symbols == [
        "BTC/USD_LEVERAGE",
    ]


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

    service = StubExchangeService(
        validation=validation,
    )

    websocket_symbols: list[str] = []

    settings = SimpleNamespace(
        exchange_enabled=True,
        default_symbol="ETH/USD_LEVERAGE",
    )

    monkeypatch.setattr(
        stream_module,
        "load_settings",
        lambda: settings,
    )

    monkeypatch.setattr(
        stream_module,
        "JournalService",
        lambda: SimpleNamespace(
            log_info=lambda *args, **kwargs: None,
            log_warning=lambda *args, **kwargs: None,
        ),
    )

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

    monkeypatch.setattr(
        stream_module,
        "ExchangeWebSocketClient",
        lambda: StubWebSocketClient(
            requested_symbols=websocket_symbols,
        ),
    )

    monkeypatch.setattr(
        stream_module.asyncio,
        "sleep",
        _raise_stop_stream,
    )

    with pytest.raises(StopStream):
        asyncio.run(
            start_market_stream()
        )

    assert websocket_symbols == [
        "ETH/USD_LEVERAGE",
    ]

    assert not hasattr(
        validation,
        "symbol_info",
    )


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

    service = StubExchangeService(
        validation=validation,
    )

    websocket_client_calls = 0

    settings = SimpleNamespace(
        exchange_enabled=True,
        default_symbol="UNKNOWN/USD",
    )

    def create_websocket_client() -> object:
        nonlocal websocket_client_calls

        websocket_client_calls += 1

        raise AssertionError(
            "WebSocket client must not be created "
            "for an invalid symbol."
        )

    monkeypatch.setattr(
        stream_module,
        "load_settings",
        lambda: settings,
    )

    monkeypatch.setattr(
        stream_module,
        "JournalService",
        lambda: SimpleNamespace(
            log_info=lambda *args, **kwargs: None,
            log_warning=lambda *args, **kwargs: None,
        ),
    )

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

    monkeypatch.setattr(
        stream_module,
        "ExchangeWebSocketClient",
        create_websocket_client,
    )

    monkeypatch.setattr(
        stream_module.asyncio,
        "sleep",
        _raise_stop_stream,
    )

    with pytest.raises(StopStream):
        asyncio.run(
            start_market_stream()
        )

    assert service.requested_symbols == [
        "UNKNOWN/USD",
    ]

    assert websocket_client_calls == 0


def test_start_market_stream_returns_when_exchange_disabled(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    settings = SimpleNamespace(
        exchange_enabled=False,
        default_symbol="BTC/USD",
    )

    service_calls = 0

    def create_service() -> object:
        nonlocal service_calls

        service_calls += 1

        raise AssertionError(
            "ExchangeService must not be created "
            "when exchange is disabled."
        )

    monkeypatch.setattr(
        stream_module,
        "load_settings",
        lambda: settings,
    )

    monkeypatch.setattr(
        stream_module,
        "JournalService",
        lambda: SimpleNamespace(),
    )

    monkeypatch.setattr(
        stream_module,
        "ExchangeService",
        create_service,
    )

    asyncio.run(
        start_market_stream()
    )

    assert service_calls == 0

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

    from src.market_data.acquisition.models.quote import Quote

    validation = SimpleNamespace(
        is_valid=True,
        normalized_symbol="BTC/USD_LEVERAGE",
    )
    service = StubExchangeService(validation=validation)
    settings = SimpleNamespace(
        exchange_enabled=True,
        default_symbol="BTC/USD_LEVERAGE",
    )
    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):
            assert symbol == "BTC/USD_LEVERAGE"
            yield payload
            raise StopStream

    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 == stream_module.QUOTE_RUNTIME_SOURCE_NAME
            stored_quotes.append((value, runtime_key))

    monkeypatch.setattr(stream_module, "load_settings", lambda: settings)
    monkeypatch.setattr(
        stream_module,
        "JournalService",
        lambda: SimpleNamespace(
            log_info=lambda *args, **kwargs: None,
            log_warning=lambda *args, **kwargs: None,
        ),
    )
    monkeypatch.setattr(stream_module, "ExchangeService", lambda: service)
    monkeypatch.setattr(stream_module, "ExchangeWebSocketClient", Client)
    monkeypatch.setattr(stream_module, "DzengiWebSocketQuoteAdapter", Adapter)
    monkeypatch.setattr(stream_module, "get_quote_store", lambda: Store())
    monkeypatch.setattr(stream_module.asyncio, "sleep", _raise_stop_stream)

    with pytest.raises(StopStream):
        asyncio.run(start_market_stream())

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


def test_start_market_stream_skips_invalid_adapter_message(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    from src.market_data.acquisition.exceptions import QuoteSchemaError

    validation = SimpleNamespace(
        is_valid=True,
        normalized_symbol="BTC/USD_LEVERAGE",
    )
    service = StubExchangeService(validation=validation)
    settings = SimpleNamespace(
        exchange_enabled=True,
        default_symbol="BTC/USD_LEVERAGE",
    )
    cache_calls = 0

    class Client:
        async def stream_depth(self, symbol: str):
            del symbol
            yield {"invalid": True}
            raise StopStream

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

    class Store:
        def set(self, *args: object, **kwargs: object) -> None:
            nonlocal cache_calls
            cache_calls += 1

    monkeypatch.setattr(stream_module, "load_settings", lambda: settings)
    monkeypatch.setattr(
        stream_module,
        "JournalService",
        lambda: SimpleNamespace(
            log_info=lambda *args, **kwargs: None,
            log_warning=lambda *args, **kwargs: None,
        ),
    )
    monkeypatch.setattr(stream_module, "ExchangeService", lambda: service)
    monkeypatch.setattr(stream_module, "ExchangeWebSocketClient", Client)
    monkeypatch.setattr(stream_module, "DzengiWebSocketQuoteAdapter", Adapter)
    monkeypatch.setattr(stream_module, "get_quote_store", lambda: Store())
    monkeypatch.setattr(stream_module.asyncio, "sleep", _raise_stop_stream)

    with pytest.raises(StopStream):
        asyncio.run(start_market_stream())

    assert cache_calls == 0
