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

from __future__ import annotations

from datetime import datetime, timedelta, timezone
from decimal import Decimal
from types import SimpleNamespace
from typing import cast

import pytest

import src.integrations.exchange.service as service_module
from src.core.config import Settings
from src.integrations.exchange.exceptions import ExchangeError
from src.integrations.exchange.service import ExchangeService
from src.market_data.acquisition.models.quote import Quote
import src.storage.quote_store as quote_store_module
from src.storage.quote_store import (
    InMemoryQuoteStore,
    QUOTE_RUNTIME_SOURCE_NAME,
    get_quote_store,
)


def _service(*, exchange_enabled: bool = True) -> ExchangeService:
    service = object.__new__(ExchangeService)
    service.settings = cast(
        Settings,
        SimpleNamespace(
            exchange_enabled=exchange_enabled,
            default_symbol="BTC/USD_LEVERAGE",
        ),
    )
    return service


def _quote(*, age_seconds: float = 0.0) -> Quote:
    return 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)
            - timedelta(seconds=age_seconds)
        ),
        source="test",
    )


@pytest.fixture(autouse=True)
def reset_store(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(
        quote_store_module,
        "_SHARED_QUOTE_STORE",
        InMemoryQuoteStore(),
    )
    yield


def test_get_quote_returns_fresh_cached_identity(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    quote = _quote()
    get_quote_store().set(
        QUOTE_RUNTIME_SOURCE_NAME,quote, runtime_key="auto")
    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda _: SimpleNamespace(
            is_valid=True,
            normalized_symbol=quote.symbol,
            message="",
        ),
    )
    monkeypatch.setattr(
        service,
        "_get_fresh_quote",
        lambda _: (_ for _ in ()).throw(AssertionError("REST not expected")),
    )

    assert service.get_quote(quote.symbol, runtime_key="auto") is quote


def test_get_quote_refreshes_stale_quote(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    stale = _quote(age_seconds=10)
    fresh = _quote()
    get_quote_store().set(
        QUOTE_RUNTIME_SOURCE_NAME,stale, runtime_key="auto")
    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda _: SimpleNamespace(
            is_valid=True,
            normalized_symbol=fresh.symbol,
            message="",
        ),
    )
    monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh)

    result = service.get_quote(fresh.symbol, runtime_key="auto")

    assert result is fresh
    assert get_quote_store().get(
            QUOTE_RUNTIME_SOURCE_NAME,fresh.symbol, runtime_key="auto") is fresh


def test_get_quote_wraps_acquisition_error(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    original = RuntimeError("unavailable")
    monkeypatch.setattr(
        service,
        "_load_quote_via_acquisition",
        lambda _: (_ for _ in ()).throw(original),
    )
    monkeypatch.setattr(service, "_log_exchange_error", lambda **_: None)

    with pytest.raises(ExchangeError) as exc_info:
        service._get_fresh_quote("BTC/USD_LEVERAGE")

    assert exc_info.value.__cause__ is original


def test_refresh_quote_cache_forces_fresh_quote_and_preserves_identity(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    cached = _quote()
    fresh = _quote()
    get_quote_store().set(
        QUOTE_RUNTIME_SOURCE_NAME,cached, runtime_key="auto")
    requested_symbols: list[str] = []

    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda symbol: SimpleNamespace(
            is_valid=True,
            normalized_symbol=(requested_symbols.append(symbol) or fresh.symbol),
            message="",
        ),
    )
    monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh)

    result = service.refresh_quote_cache(
        " btc/usd_leverage ",
        runtime_key="auto",
    )

    assert requested_symbols == [" btc/usd_leverage "]
    assert result is fresh
    assert result is not cached
    assert get_quote_store().get(
            QUOTE_RUNTIME_SOURCE_NAME,fresh.symbol, runtime_key="auto") is fresh


def test_refresh_quote_cache_uses_default_runtime_key(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    fresh = _quote()
    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda _: SimpleNamespace(
            is_valid=True,
            normalized_symbol=fresh.symbol,
            message="",
        ),
    )
    monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh)

    result = service.refresh_quote_cache(fresh.symbol)

    assert result is fresh
    assert get_quote_store().get(
            QUOTE_RUNTIME_SOURCE_NAME,fresh.symbol, runtime_key="auto") is fresh


def test_refresh_quote_cache_normalizes_runtime_key(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    fresh = _quote()
    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda _: SimpleNamespace(
            is_valid=True,
            normalized_symbol=fresh.symbol,
            message="",
        ),
    )
    monkeypatch.setattr(service, "_get_fresh_quote", lambda _: fresh)

    result = service.refresh_quote_cache(
        fresh.symbol,
        runtime_key=" Debug_Auto ",
    )

    assert result is fresh
    assert (
        get_quote_store().get(
            QUOTE_RUNTIME_SOURCE_NAME,
            fresh.symbol,
            runtime_key="debug_auto",
        )
        is fresh
    )


def test_refresh_quote_cache_rejects_invalid_symbol(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda _: SimpleNamespace(
            is_valid=False,
            normalized_symbol="UNKNOWN/USD",
            message="Символ не найден.",
        ),
    )
    monkeypatch.setattr(
        service,
        "_get_fresh_quote",
        lambda _: (_ for _ in ()).throw(
            AssertionError("Fresh Quote must not be requested.")
        ),
    )

    with pytest.raises(ExchangeError, match="Символ не найден"):
        service.refresh_quote_cache("UNKNOWN/USD", runtime_key="auto")


def test_refresh_quote_cache_does_not_update_cache_on_acquisition_error(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service()
    cached = _quote()
    original = RuntimeError("unavailable")
    get_quote_store().set(
        QUOTE_RUNTIME_SOURCE_NAME,cached, runtime_key="auto")
    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda _: SimpleNamespace(
            is_valid=True,
            normalized_symbol=cached.symbol,
            message="",
        ),
    )
    monkeypatch.setattr(
        service,
        "_load_quote_via_acquisition",
        lambda _: (_ for _ in ()).throw(original),
    )
    monkeypatch.setattr(service, "_log_exchange_error", lambda **_: None)

    with pytest.raises(ExchangeError) as exc_info:
        service.refresh_quote_cache(cached.symbol, runtime_key="auto")

    assert exc_info.value.__cause__ is original
    assert get_quote_store().get(
            QUOTE_RUNTIME_SOURCE_NAME,cached.symbol, runtime_key="auto") is cached


def test_refresh_quote_cache_uses_and_stores_mock_quote(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service = _service(exchange_enabled=False)
    mock = _quote()
    monkeypatch.setattr(service_module, "mock_quote", lambda _: mock)
    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda _: (_ for _ in ()).throw(
            AssertionError("Validation is not expected in mock mode.")
        ),
    )
    monkeypatch.setattr(
        service,
        "_get_fresh_quote",
        lambda _: (_ for _ in ()).throw(
            AssertionError("REST is not expected in mock mode.")
        ),
    )

    result = service.refresh_quote_cache(
        "BTC/USD_LEVERAGE",
        runtime_key="debug_auto",
    )

    assert result is mock
    assert (
        get_quote_store().get(
            QUOTE_RUNTIME_SOURCE_NAME,
            mock.symbol,
            runtime_key="debug_auto",
        )
        is mock
    )

