# app/tests/unit/market_data/acquisition/adapters/dzengi/test_rest.py

from __future__ import annotations

import pytest

from src.integrations.exchange.exceptions import (
    ExchangeConnectionError,
    ExchangeResponseError,
)
from src.market_data.acquisition.adapters.dzengi.rest import (
    DzengiInstrumentDocumentSource,
)
from src.market_data.acquisition.exceptions import (
    InstrumentReferenceTransportError,
)
from src.market_data.acquisition.protocol import (
    InstrumentDocumentSource,
)


class StubRestClient:
    def __init__(
        self,
        *,
        result: object = None,
        error: Exception | None = None,
    ) -> None:
        self.result = result
        self.error = error
        self.calls: list[str] = []

    def get_payload(
        self,
        path: str,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
    ) -> object:
        del params
        del headers

        self.calls.append(path)

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

        return self.result


def test_source_satisfies_instrument_document_source_protocol() -> None:
    source = DzengiInstrumentDocumentSource(
        client=StubRestClient(
            result={
                "symbols": [],
            }
        )
    )

    assert isinstance(source, InstrumentDocumentSource)


def test_fetch_instrument_document_calls_exchange_info_endpoint() -> None:
    client = StubRestClient(
        result={
            "symbols": [],
        }
    )
    source = DzengiInstrumentDocumentSource(client=client)

    result = source.fetch_instrument_document()

    assert result == {
        "symbols": [],
    }
    assert client.calls == [
        "/api/v1/exchangeInfo",
    ]


def test_fetch_instrument_document_returns_dict_without_changes() -> None:
    document = {
        "timezone": "UTC",
        "serverTime": 1783537921471,
        "symbols": [
            {
                "symbol": "BTC/USD_LEVERAGE",
            }
        ],
    }

    source = DzengiInstrumentDocumentSource(
        client=StubRestClient(result=document)
    )

    result = source.fetch_instrument_document()

    assert result is document


def test_fetch_instrument_document_returns_list_without_changes() -> None:
    document = [
        {
            "symbol": "BTC/USD_LEVERAGE",
        }
    ]

    source = DzengiInstrumentDocumentSource(
        client=StubRestClient(result=document)
    )

    result = source.fetch_instrument_document()

    assert result is document


def test_source_uses_injected_client() -> None:
    client = StubRestClient(
        result={
            "symbols": [],
        }
    )
    source = DzengiInstrumentDocumentSource(client=client)

    source.fetch_instrument_document()
    source.fetch_instrument_document()

    assert client.calls == [
        "/api/v1/exchangeInfo",
        "/api/v1/exchangeInfo",
    ]


@pytest.mark.parametrize(
    "error",
    [
        ExchangeConnectionError("Network error."),
        ExchangeResponseError("Invalid response."),
        RuntimeError("Unexpected transport failure."),
    ],
)
def test_transport_errors_are_wrapped(
    error: Exception,
) -> None:
    source = DzengiInstrumentDocumentSource(
        client=StubRestClient(error=error)
    )

    with pytest.raises(
        InstrumentReferenceTransportError,
        match=r"Не удалось получить Instrument Reference Data от Dzengi",
    ) as exc_info:
        source.fetch_instrument_document()

    assert exc_info.value.__cause__ is error
    assert str(error) in str(exc_info.value)


