"""Tests for exchange gap floor clamping in sidecar resume TID calculation.

The sidecar clamps resume TIDs above known exchange gaps so fill_from_rest
never tries to fill trade ID ranges where the exchange had no trades.
"""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest
from opendeviationbar.sidecar import _clamp_resume_tid_above_exchange_gaps


def _make_mock_cache(result_rows: list[tuple]) -> MagicMock:
    """Create a mock ch_cache with a client.query() that returns result_rows."""
    mock_cache = MagicMock()
    mock_result = MagicMock()
    mock_result.result_rows = result_rows
    mock_cache.client.query.return_value = mock_result
    return mock_cache


class TestClampResumeAboveExchangeGap:
    """Test _clamp_resume_tid_above_exchange_gaps()."""

    def test_clamp_above_exchange_gap(self) -> None:
        """When exchange_gap ariadne_from_id=5000 and resume_tid=4000, clamp to 5000."""
        cache = _make_mock_cache([("BTCUSDT", 5000)])
        resume_tids = {"BTCUSDT": 4000}

        result = _clamp_resume_tid_above_exchange_gaps(cache, resume_tids)

        assert result["BTCUSDT"] == 5000

    def test_no_exchange_gaps_no_change(self) -> None:
        """When no exchange_gap entries exist, resume_tid is unchanged."""
        cache = _make_mock_cache([])
        resume_tids = {"BTCUSDT": 4000}

        result = _clamp_resume_tid_above_exchange_gaps(cache, resume_tids)

        assert result["BTCUSDT"] == 4000

    def test_resume_tid_already_above_gap(self) -> None:
        """When resume_tid=6000 > ariadne_from_id=5000, no change (max semantics)."""
        cache = _make_mock_cache([("BTCUSDT", 5000)])
        resume_tids = {"BTCUSDT": 6000}

        result = _clamp_resume_tid_above_exchange_gaps(cache, resume_tids)

        assert result["BTCUSDT"] == 6000

    def test_multiple_exchange_gaps_uses_highest(self) -> None:
        """SQL GROUP BY + max() returns highest per symbol. Verify max is used."""
        # The SQL query uses max(ariadne_from_id) GROUP BY symbol,
        # so the mock returns the pre-aggregated result.
        cache = _make_mock_cache([("BTCUSDT", 5000)])
        resume_tids = {"BTCUSDT": 3000}

        result = _clamp_resume_tid_above_exchange_gaps(cache, resume_tids)

        assert result["BTCUSDT"] == 5000

    def test_per_symbol_clamping(self) -> None:
        """Exchange gap for BTCUSDT does not affect ETHUSDT resume_tid."""
        cache = _make_mock_cache([("BTCUSDT", 5000)])
        resume_tids = {"BTCUSDT": 4000, "ETHUSDT": 3000}

        result = _clamp_resume_tid_above_exchange_gaps(cache, resume_tids)

        assert result["BTCUSDT"] == 5000
        assert result["ETHUSDT"] == 3000  # unaffected

    def test_query_failure_logs_warning(self, caplog: pytest.LogCaptureFixture) -> None:
        """If kintsugi_log query fails, log warning and return unchanged."""
        mock_cache = MagicMock()
        mock_cache.client.query.side_effect = ConnectionError("CH unreachable")
        resume_tids = {"BTCUSDT": 4000}

        with caplog.at_level("WARNING"):
            result = _clamp_resume_tid_above_exchange_gaps(mock_cache, resume_tids)

        assert result["BTCUSDT"] == 4000  # unchanged
        assert any("exchange gap" in r.message.lower() for r in caplog.records)

    def test_none_resume_tid_not_clamped(self) -> None:
        """Symbols with resume_tid=None are not clamped (no comparison possible)."""
        cache = _make_mock_cache([("BTCUSDT", 5000)])
        resume_tids: dict[str, int | None] = {"BTCUSDT": None}

        result = _clamp_resume_tid_above_exchange_gaps(cache, resume_tids)

        assert result["BTCUSDT"] is None
