# app/tests/unit/storage/test_quote_store.py

from __future__ import annotations

from datetime import datetime, timezone
from decimal import Decimal
from typing import cast

import pytest

from src.market_data.acquisition.models.quote import Quote
from src.storage.exceptions import QuoteStoreError, StorageError
import src.storage.quote_store as quote_store_module
from src.storage.quote_store import (
    InMemoryQuoteStore,
    QUOTE_RUNTIME_SOURCE_NAME,
    QuoteStoreProtocol,
    get_quote_store,
)


def _quote(
    *,
    symbol: str = "BTC/USD_LEVERAGE",
    last_price: Decimal = Decimal("64159.45"),
    source: str = "dzengi",
) -> Quote:
    return Quote(
        symbol=symbol,
        last_price=last_price,
        bid_price=last_price - Decimal("0.05"),
        ask_price=last_price + Decimal("0.05"),
        exchange_timestamp=datetime(
            2026,
            7,
            12,
            18,
            0,
            tzinfo=timezone.utc,
        ),
        received_at=datetime(
            2026,
            7,
            12,
            18,
            0,
            1,
            tzinfo=timezone.utc,
        ),
        source=source,
    )


def test_in_memory_quote_store_matches_protocol() -> None:
    assert isinstance(
        InMemoryQuoteStore(),
        QuoteStoreProtocol,
    )


def test_get_returns_none_for_unknown_key() -> None:
    store = InMemoryQuoteStore()

    assert store.get("dzengi", "BTC/USD_LEVERAGE") is None


def test_set_and_get_preserve_quote_identity() -> None:
    store = InMemoryQuoteStore()
    quote = _quote()

    store.set("dzengi", quote)

    assert store.get("dzengi", quote.symbol) is quote


def test_set_replaces_existing_quote() -> None:
    store = InMemoryQuoteStore()
    first = _quote(last_price=Decimal("100"))
    second = _quote(last_price=Decimal("101"))

    store.set("dzengi", first)
    store.set("dzengi", second)

    assert store.get("dzengi", second.symbol) is second


def test_different_sources_are_isolated() -> None:
    store = InMemoryQuoteStore()
    first = _quote(source="dzengi")
    second = _quote(source="secondary")

    store.set("dzengi", first)
    store.set("secondary", second)

    assert store.get("dzengi", first.symbol) is first
    assert store.get("secondary", second.symbol) is second


def test_source_name_alias_does_not_have_to_match_quote_source() -> None:
    store = InMemoryQuoteStore()
    quote = _quote(source="dzengi")

    store.set("dzengi-primary", quote)

    assert store.get("dzengi-primary", quote.symbol) is quote


def test_source_name_outer_spaces_are_normalized() -> None:
    store = InMemoryQuoteStore()
    quote = _quote()

    store.set("  dzengi  ", quote)

    assert store.get("dzengi", quote.symbol) is quote
    assert store.get("  dzengi  ", quote.symbol) is quote


def test_source_name_case_is_preserved() -> None:
    store = InMemoryQuoteStore()
    lowercase = _quote(last_price=Decimal("100"))
    uppercase = _quote(last_price=Decimal("200"))

    store.set("dzengi", lowercase)
    store.set("DZENGI", uppercase)

    assert store.get("dzengi", lowercase.symbol) is lowercase
    assert store.get("DZENGI", uppercase.symbol) is uppercase


def test_runtime_keys_are_isolated() -> None:
    store = InMemoryQuoteStore()
    auto_quote = _quote(last_price=Decimal("100"))
    debug_quote = _quote(last_price=Decimal("200"))

    store.set("dzengi", auto_quote, runtime_key="auto")
    store.set("dzengi", debug_quote, runtime_key="debug_auto")

    assert store.get(
        "dzengi",
        auto_quote.symbol,
        runtime_key="auto",
    ) is auto_quote
    assert store.get(
        "dzengi",
        debug_quote.symbol,
        runtime_key="debug_auto",
    ) is debug_quote


def test_runtime_key_is_trimmed_and_lowercased() -> None:
    store = InMemoryQuoteStore()
    quote = _quote()

    store.set("dzengi", quote, runtime_key="  AUTO  ")

    assert store.get(
        "dzengi",
        quote.symbol,
        runtime_key="auto",
    ) is quote
    assert store.get(
        "dzengi",
        quote.symbol,
        runtime_key=" AUTO ",
    ) is quote


def test_symbols_are_isolated() -> None:
    store = InMemoryQuoteStore()
    btc = _quote(symbol="BTC/USD_LEVERAGE")
    eth = _quote(
        symbol="ETH/USD_LEVERAGE",
        last_price=Decimal("3500"),
    )

    store.set("dzengi", btc)
    store.set("dzengi", eth)

    assert store.get("dzengi", btc.symbol) is btc
    assert store.get("dzengi", eth.symbol) is eth


