# app/tests/unit/market_data/acquisition/feeds/test_candles_feed.py

from __future__ import annotations

from datetime import datetime, timezone
from decimal import Decimal

import pytest

from src.market_data.acquisition.feeds.candles_feed import CandlesFeed
from src.market_data.acquisition.models.candle import Candle


def _candle() -> Candle:
    return Candle(
        symbol="BTC/USD",
        interval="1m",
        open_time=datetime(2026, 7, 15, 6, 0, tzinfo=timezone.utc),
        open_price=Decimal("100"),
        high_price=Decimal("110"),
        low_price=Decimal("90"),
        close_price=Decimal("105"),
        volume=Decimal("12"),
        source="rest_klines:bid",
    )


class StubCandlesDocumentSource:
    def __init__(
        self,
        *,
        document: object,
        error: Exception | None = None,
    ) -> None:
        self.document = document
        self.error = error
        self.calls: list[tuple[str, str, int, str]] = []

    def fetch_candles_document(
        self,
        symbol: str,
        *,
        interval: str,
        limit: int,
        price_type: str,
    ) -> object:
        self.calls.append((symbol, interval, limit, price_type))

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

        return self.document


class StubCandlesDocumentHandler:
    def __init__(
        self,
        *,
        candles: tuple[Candle, ...],
        error: Exception | None = None,
    ) -> None:
        self.candles = candles
        self.error = error
        self.calls: list[tuple[object, str, str, str]] = []

    def handle_candles_document(
        self,
        document: object,
        *,
        symbol: str,
        interval: str,
        source: str,
    ) -> tuple[Candle, ...]:
        self.calls.append((document, symbol, interval, source))

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

        return self.candles


def test_load_candles_coordinates_source_and_handler() -> None:
    document = {"klines": []}
    candles = (_candle(),)
    source = StubCandlesDocumentSource(document=document)
    handler = StubCandlesDocumentHandler(candles=candles)
    feed = CandlesFeed(source=source, handler=handler)

    result = feed.load_candles(
        "BTC/USD",
        interval="1m",
        limit=100,
        price_type="bid",
    )

    assert result is candles
    assert source.calls == [("BTC/USD", "1m", 100, "bid")]
    assert handler.calls == [
        (
            document,
            "BTC/USD",
            "1m",
            "rest_klines:bid",
        )
    ]


def test_load_candles_does_not_trim_handler_result() -> None:
    candles = (_candle(), _candle())
    source = StubCandlesDocumentSource(document=[])
    handler = StubCandlesDocumentHandler(candles=candles)
    feed = CandlesFeed(source=source, handler=handler)

    result = feed.load_candles(
        "BTC/USD",
        interval="1m",
        limit=1,
        price_type="ask",
    )

    assert result is candles
    assert len(result) == 2


def test_load_candles_propagates_source_error() -> None:
    original_error = RuntimeError("source failed")
    source = StubCandlesDocumentSource(
        document=None,
        error=original_error,
    )
    handler = StubCandlesDocumentHandler(candles=())
    feed = CandlesFeed(source=source, handler=handler)

    with pytest.raises(RuntimeError, match="source failed") as error_info:
        feed.load_candles(
            "BTC/USD",
            interval="1m",
            limit=100,
            price_type="bid",
        )

    assert error_info.value is original_error
    assert handler.calls == []


def test_load_candles_propagates_handler_error() -> None:
    original_error = ValueError("handler failed")
    document = {"klines": []}
    source = StubCandlesDocumentSource(document=document)
    handler = StubCandlesDocumentHandler(
        candles=(),
        error=original_error,
    )
    feed = CandlesFeed(source=source, handler=handler)

    with pytest.raises(ValueError, match="handler failed") as error_info:
        feed.load_candles(
            "BTC/USD",
            interval="5m",
            limit=50,
            price_type="ask",
        )

    assert error_info.value is original_error
