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

from __future__ import annotations

from collections.abc import Iterator
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.models import SymbolValidationResult
from src.integrations.exchange.service import ExchangeService
from src.integrations.exchange.status import (
    ExchangeRuntimeStatus,
    ExchangeStatusCode,
)
from src.market_data.acquisition.models.instrument import Instrument
from src.market_data.acquisition.models.quote import Quote


def _make_settings(
    *,
    exchange_enabled: bool = True,
    default_symbol: str = "BTC/USD",
) -> Settings:
    return cast(
        Settings,
        SimpleNamespace(
            exchange_enabled=exchange_enabled,
            default_symbol=default_symbol,
            exchange_name="dzengi",
            exchange_base_url="https://demo-api-adapter.dzengi.com",
            tz="UTC",
        ),
    )


def _quote_for_status(
    symbol: str,
    *,
    age_seconds: float | None,
) -> Quote:
    now = datetime.now(timezone.utc)

    exchange_timestamp = (
        now - timedelta(seconds=age_seconds)
        if age_seconds is not None
        else None
    )

    return Quote(
        symbol=symbol,
        last_price=Decimal("100.0"),
        bid_price=Decimal("99.0"),
        ask_price=Decimal("101.0"),
        exchange_timestamp=exchange_timestamp,
        received_at=now,
        source="test",
    )


def _make_instrument(
    *,
    symbol: str = "BTC/USD",
    status: str = "TRADING",
) -> Instrument:
    return Instrument(
        symbol=symbol,
        name=symbol,
        status=status,
        base_asset="BTC",
        quote_asset="USD",
        asset_type="CRYPTOCURRENCY",
        market_type="unknown",
        market_modes=(),
        order_types=(),
        base_asset_precision=None,
        quote_asset_precision=None,
        tick_size=None,
        tick_value=None,
        step_size=None,
        min_qty=None,
        max_qty=None,
        min_notional=None,
        country=None,
        sector=None,
        industry=None,
        trading_hours=None,
    )


def _make_valid_validation(
    *,
    symbol: str = "BTC/USD",
    status: str = "TRADING",
) -> SymbolValidationResult:
    symbol_info = _make_instrument(
        symbol=symbol,
        status=status,
    )

    return SymbolValidationResult(
        requested_symbol=symbol,
        normalized_symbol=symbol,
        is_valid=True,
        message="Символ найден в exchangeInfo.",
        symbol_info=symbol_info,
    )


def _make_invalid_validation(
    *,
    symbol: str = "UNKNOWN/USD",
    message: str | None = None,
) -> SymbolValidationResult:
    return SymbolValidationResult(
        requested_symbol=symbol,
        normalized_symbol=symbol,
        is_valid=False,
        message=message or f"Символ '{symbol}' не найден в exchangeInfo.",
        symbol_info=None,
    )


@pytest.fixture
def service(
    monkeypatch: pytest.MonkeyPatch,
) -> Iterator[ExchangeService]:
    monkeypatch.setattr(
        service_module,
        "load_settings",
        lambda: _make_settings(),
    )

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

    yield ExchangeService()


def test_get_symbol_runtime_status_returns_mock_status_when_exchange_disabled(
    service: ExchangeService,
) -> None:
    service.settings = _make_settings(
        exchange_enabled=False,
        default_symbol="ETH/USD",
    )

    result = service.get_symbol_runtime_status()

    assert isinstance(result, ExchangeRuntimeStatus)
    assert result.code == ExchangeStatusCode.OPEN
    assert result.is_open is True
    assert result.is_available is True
    assert result.is_auth_ok is True
    assert result.reason == "mock_exchange"
    assert result.symbol == "ETH/USD"
    assert result.raw_status == "OPEN"