def test_symbol_key_is_trimmed_and_uppercased() -> None:
    store = InMemoryQuoteStore()
    quote = _quote(symbol=" btc/usd_leverage ")

    store.set("dzengi", quote)

    assert store.get("dzengi", "BTC/USD_LEVERAGE") is quote
    assert store.get("dzengi", " btc/usd_leverage ") is quote


@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"])
def test_get_rejects_empty_source_name(source_name: str) -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(
        QuoteStoreError,
        match=r"Имя источника Quote Store не должно быть пустым",
    ):
        store.get(source_name, "BTC/USD_LEVERAGE")


@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"])
def test_set_rejects_empty_source_name(source_name: str) -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(QuoteStoreError):
        store.set(source_name, _quote())


@pytest.mark.parametrize("source_name", ["", " ", "\t", "\n"])
def test_clear_rejects_empty_source_name(source_name: str) -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(QuoteStoreError):
        store.clear(source_name=source_name)


@pytest.mark.parametrize("runtime_key", ["", " ", "\t", "\n"])
def test_get_rejects_empty_runtime_key(runtime_key: str) -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(
        QuoteStoreError,
        match=r"Runtime key Quote Store не должен быть пустым",
    ):
        store.get(
            "dzengi",
            "BTC/USD_LEVERAGE",
            runtime_key=runtime_key,
        )


@pytest.mark.parametrize("runtime_key", ["", " ", "\t", "\n"])
def test_set_rejects_empty_runtime_key(runtime_key: str) -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(QuoteStoreError):
        store.set(
            "dzengi",
            _quote(),
            runtime_key=runtime_key,
        )


@pytest.mark.parametrize("runtime_key", ["", " ", "\t", "\n"])
def test_clear_rejects_empty_runtime_key(runtime_key: str) -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(QuoteStoreError):
        store.clear(runtime_key=runtime_key)


@pytest.mark.parametrize("symbol", ["", " ", "\t", "\n"])
def test_get_rejects_empty_symbol(symbol: str) -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(
        QuoteStoreError,
        match=r"Символ Quote Store не должен быть пустым",
    ):
        store.get("dzengi", symbol)


@pytest.mark.parametrize("symbol", ["", " ", "\t", "\n"])
def test_set_rejects_quote_with_empty_symbol(symbol: str) -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(QuoteStoreError):
        store.set("dzengi", _quote(symbol=symbol))


@pytest.mark.parametrize("symbol", ["", " ", "\t", "\n"])
def test_clear_rejects_empty_symbol(symbol: str) -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(QuoteStoreError):
        store.clear(symbol=symbol)


def test_set_rejects_non_quote_object() -> None:
    store = InMemoryQuoteStore()

    with pytest.raises(
        QuoteStoreError,
        match=r"принимает только объект Quote",
    ):
        store.set(
            "dzengi",
            cast(Quote, object()),
        )


def test_clear_without_filters_removes_all_quotes() -> None:
    store = InMemoryQuoteStore()
    store.set("dzengi", _quote(symbol="BTC/USD_LEVERAGE"))
    store.set("secondary", _quote(symbol="ETH/USD_LEVERAGE"))

    store.clear()

    assert store.get("dzengi", "BTC/USD_LEVERAGE") is None
    assert store.get("secondary", "ETH/USD_LEVERAGE") is None


def test_clear_by_source_removes_all_source_quotes() -> None:
    store = InMemoryQuoteStore()
    dzengi_btc = _quote(symbol="BTC/USD_LEVERAGE")
    dzengi_eth = _quote(symbol="ETH/USD_LEVERAGE")
    secondary_btc = _quote(symbol="BTC/USD_LEVERAGE")

    store.set("dzengi", dzengi_btc, runtime_key="auto")
    store.set("dzengi", dzengi_eth, runtime_key="debug_auto")
    store.set("secondary", secondary_btc, runtime_key="auto")

    store.clear(source_name="dzengi")

    assert store.get("dzengi", dzengi_btc.symbol, runtime_key="auto") is None
    assert store.get("dzengi", dzengi_eth.symbol, runtime_key="debug_auto") is None
    assert store.get("secondary", secondary_btc.symbol, runtime_key="auto") is secondary_btc


def test_clear_by_runtime_removes_all_runtime_quotes() -> None:
    store = InMemoryQuoteStore()
    auto_btc = _quote(symbol="BTC/USD_LEVERAGE")
    auto_eth = _quote(symbol="ETH/USD_LEVERAGE")
    debug_btc = _quote(symbol="BTC/USD_LEVERAGE")

    store.set("dzengi", auto_btc, runtime_key="auto")
    store.set("secondary", auto_eth, runtime_key="auto")
    store.set("dzengi", debug_btc, runtime_key="debug_auto")

    store.clear(runtime_key="AUTO")

    assert store.get("dzengi", auto_btc.symbol, runtime_key="auto") is None
    assert store.get("secondary", auto_eth.symbol, runtime_key="auto") is None
    assert store.get("dzengi", debug_btc.symbol, runtime_key="debug_auto") is debug_btc


