# app/tests/unit/market_data/acquisition/test_registry.py

from __future__ import annotations

from decimal import Decimal

import pytest

from src.market_data.acquisition.exceptions import (
    InstrumentFeedRegistryError,
    MarketDataAcquisitionError,
)
from src.market_data.acquisition.models.instrument import Instrument
from src.market_data.acquisition.protocol import InstrumentFeedProtocol
from src.market_data.acquisition.registry import InstrumentFeedRegistry


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 StubInstrumentFeed:
    def __init__(
        self,
        *,
        instruments: tuple[Instrument, ...] = (),
    ) -> None:
        self.instruments = instruments
        self.load_call_count = 0

    def load_instruments(self) -> tuple[Instrument, ...]:
        self.load_call_count += 1
        return self.instruments


class InvalidFeed:
    pass


def test_register_and_get_feed() -> None:
    registry = InstrumentFeedRegistry()
    feed = StubInstrumentFeed()

    registry.register("dzengi", feed)

    result = registry.get("dzengi")

    assert result is feed


def test_registry_preserves_feed_identity() -> None:
    registry = InstrumentFeedRegistry()
    feed = StubInstrumentFeed(
        instruments=(
            _instrument(),
        )
    )

    registry.register("dzengi", feed)

    registered_feed = registry.get("dzengi")

    assert registered_feed is feed
    assert registered_feed.load_instruments() is feed.instruments


def test_registry_accepts_instrument_feed_protocol() -> None:
    registry = InstrumentFeedRegistry()
    feed = StubInstrumentFeed()

    assert isinstance(feed, InstrumentFeedProtocol)

    registry.register("dzengi", feed)

    assert registry.get("dzengi") is feed


def test_registry_supports_multiple_source_names() -> None:
    registry = InstrumentFeedRegistry()

    dzengi_feed = StubInstrumentFeed(
        instruments=(
            _instrument(symbol="BTC/USD_LEVERAGE"),
        )
    )
    secondary_feed = StubInstrumentFeed(
        instruments=(
            _instrument(symbol="ETH/USD_LEVERAGE"),
        )
    )

    registry.register("dzengi", dzengi_feed)
    registry.register("secondary", secondary_feed)

    assert registry.get("dzengi") is dzengi_feed
    assert registry.get("secondary") is secondary_feed


def test_registry_strips_outer_whitespace_from_source_name() -> None:
    registry = InstrumentFeedRegistry()
    feed = StubInstrumentFeed()

    registry.register("  dzengi  ", feed)

    assert registry.get("dzengi") is feed
    assert registry.get("  dzengi  ") is feed


@pytest.mark.parametrize(
    "source_name",
    [
        "",
        " ",
        "   ",
        "\t",
        "\n",
    ],
)
def test_registry_rejects_empty_source_name(
    source_name: str,
) -> None:
    registry = InstrumentFeedRegistry()
    feed = StubInstrumentFeed()

    with pytest.raises(
        InstrumentFeedRegistryError,
        match=r"Имя источника Instrument Feed не должно быть пустым",
    ):
        registry.register(source_name, feed)


@pytest.mark.parametrize(
    "source_name",
    [
        "",
        " ",
        "   ",
        "\t",
        "\n",
    ],
)
def test_registry_rejects_empty_source_name_on_get(
    source_name: str,
) -> None:
    registry = InstrumentFeedRegistry()

    with pytest.raises(
        InstrumentFeedRegistryError,
        match=r"Имя источника Instrument Feed не должно быть пустым",
    ):
        registry.get(source_name)


def test_registry_rejects_duplicate_registration() -> None:
    registry = InstrumentFeedRegistry()

    first_feed = StubInstrumentFeed()
    second_feed = StubInstrumentFeed()

    registry.register("dzengi", first_feed)

    with pytest.raises(
        InstrumentFeedRegistryError,
        match=r"уже зарегистрирован",
    ):
        registry.register("dzengi", second_feed)


def test_duplicate_registration_does_not_replace_original_feed() -> None:
    registry = InstrumentFeedRegistry()

    first_feed = StubInstrumentFeed()
    second_feed = StubInstrumentFeed()

    registry.register("dzengi", first_feed)

    with pytest.raises(InstrumentFeedRegistryError):
        registry.register("dzengi", second_feed)

    assert registry.get("dzengi") is first_feed


