# app/tests/unit/market_data/acquisition/feeds/test_instrument_feed.py

from __future__ import annotations

from decimal import Decimal

import pytest

from src.market_data.acquisition.exceptions import (
    InstrumentReferenceTransportError,
    InstrumentReferenceValueError,
)
from src.market_data.acquisition.feeds.instrument_feed import InstrumentFeed
from src.market_data.acquisition.models.instrument import Instrument
from src.market_data.acquisition.protocol import InstrumentFeedProtocol


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


class StubInstrumentDocumentSource:
    def __init__(
        self,
        *,
        document: object,
        error: Exception | None = None,
    ) -> None:
        self.document = document
        self.error = error
        self.call_count = 0

    def fetch_instrument_document(self) -> object:
        self.call_count += 1

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

        return self.document


class StubInstrumentDocumentHandler:
    def __init__(
        self,
        *,
        instruments: tuple[Instrument, ...],
        error: Exception | None = None,
    ) -> None:
        self.instruments = instruments
        self.error = error
        self.documents: list[object] = []

    def handle_instrument_document(
        self,
        document: object,
    ) -> tuple[Instrument, ...]:
        self.documents.append(document)

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

        return self.instruments


def test_feed_satisfies_instrument_feed_protocol() -> None:
    source = StubInstrumentDocumentSource(
        document={
            "symbols": [],
        }
    )
    handler = StubInstrumentDocumentHandler(
        instruments=(),
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    assert isinstance(feed, InstrumentFeedProtocol)


def test_feed_calls_source_once() -> None:
    source = StubInstrumentDocumentSource(
        document={
            "symbols": [],
        }
    )
    handler = StubInstrumentDocumentHandler(
        instruments=(),
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    feed.load_instruments()

    assert source.call_count == 1


def test_feed_calls_handler_once() -> None:
    source = StubInstrumentDocumentSource(
        document={
            "symbols": [],
        }
    )
    handler = StubInstrumentDocumentHandler(
        instruments=(),
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    feed.load_instruments()

    assert len(handler.documents) == 1


def test_feed_passes_document_to_handler_without_changes() -> None:
    document = {
        "status": "OK",
        "payload": {
            "symbols": [],
        },
    }

    source = StubInstrumentDocumentSource(
        document=document,
    )
    handler = StubInstrumentDocumentHandler(
        instruments=(),
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    feed.load_instruments()

    assert handler.documents == [document]
    assert handler.documents[0] is document


def test_feed_returns_handler_result_without_changes() -> None:
    instruments = (
        _instrument(),
    )

    source = StubInstrumentDocumentSource(
        document={
            "symbols": [],
        }
    )
    handler = StubInstrumentDocumentHandler(
        instruments=instruments,
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    result = feed.load_instruments()

    assert result is instruments


def test_feed_preserves_instrument_order() -> None:
    instruments = (
        _instrument(symbol="BTC/USD_LEVERAGE"),
        _instrument(symbol="ETH/USD_LEVERAGE"),
        _instrument(symbol="XRP/USD_LEVERAGE"),
    )

    source = StubInstrumentDocumentSource(
        document={
            "symbols": [],
        }
    )
    handler = StubInstrumentDocumentHandler(
        instruments=instruments,
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    result = feed.load_instruments()

    assert tuple(item.symbol for item in result) == (
        "BTC/USD_LEVERAGE",
        "ETH/USD_LEVERAGE",
        "XRP/USD_LEVERAGE",
    )


def test_feed_returns_empty_tuple_without_error() -> None:
    source = StubInstrumentDocumentSource(
        document={
            "symbols": [],
        }
    )
    handler = StubInstrumentDocumentHandler(
        instruments=(),
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    result = feed.load_instruments()

    assert result == ()


def test_feed_preserves_transport_error_without_wrapping() -> None:
    original_error = InstrumentReferenceTransportError(
        "Не удалось получить exchangeInfo."
    )

    source = StubInstrumentDocumentSource(
        document=None,
        error=original_error,
    )
    handler = StubInstrumentDocumentHandler(
        instruments=(),
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    with pytest.raises(
        InstrumentReferenceTransportError,
    ) as exc_info:
        feed.load_instruments()

    assert exc_info.value is original_error
    assert source.call_count == 1
    assert handler.documents == []


def test_feed_preserves_processing_error_without_wrapping() -> None:
    document = {
        "symbols": [],
    }
    original_error = InstrumentReferenceValueError(
        "Некорректное значение."
    )

    source = StubInstrumentDocumentSource(
        document=document,
    )
    handler = StubInstrumentDocumentHandler(
        instruments=(),
        error=original_error,
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    with pytest.raises(
        InstrumentReferenceValueError,
    ) as exc_info:
        feed.load_instruments()

    assert exc_info.value is original_error
    assert source.call_count == 1
    assert handler.documents == [document]


def test_feed_does_not_retry_source_after_transport_error() -> None:
    original_error = InstrumentReferenceTransportError(
        "Network error."
    )

    source = StubInstrumentDocumentSource(
        document=None,
        error=original_error,
    )
    handler = StubInstrumentDocumentHandler(
        instruments=(),
    )

    feed = InstrumentFeed(
        source=source,
        handler=handler,
    )

    with pytest.raises(InstrumentReferenceTransportError):
        feed.load_instruments()

    assert source.call_count == 1