def test_get_symbol_runtime_status_uses_explicit_symbol(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    captured_symbols: list[str] = []

    def fake_validate_symbol(
        raw_symbol: str,
    ) -> SymbolValidationResult:
        captured_symbols.append(raw_symbol)

        return _make_invalid_validation(
            symbol=raw_symbol,
        )

    monkeypatch.setattr(
        service,
        "validate_symbol",
        fake_validate_symbol,
    )

    result = service.get_symbol_runtime_status("ETH/USD")

    assert captured_symbols == ["ETH/USD"]
    assert result.code == ExchangeStatusCode.INVALID_SYMBOL
    assert result.symbol == "ETH/USD"


def test_get_symbol_runtime_status_uses_default_symbol_when_symbol_is_none(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    service.settings = _make_settings(
        default_symbol="ETH/USD",
    )

    captured_symbols: list[str] = []

    def fake_validate_symbol(
        raw_symbol: str,
    ) -> SymbolValidationResult:
        captured_symbols.append(raw_symbol)

        return _make_invalid_validation(
            symbol=raw_symbol,
        )

    monkeypatch.setattr(
        service,
        "validate_symbol",
        fake_validate_symbol,
    )

    result = service.get_symbol_runtime_status()

    assert captured_symbols == ["ETH/USD"]
    assert result.symbol == "ETH/USD"


def test_get_symbol_runtime_status_returns_invalid_symbol_status(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    validation = _make_invalid_validation(
        symbol="UNKNOWN/USD",
        message="Проверочное сообщение о недоступном инструменте.",
    )

    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda raw_symbol: validation,
    )

    result = service.get_symbol_runtime_status("UNKNOWN/USD")

    assert result.code == ExchangeStatusCode.INVALID_SYMBOL
    assert result.is_open is False
    assert result.is_available is True
    assert result.is_auth_ok is True
    assert result.reason == "invalid_symbol"
    assert result.symbol == "UNKNOWN/USD"
    assert result.message == (
        "Проверочное сообщение о недоступном инструменте."
    )


def test_get_symbol_runtime_status_returns_exchange_error_when_validation_fails(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    error = ExchangeError("timeout while loading exchangeInfo")

    def fake_validate_symbol(
        raw_symbol: str,
    ) -> SymbolValidationResult:
        raise error

    logged_errors: list[dict[str, object]] = []

    monkeypatch.setattr(
        service,
        "validate_symbol",
        fake_validate_symbol,
    )

    monkeypatch.setattr(
        service,
        "_log_exchange_error",
        lambda **kwargs: logged_errors.append(kwargs),
    )

    result = service.get_symbol_runtime_status("BTC/USD")

    assert result.code == ExchangeStatusCode.EXCHANGE_UNAVAILABLE
    assert result.is_open is False
    assert result.is_available is False
    assert result.reason == "exchange_unavailable"
    assert result.raw_error == str(error)

    assert logged_errors == [
        {
            "endpoint": "symbol_market_status",
            "exc": error,
            "symbol": "BTC/USD",
        }
    ]


@pytest.mark.parametrize(
    ("raw_status", "expected_code", "expected_reason"),
    [
        ("TRADING", ExchangeStatusCode.OPEN, "market_open"),
        ("OPEN", ExchangeStatusCode.OPEN, "market_open"),
        ("ACTIVE", ExchangeStatusCode.OPEN, "market_open"),
        ("ENABLED", ExchangeStatusCode.OPEN, "market_open"),
        ("ONLINE", ExchangeStatusCode.OPEN, "market_open"),
        ("BREAK", ExchangeStatusCode.BREAK, "market_break"),
        ("CLOSED", ExchangeStatusCode.BREAK, "market_break"),
        ("HALT", ExchangeStatusCode.BREAK, "market_break"),
        ("HALTED", ExchangeStatusCode.BREAK, "market_break"),
        ("PAUSED", ExchangeStatusCode.BREAK, "market_break"),
        ("SUSPENDED", ExchangeStatusCode.BREAK, "market_break"),
        ("DISABLED", ExchangeStatusCode.BREAK, "market_break"),
        ("SETTLING", ExchangeStatusCode.BREAK, "market_break"),
        ("POST_ONLY", ExchangeStatusCode.BREAK, "market_break"),
        (
            "NOT_TRADABLE",
            ExchangeStatusCode.BREAK,
            "market_not_tradable",
        ),
        (
            "TRADING_DISABLED",
            ExchangeStatusCode.BREAK,
            "market_not_tradable",
        ),
        (
            "MARKET_DISABLED",
            ExchangeStatusCode.BREAK,
            "market_not_tradable",
        ),
        (
            "UNAVAILABLE_FOR_TRADING",
            ExchangeStatusCode.BREAK,
            "market_not_tradable",
        ),
        (
            "CLOSE_ONLY",
            ExchangeStatusCode.BREAK,
            "market_not_tradable",
        ),
        (
            "REDUCE_ONLY",
            ExchangeStatusCode.BREAK,
            "market_not_tradable",
        ),
        (
            "VIEW_ONLY",
            ExchangeStatusCode.BREAK,
            "market_not_tradable",
        ),
        (
            "UNRECOGNIZED_STATUS",
            ExchangeStatusCode.UNKNOWN,
            "market_status_unknown",
        ),
    ],
)
def test_get_symbol_runtime_status_preserves_status_classification(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
    raw_status: str,
    expected_code: ExchangeStatusCode,
    expected_reason: str,
) -> None:
    validation = _make_valid_validation(
        status=raw_status,
    )

    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda raw_symbol: validation,
    )

    monkeypatch.setattr(
        service,
        "_get_fresh_quote",
        lambda symbol: _quote_for_status(
            symbol,
            age_seconds=0.0,
        ),
    )

    result = service.get_symbol_runtime_status("BTC/USD")

    assert isinstance(result, ExchangeRuntimeStatus)
    assert result.code == expected_code
    assert result.reason == expected_reason
    assert result.symbol == "BTC/USD"


def test_get_symbol_runtime_status_checks_freshness_only_for_open_market(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    validation = _make_valid_validation(
        status="BREAK",
    )

    quote_calls: list[str] = []

    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda raw_symbol: validation,
    )

    monkeypatch.setattr(
        service,
        "_get_fresh_quote",
        lambda symbol: quote_calls.append(symbol),
    )

    result = service.get_symbol_runtime_status("BTC/USD")

    assert result.code == ExchangeStatusCode.BREAK
    assert result.reason == "market_break"
    assert quote_calls == []


def test_get_symbol_runtime_status_returns_stale_status_for_old_market_data(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    validation = _make_valid_validation(
        status="TRADING",
    )

    fixed_timestamp = datetime(
        2026,
        7,
        10,
        12,
        0,
        0,
        tzinfo=timezone.utc,
    )

    quote = Quote(
        symbol="BTC/USD",
        last_price=Decimal("100.0"),
        bid_price=Decimal("99.0"),
        ask_price=Decimal("101.0"),
        exchange_timestamp=fixed_timestamp,
        received_at=fixed_timestamp,
        source="test",
    )

    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda raw_symbol: validation,
    )

    monkeypatch.setattr(
        service,
        "_get_fresh_quote",
        lambda symbol: quote,
    )

    monkeypatch.setattr(
        service,
        "_exchange_timestamp_age_seconds",
        lambda raw_timestamp: 61.0,
    )

    result = service.get_symbol_runtime_status("BTC/USD")

    assert result.code == ExchangeStatusCode.BREAK
    assert result.is_open is False
    assert result.is_available is True
    assert result.reason == "market_data_stale"
    assert result.raw_status == "STALE_MARKET_DATA"
    assert result.symbol == "BTC/USD"
    assert "61с" in result.message
    assert "10.07.2026 12:00:00" in result.message


def test_get_symbol_runtime_status_keeps_open_status_at_stale_threshold(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    validation = _make_valid_validation(
        status="TRADING",
    )

    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda raw_symbol: validation,
    )

    monkeypatch.setattr(
        service,
        "_get_fresh_quote",
        lambda symbol: _quote_for_status(
            symbol,
            age_seconds=0.0,
        ),
    )

    monkeypatch.setattr(
        service,
        "_exchange_timestamp_age_seconds",
        lambda raw_timestamp: 60.0,
    )

    result = service.get_symbol_runtime_status("BTC/USD")

    assert result.code == ExchangeStatusCode.OPEN
    assert result.is_open is True
    assert result.reason == "market_open"


def test_get_symbol_runtime_status_keeps_open_status_when_age_is_missing(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    validation = _make_valid_validation(
        status="TRADING",
    )

    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda raw_symbol: validation,
    )

    monkeypatch.setattr(
        service,
        "_get_fresh_quote",
        lambda symbol: _quote_for_status(
            symbol,
            age_seconds=None,
        ),
    )

    result = service.get_symbol_runtime_status("BTC/USD")

    assert result.code == ExchangeStatusCode.OPEN
    assert result.is_open is True
    assert result.reason == "market_open"


def test_get_symbol_runtime_status_keeps_open_status_when_quote_fails(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    validation = _make_valid_validation(
        status="TRADING",
    )

    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda raw_symbol: validation,
    )

    def fake_get_fresh_quote(
        symbol: str,
    ) -> Quote:
        raise ExchangeError("ticker unavailable")

    monkeypatch.setattr(
        service,
        "_get_fresh_quote",
        fake_get_fresh_quote,
    )

    result = service.get_symbol_runtime_status("BTC/USD")

    assert result.code == ExchangeStatusCode.OPEN
    assert result.is_open is True
    assert result.reason == "market_open"


def test_get_symbol_runtime_status_uses_normalized_matched_symbol(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    symbol_info = _make_instrument(
        symbol="btc/usd",
        status="BREAK",
    )

    validation = SymbolValidationResult(
        requested_symbol="BTC/USD",
        normalized_symbol="BTC/USD",
        is_valid=True,
        message="Символ найден в exchangeInfo.",
        symbol_info=symbol_info,
    )

    monkeypatch.setattr(
        service,
        "validate_symbol",
        lambda raw_symbol: validation,
    )

    result = service.get_symbol_runtime_status("btc/usd")

    assert result.symbol == "BTC/USD"
    assert result.raw_status == "BREAK"


def test_get_symbol_market_status_preserves_legacy_dict_contract(
    service: ExchangeService,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime_status = ExchangeRuntimeStatus(
        code=ExchangeStatusCode.BREAK,
        is_open=False,
        is_available=True,
        is_auth_ok=True,
        title="Перерыв в торгах",
        message="Торги временно остановлены.",
        ui_line="⏸️ Перерыв в торгах",
        reason="market_break",
        symbol="BTC/USD",
        raw_status="BREAK",
        raw_error=None,
    )

    monkeypatch.setattr(
        service,
        "get_symbol_runtime_status",
        lambda symbol=None: runtime_status,
    )

    result = service.get_symbol_market_status("BTC/USD")

    assert result == runtime_status.as_dict()

    assert result == {
        "code": "BREAK",
        "status": "BREAK",
        "symbol": "BTC/USD",
        "is_open": False,
        "is_available": True,
        "is_auth_ok": True,
        "title": "Перерыв в торгах",
        "message": "Торги временно остановлены.",
        "ui_line": "⏸️ Перерыв в торгах",
        "reason": "market_break",
        "raw_status": "BREAK",
        "raw_error": None,
    }