def test_duplicate_registration_uses_normalized_source_name() -> None:
    registry = InstrumentFeedRegistry()

    first_feed = StubInstrumentFeed()
    second_feed = StubInstrumentFeed()

    registry.register("dzengi", first_feed)

    with pytest.raises(
        InstrumentFeedRegistryError,
        match=r"уже зарегистрирован",
    ):
        registry.register("  dzengi  ", second_feed)


def test_registry_keeps_source_name_case_sensitive() -> None:
    registry = InstrumentFeedRegistry()

    lowercase_feed = StubInstrumentFeed()
    uppercase_feed = StubInstrumentFeed()

    registry.register("dzengi", lowercase_feed)
    registry.register("DZENGI", uppercase_feed)

    assert registry.get("dzengi") is lowercase_feed
    assert registry.get("DZENGI") is uppercase_feed


def test_registry_rejects_unregistered_source() -> None:
    registry = InstrumentFeedRegistry()

    with pytest.raises(
        InstrumentFeedRegistryError,
        match=r"не зарегистрирован",
    ):
        registry.get("dzengi")


def test_registry_rejects_object_without_feed_protocol() -> None:
    registry = InstrumentFeedRegistry()

    with pytest.raises(
        InstrumentFeedRegistryError,
        match=r"не соответствует InstrumentFeedProtocol",
    ):
        registry.register(
            "invalid",
            InvalidFeed(),  # type: ignore[arg-type]
        )


def test_registry_does_not_load_feed_during_registration() -> None:
    registry = InstrumentFeedRegistry()
    feed = StubInstrumentFeed()

    registry.register("dzengi", feed)

    assert feed.load_call_count == 0


def test_registry_does_not_load_feed_during_get() -> None:
    registry = InstrumentFeedRegistry()
    feed = StubInstrumentFeed()

    registry.register("dzengi", feed)
    result = registry.get("dzengi")

    assert result is feed
    assert feed.load_call_count == 0


def test_registry_stores_feed_not_instrument_result() -> None:
    registry = InstrumentFeedRegistry()
    instruments = (
        _instrument(),
    )
    feed = StubInstrumentFeed(
        instruments=instruments,
    )

    registry.register("dzengi", feed)

    registered_feed = registry.get("dzengi")

    assert registered_feed is feed
    assert registered_feed is not instruments


def test_registry_error_inherits_acquisition_error() -> None:
    error = InstrumentFeedRegistryError(
        "Registry error."
    )

    assert isinstance(error, MarketDataAcquisitionError)
    assert str(error) == "Registry error."

# Quotes Feed Registry tests.
from datetime import datetime, timezone

from src.market_data.acquisition.exceptions import QuoteFeedRegistryError
from src.market_data.acquisition.models.quote import Quote
from src.market_data.acquisition.protocol import QuoteFeedProtocol
from src.market_data.acquisition.registry import QuoteFeedRegistry


def _quote() -> Quote:
    return Quote(
        symbol="BTC/USD_LEVERAGE",
        last_price=Decimal("64159.45"),
        bid_price=Decimal("64159.45"),
        ask_price=Decimal("64159.55"),
        exchange_timestamp=datetime.now(timezone.utc),
        received_at=datetime.now(timezone.utc),
        source="dzengi",
    )


class StubQuoteFeed:
    def __init__(self) -> None:
        self.quote = _quote()
        self.symbols: list[str] = []

    def load_quote(
        self,
        symbol: str,
    ) -> Quote:
        self.symbols.append(symbol)
        return self.quote


def test_quote_registry_registers_and_returns_feed() -> None:
    registry = QuoteFeedRegistry()
    feed = StubQuoteFeed()

    registry.register("dzengi", feed)

    assert registry.get("dzengi") is feed


def test_quote_registry_accepts_quote_feed_protocol() -> None:
    registry = QuoteFeedRegistry()
    feed = StubQuoteFeed()

    assert isinstance(feed, QuoteFeedProtocol)

    registry.register("dzengi", feed)

    assert registry.get("dzengi") is feed


def test_quote_registry_strips_outer_whitespace() -> None:
    registry = QuoteFeedRegistry()
    feed = StubQuoteFeed()

    registry.register("  dzengi  ", feed)

    assert registry.get("dzengi") is feed
    assert registry.get("  dzengi  ") is feed