def test_client_creation_error_is_wrapped(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    original_error = RuntimeError("EXCHANGE_BASE_URL is invalid.")

    def raise_client_creation_error() -> None:
        raise original_error

    monkeypatch.setattr(
        "src.market_data.acquisition.adapters.dzengi.rest.ExchangeRestClient",
        raise_client_creation_error,
    )

    source = DzengiInstrumentDocumentSource()

    with pytest.raises(
        InstrumentReferenceTransportError,
        match=r"Не удалось получить Instrument Reference Data от Dzengi",
    ) as exc_info:
        source.fetch_instrument_document()

    assert exc_info.value.__cause__ is original_error


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

    source = DzengiInstrumentDocumentSource(
        client=StubRestClient(result=document)
    )

    result = source.fetch_instrument_document()

    assert result is document
    assert result == {
        "status": "OK",
        "payload": {
            "symbols": [],
        },
    }

# Quotes Feed REST source tests.
from src.market_data.acquisition.adapters.dzengi.rest import (
    DzengiQuoteDocumentSource,
)
from src.market_data.acquisition.exceptions import QuoteTransportError
from src.market_data.acquisition.protocol import QuoteDocumentSource


class RecordingQuoteRestClient:
    def __init__(
        self,
        *,
        result: object = None,
        error: Exception | None = None,
    ) -> None:
        self.result = result
        self.error = error
        self.calls: list[dict[str, object]] = []

    def get_payload(
        self,
        path: str,
        params: dict[str, str] | None = None,
        headers: dict[str, str] | None = None,
    ) -> object:
        self.calls.append(
            {
                "path": path,
                "params": params,
                "headers": headers,
            }
        )

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

        return self.result


def test_quote_source_satisfies_quote_document_source_protocol() -> None:
    source = DzengiQuoteDocumentSource(
        client=RecordingQuoteRestClient(result={})
    )

    assert isinstance(source, QuoteDocumentSource)


def test_fetch_quote_document_calls_ticker_endpoint_with_symbol() -> None:
    client = RecordingQuoteRestClient(result={})
    source = DzengiQuoteDocumentSource(client=client)

    source.fetch_quote_document("BTC/USD_LEVERAGE")

    assert client.calls == [
        {
            "path": "/api/v1/ticker/24hr",
            "params": {
                "symbol": "BTC/USD_LEVERAGE",
            },
            "headers": None,
        }
    ]


def test_fetch_quote_document_passes_symbol_without_changes() -> None:
    client = RecordingQuoteRestClient(result={})
    source = DzengiQuoteDocumentSource(client=client)

    source.fetch_quote_document("  btc/usd_leverage  ")

    assert client.calls[0]["params"] == {
        "symbol": "  btc/usd_leverage  ",
    }


def test_fetch_quote_document_returns_payload_without_changes() -> None:
    document = {
        "symbol": "BTC/USD_LEVERAGE",
        "lastPrice": "64159.45",
        "bidPrice": "64159.45",
        "askPrice": "64159.55",
        "closeTime": 1783887270312,
    }
    source = DzengiQuoteDocumentSource(
        client=RecordingQuoteRestClient(result=document)
    )

    result = source.fetch_quote_document("BTC/USD_LEVERAGE")

    assert result is document


def test_quote_source_uses_injected_client_once() -> None:
    client = RecordingQuoteRestClient(result={})
    source = DzengiQuoteDocumentSource(client=client)

    source.fetch_quote_document("BTC/USD_LEVERAGE")

    assert len(client.calls) == 1


def test_quote_source_creates_default_client(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    client = RecordingQuoteRestClient(result={})
    client_creation_count = 0

    def create_client() -> RecordingQuoteRestClient:
        nonlocal client_creation_count
        client_creation_count += 1
        return client

    monkeypatch.setattr(
        "src.market_data.acquisition.adapters.dzengi.rest.ExchangeRestClient",
        create_client,
    )

    source = DzengiQuoteDocumentSource()
    source.fetch_quote_document("BTC/USD_LEVERAGE")

    assert client_creation_count == 1
    assert len(client.calls) == 1


@pytest.mark.parametrize(
    "error",
    [
        ExchangeConnectionError("Network error."),
        ExchangeResponseError("Invalid response."),
        RuntimeError("Unexpected transport failure."),
    ],
)
def test_quote_transport_errors_are_wrapped(
    error: Exception,
) -> None:
    source = DzengiQuoteDocumentSource(
        client=RecordingQuoteRestClient(error=error)
    )

    with pytest.raises(
        QuoteTransportError,
        match=r"Не удалось получить текущую котировку от Dzengi",
    ) as exc_info:
        source.fetch_quote_document("BTC/USD_LEVERAGE")

    assert exc_info.value.__cause__ is error
    assert str(error) in str(exc_info.value)


def test_quote_client_creation_error_is_wrapped(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    original_error = RuntimeError("EXCHANGE_BASE_URL is invalid.")

    def raise_client_creation_error() -> None:
        raise original_error

    monkeypatch.setattr(
        "src.market_data.acquisition.adapters.dzengi.rest.ExchangeRestClient",
        raise_client_creation_error,
    )

    source = DzengiQuoteDocumentSource()

    with pytest.raises(QuoteTransportError) as exc_info:
        source.fetch_quote_document("BTC/USD_LEVERAGE")

    assert exc_info.value.__cause__ is original_error