def test_clear_by_symbol_removes_symbol_from_all_namespaces() -> None:
    store = InMemoryQuoteStore()
    dzengi_btc = _quote(symbol="BTC/USD_LEVERAGE")
    secondary_btc = _quote(symbol="BTC/USD_LEVERAGE")
    dzengi_eth = _quote(symbol="ETH/USD_LEVERAGE")

    store.set("dzengi", dzengi_btc, runtime_key="auto")
    store.set("secondary", secondary_btc, runtime_key="debug_auto")
    store.set("dzengi", dzengi_eth, runtime_key="auto")

    store.clear(symbol=" btc/usd_leverage ")

    assert store.get("dzengi", dzengi_btc.symbol, runtime_key="auto") is None
    assert store.get("secondary", secondary_btc.symbol, runtime_key="debug_auto") is None
    assert store.get("dzengi", dzengi_eth.symbol, runtime_key="auto") is dzengi_eth


def test_clear_exact_record_removes_only_that_record() -> None:
    store = InMemoryQuoteStore()
    auto_quote = _quote()
    debug_quote = _quote(last_price=Decimal("65000"))

    store.set("dzengi", auto_quote, runtime_key="auto")
    store.set("dzengi", debug_quote, runtime_key="debug_auto")

    store.clear(
        source_name="dzengi",
        symbol="BTC/USD_LEVERAGE",
        runtime_key="auto",
    )

    assert store.get("dzengi", auto_quote.symbol, runtime_key="auto") is None
    assert store.get("dzengi", debug_quote.symbol, runtime_key="debug_auto") is debug_quote


def test_clear_combined_source_and_runtime_filters() -> None:
    store = InMemoryQuoteStore()
    dzengi_auto_btc = _quote(symbol="BTC/USD_LEVERAGE")
    dzengi_auto_eth = _quote(symbol="ETH/USD_LEVERAGE")
    dzengi_debug_btc = _quote(symbol="BTC/USD_LEVERAGE")
    secondary_auto_btc = _quote(symbol="BTC/USD_LEVERAGE")

    store.set("dzengi", dzengi_auto_btc, runtime_key="auto")
    store.set("dzengi", dzengi_auto_eth, runtime_key="auto")
    store.set("dzengi", dzengi_debug_btc, runtime_key="debug_auto")
    store.set("secondary", secondary_auto_btc, runtime_key="auto")

    store.clear(source_name="dzengi", runtime_key="auto")

    assert store.get("dzengi", dzengi_auto_btc.symbol, runtime_key="auto") is None
    assert store.get("dzengi", dzengi_auto_eth.symbol, runtime_key="auto") is None
    assert store.get("dzengi", dzengi_debug_btc.symbol, runtime_key="debug_auto") is dzengi_debug_btc
    assert store.get("secondary", secondary_auto_btc.symbol, runtime_key="auto") is secondary_auto_btc


def test_clear_unknown_record_is_idempotent() -> None:
    store = InMemoryQuoteStore()
    quote = _quote()
    store.set("dzengi", quote)

    store.clear(
        source_name="unknown",
        symbol="ETH/USD_LEVERAGE",
        runtime_key="missing",
    )

    assert store.get("dzengi", quote.symbol) is quote


def test_store_instances_are_independent() -> None:
    first = InMemoryQuoteStore()
    second = InMemoryQuoteStore()
    quote = _quote()

    first.set("dzengi", quote)

    assert first.get("dzengi", quote.symbol) is quote
    assert second.get("dzengi", quote.symbol) is None


def test_store_preserves_decimal_values() -> None:
    store = InMemoryQuoteStore()
    quote = _quote(
        last_price=Decimal("64159.45000001"),
    )

    store.set("dzengi", quote)
    result = store.get("dzengi", quote.symbol)

    assert result is not None
    assert result is quote
    assert isinstance(result.last_price, Decimal)
    assert result.last_price == Decimal("64159.45000001")


def test_store_preserves_datetime_values() -> None:
    store = InMemoryQuoteStore()
    quote = _quote()

    store.set("dzengi", quote)
    result = store.get("dzengi", quote.symbol)

    assert result is not None
    assert result is quote
    assert result.exchange_timestamp is quote.exchange_timestamp
    assert result.received_at is quote.received_at


def test_quote_store_error_inherits_storage_error() -> None:
    error = QuoteStoreError("test")

    assert isinstance(error, StorageError)




def test_shared_quote_store_matches_protocol() -> None:
    assert isinstance(get_quote_store(), QuoteStoreProtocol)


def test_shared_quote_store_returns_same_instance() -> None:
    assert get_quote_store() is get_quote_store()


def test_runtime_source_name_preserves_compatibility_namespace() -> None:
    assert QUOTE_RUNTIME_SOURCE_NAME == "legacy-market-price-cache"


def test_shared_quote_store_can_be_replaced_in_test(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    replacement = InMemoryQuoteStore()

    monkeypatch.setattr(
        quote_store_module,
        "_SHARED_QUOTE_STORE",
        replacement,
    )

    assert get_quote_store() is replacement