@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"])
def test_quote_registry_rejects_empty_source_name(
    source_name: str,
) -> None:
    registry = QuoteFeedRegistry()

    with pytest.raises(
        QuoteFeedRegistryError,
        match=r"Имя источника Quotes Feed не должно быть пустым",
    ):
        registry.register(source_name, StubQuoteFeed())


def test_quote_registry_rejects_duplicate_registration() -> None:
    registry = QuoteFeedRegistry()
    first_feed = StubQuoteFeed()

    registry.register("dzengi", first_feed)

    with pytest.raises(
        QuoteFeedRegistryError,
        match=r"уже зарегистрирован",
    ):
        registry.register("dzengi", StubQuoteFeed())

    assert registry.get("dzengi") is first_feed


def test_quote_registry_rejects_unregistered_source() -> None:
    registry = QuoteFeedRegistry()

    with pytest.raises(
        QuoteFeedRegistryError,
        match=r"не зарегистрирован",
    ):
        registry.get("dzengi")


def test_quote_registry_rejects_invalid_feed() -> None:
    registry = QuoteFeedRegistry()

    with pytest.raises(
        QuoteFeedRegistryError,
        match=r"не соответствует QuoteFeedProtocol",
    ):
        registry.register(
            "invalid",
            InvalidFeed(),  # type: ignore[arg-type]
        )


def test_quote_registry_does_not_load_feed() -> None:
    registry = QuoteFeedRegistry()
    feed = StubQuoteFeed()

    registry.register("dzengi", feed)
    result = registry.get("dzengi")

    assert result is feed
    assert feed.symbols == []


def test_quote_registry_error_inherits_acquisition_error() -> None:
    error = QuoteFeedRegistryError("Registry error.")

    assert isinstance(error, MarketDataAcquisitionError)

# Candles Feed Registry tests.
from datetime import datetime

from src.market_data.acquisition.exceptions import CandleFeedRegistryError
from src.market_data.acquisition.models.candle import Candle
from src.market_data.acquisition.protocol import CandlesFeedProtocol
from src.market_data.acquisition.registry import CandlesFeedRegistry


def _candle() -> Candle:
    return Candle(
        symbol="BTC/USD_LEVERAGE",
        interval="1m",
        open_time=datetime.now(timezone.utc),
        open_price=Decimal("64100.00"),
        high_price=Decimal("64200.00"),
        low_price=Decimal("64000.00"),
        close_price=Decimal("64150.00"),
        volume=Decimal("12.5"),
        source="rest_klines:bid",
    )


class StubCandlesFeed:
    def __init__(
        self,
        *,
        candles: tuple[Candle, ...] = (),
    ) -> None:
        self.candles = candles
        self.calls: list[tuple[str, str, int, str]] = []

    def load_candles(
        self,
        symbol: str,
        *,
        interval: str,
        limit: int,
        price_type: str,
    ) -> tuple[Candle, ...]:
        self.calls.append(
            (
                symbol,
                interval,
                limit,
                price_type,
            )
        )
        return self.candles


def test_candles_registry_registers_and_returns_feed() -> None:
    registry = CandlesFeedRegistry()
    feed = StubCandlesFeed()

    registry.register("dzengi", feed)

    assert registry.get("dzengi") is feed


def test_candles_registry_accepts_candles_feed_protocol() -> None:
    registry = CandlesFeedRegistry()
    feed = StubCandlesFeed()

    assert isinstance(feed, CandlesFeedProtocol)

    registry.register("dzengi", feed)

    assert registry.get("dzengi") is feed


def test_candles_registry_preserves_feed_identity() -> None:
    registry = CandlesFeedRegistry()
    candles = (_candle(),)
    feed = StubCandlesFeed(candles=candles)

    registry.register("dzengi", feed)

    registered_feed = registry.get("dzengi")

    assert registered_feed is feed
    assert registered_feed.load_candles(
        "BTC/USD_LEVERAGE",
        interval="1m",
        limit=100,
        price_type="bid",
    ) is candles


def test_candles_registry_supports_multiple_source_names() -> None:
    registry = CandlesFeedRegistry()
    dzengi_feed = StubCandlesFeed()
    secondary_feed = StubCandlesFeed()

    registry.register("dzengi", dzengi_feed)
    registry.register("secondary", secondary_feed)

    assert registry.get("dzengi") is dzengi_feed
    assert registry.get("secondary") is secondary_feed


