"""Tests for production fixes committed in 1294131 and c9972a5 (v13.5.2+).

Covers two categories of fixes:
1. Trailing-edge skip in backfill.py — prevents sidecar/backfill overlap races.
2. Stathera gap classification in health_heartbeat.py — all tid_delta > 0 are real gaps.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any
from unittest.mock import MagicMock, patch

# =============================================================================
# 1. Trailing-edge skip (backfill.py)
# =============================================================================


def _make_backfill_result(**overrides: Any) -> Any:
    """Helper to import and construct a BackfillResult."""
    from opendeviationbar.backfill import BackfillResult

    defaults = {
        "symbol": "BTCUSDT",
        "threshold_decimal_bps": 250,
        "bars_written": 0,
        "gap_seconds": 0.0,
        "latest_ts_before": None,
        "latest_ts_after": None,
        "duration_seconds": 0.0,
        "error": None,
    }
    defaults.update(overrides)
    return BackfillResult(**defaults)


@patch("opendeviationbar.symbol_registry.validate_symbol_registered")
@patch("opendeviationbar.threshold.get_min_threshold_for_symbol", return_value=100)
def test_trailing_edge_skip_fresh_gap(mock_min_thr, mock_validate):
    """When gap is < 30 min old (fresh), backfill should skip (bars_written=0).

    The sidecar is likely actively processing these trades via WebSocket.
    Backfilling via REST would race with the sidecar, producing overlapping
    bars from different starting trade IDs.
    """
    from opendeviationbar.backfill import backfill_gap
    from opendeviationbar.config.backfill import BackfillConfig
    trailing_edge_min_gap_s = BackfillConfig().trailing_edge_min_gap_s

    # Verify the constant is 1800 seconds (30 minutes)
    assert trailing_edge_min_gap_s == 1800

    # latest_ts is 10 minutes ago — well within the 30-min threshold
    # CH returns microseconds after Phase 3-4 column rename (_ms → _us)
    now_us = int(datetime.now(UTC).timestamp() * 1_000_000)
    fresh_ts = now_us - (10 * 60 * 1_000_000)  # 10 minutes ago in microseconds

    mock_cache = MagicMock()
    mock_cache.get_latest_bar_with_trade_id.return_value = (fresh_ts, 12345)
    mock_cache.__enter__ = MagicMock(return_value=mock_cache)
    mock_cache.__exit__ = MagicMock(return_value=False)

    with patch("opendeviationbar.clickhouse.OpenDeviationBarCache", return_value=mock_cache):
        result = backfill_gap("BTCUSDT", threshold_decimal_bps=250)

    assert result.bars_written == 0
    assert result.error is None  # Not an error — intentional skip
    assert result.gap_seconds < trailing_edge_min_gap_s


@patch("opendeviationbar.symbol_registry.validate_symbol_registered")
@patch("opendeviationbar.threshold.get_min_threshold_for_symbol", return_value=100)
def test_trailing_edge_skip_stale_gap(mock_min_thr, mock_validate):
    """When gap is > 30 min old (stale), backfill should proceed with REST fetch.

    A stale gap indicates the sidecar missed this pair — it may be down,
    reconnecting, or the pair is not actively streaming.
    """
    from opendeviationbar.backfill import backfill_gap
    from opendeviationbar.config.backfill import BackfillConfig
    trailing_edge_min_gap_s = BackfillConfig().trailing_edge_min_gap_s

    # latest_ts is 2 hours ago — well beyond the 30-min threshold
    # CH returns microseconds after Phase 3-4 column rename (_ms → _us)
    now_us = int(datetime.now(UTC).timestamp() * 1_000_000)
    stale_ts = now_us - (2 * 60 * 60 * 1_000_000)  # 2 hours ago in microseconds

    mock_cache = MagicMock()
    mock_cache.get_latest_bar_with_trade_id.return_value = (stale_ts, 12345)
    mock_cache.__enter__ = MagicMock(return_value=mock_cache)
    mock_cache.__exit__ = MagicMock(return_value=False)

    # Mock _fetch_and_process_gap to avoid actual REST calls
    with (
        patch("opendeviationbar.clickhouse.OpenDeviationBarCache", return_value=mock_cache),
        patch("opendeviationbar.backfill._fetch_and_process_gap", return_value=5) as mock_fetch,
    ):
        result = backfill_gap("BTCUSDT", threshold_decimal_bps=250)

    # Should have proceeded to fetch (not skipped)
    mock_fetch.assert_called_once()
    assert result.gap_seconds >= trailing_edge_min_gap_s


# =============================================================================
# 2. Stathera gap classification (health_heartbeat.py)
# =============================================================================
#
# Stathera invariant: tid_delta > 0 = gap, tid_delta < 0 = overlap.
# All gaps are real and must be repaired.


def _simulate_heartbeat_gap_count(tid_delta: int) -> dict[str, str]:
    """Simulate the heartbeat's Stathera gap counting.

    Simple classification: tid_delta > 0 = gap, tid_delta < 0 = overlap.
    """
    gap_count = 1 if tid_delta > 0 else 0
    overlap_count = 1 if tid_delta < 0 else 0

    return {
        "gap_count": str(gap_count),
        "overlap_count": str(overlap_count),
    }


def test_positive_tid_delta_is_gap():
    """Any tid_delta > 0 is a gap regardless of day boundaries."""
    result = _simulate_heartbeat_gap_count(tid_delta=150000)
    assert result["gap_count"] == "1"
    assert result["overlap_count"] == "0"


def test_negative_tid_delta_is_overlap():
    """tid_delta < 0 is an overlap."""
    result = _simulate_heartbeat_gap_count(tid_delta=-500)
    assert result["gap_count"] == "0"
    assert result["overlap_count"] == "1"


def test_zero_tid_delta_is_clean():
    """tid_delta == 0 means no anomaly."""
    result = _simulate_heartbeat_gap_count(tid_delta=0)
    assert result["gap_count"] == "0"
    assert result["overlap_count"] == "0"