def test_candles_registry_strips_outer_whitespace() -> None:
    registry = CandlesFeedRegistry()
    feed = StubCandlesFeed()

    registry.register("  dzengi  ", feed)

    assert registry.get("dzengi") is feed
    assert registry.get("  dzengi  ") is feed


@pytest.mark.parametrize(
    "source_name",
    [
        "",
        " ",
        "   ",
        "\t",
        "\n",
    ],
)
def test_candles_registry_rejects_empty_source_name(
    source_name: str,
) -> None:
    registry = CandlesFeedRegistry()

    with pytest.raises(
        CandleFeedRegistryError,
        match=r"Имя источника Candles Feed не должно быть пустым",
    ):
        registry.register(source_name, StubCandlesFeed())


@pytest.mark.parametrize(
    "source_name",
    [
        "",
        " ",
        "   ",
        "\t",
        "\n",
    ],
)
def test_candles_registry_rejects_empty_source_name_on_get(
    source_name: str,
) -> None:
    registry = CandlesFeedRegistry()

    with pytest.raises(
        CandleFeedRegistryError,
        match=r"Имя источника Candles Feed не должно быть пустым",
    ):
        registry.get(source_name)


def test_candles_registry_rejects_duplicate_registration() -> None:
    registry = CandlesFeedRegistry()
    first_feed = StubCandlesFeed()
    second_feed = StubCandlesFeed()

    registry.register("dzengi", first_feed)

    with pytest.raises(
        CandleFeedRegistryError,
        match=r"уже зарегистрирован",
    ):
        registry.register("dzengi", second_feed)


def test_candles_duplicate_does_not_replace_original_feed() -> None:
    registry = CandlesFeedRegistry()
    first_feed = StubCandlesFeed()
    second_feed = StubCandlesFeed()

    registry.register("dzengi", first_feed)

    with pytest.raises(CandleFeedRegistryError):
        registry.register("dzengi", second_feed)

    assert registry.get("dzengi") is first_feed


def test_candles_duplicate_uses_normalized_source_name() -> None:
    registry = CandlesFeedRegistry()
    first_feed = StubCandlesFeed()
    second_feed = StubCandlesFeed()

    registry.register("dzengi", first_feed)

    with pytest.raises(
        CandleFeedRegistryError,
        match=r"уже зарегистрирован",
    ):
        registry.register("  dzengi  ", second_feed)


def test_candles_registry_keeps_source_name_case_sensitive() -> None:
    registry = CandlesFeedRegistry()
    lowercase_feed = StubCandlesFeed()
    uppercase_feed = StubCandlesFeed()

    registry.register("dzengi", lowercase_feed)
    registry.register("DZENGI", uppercase_feed)

    assert registry.get("dzengi") is lowercase_feed
    assert registry.get("DZENGI") is uppercase_feed


def test_candles_registry_rejects_unregistered_source() -> None:
    registry = CandlesFeedRegistry()

    with pytest.raises(
        CandleFeedRegistryError,
        match=r"не зарегистрирован",
    ):
        registry.get("dzengi")


def test_candles_registry_rejects_invalid_feed() -> None:
    registry = CandlesFeedRegistry()

    with pytest.raises(
        CandleFeedRegistryError,
        match=r"не соответствует CandlesFeedProtocol",
    ):
        registry.register(
            "invalid",
            InvalidFeed(),  # type: ignore[arg-type]
        )


def test_candles_registry_does_not_load_feed_during_registration() -> None:
    registry = CandlesFeedRegistry()
    feed = StubCandlesFeed()

    registry.register("dzengi", feed)

    assert feed.calls == []


def test_candles_registry_does_not_load_feed_during_get() -> None:
    registry = CandlesFeedRegistry()
    feed = StubCandlesFeed()

    registry.register("dzengi", feed)
    result = registry.get("dzengi")

    assert result is feed
    assert feed.calls == []


def test_candles_registry_error_inherits_acquisition_error() -> None:
    error = CandleFeedRegistryError("Registry error.")

    assert isinstance(error, MarketDataAcquisitionError)
    assert str(error) == "Registry error."
