"""Tests for yesterday completion (TELEM-02) and junction telemetry (TELEM-03)."""

from __future__ import annotations

import types
from dataclasses import dataclass
from datetime import date
from unittest.mock import MagicMock, patch


def _make_client(queries: dict[str, list[list]]) -> MagicMock:
    """Create a mock ClickHouse client returning canned results per query substring."""
    client = MagicMock()

    def _query(sql: str, **_kw: object) -> MagicMock:
        result = MagicMock()
        for substr, rows in queries.items():
            if substr in sql:
                result.result_rows = rows
                return result
        result.result_rows = []
        return result

    client.query.side_effect = _query
    return client


def _make_symbol_entry(
    symbol: str,
    exchange: str = "binance",
    market: str = "spot",
    enabled: bool = True,
    asset_class: str = "crypto",
) -> object:
    """Create a minimal SymbolEntry-like object for mocking get_symbol_entries."""

    @dataclass(frozen=True)
    class FakeSymbolEntry:
        symbol: str
        enabled: bool
        asset_class: str
        exchange: str
        market: str
        listing_date: date = date(2020, 1, 1)

    return FakeSymbolEntry(
        symbol=symbol,
        enabled=enabled,
        asset_class=asset_class,
        exchange=exchange,
        market=market,
    )


def _mock_symbol_entries() -> types.MappingProxyType:
    """Return a mixed registry with Binance spot + Exness forex entries."""
    entries = {
        "BTCUSDT": _make_symbol_entry("BTCUSDT", "binance", "spot"),
        "ETHUSDT": _make_symbol_entry("ETHUSDT", "binance", "spot"),
        "SOLUSDT": _make_symbol_entry("SOLUSDT", "binance", "spot"),
        "ADAUSDT": _make_symbol_entry("ADAUSDT", "binance", "spot"),
        "EURUSD": _make_symbol_entry("EURUSD", "exness", "raw_spread", asset_class="forex"),
        "GBPUSD": _make_symbol_entry("GBPUSD", "exness", "raw_spread", asset_class="forex"),
        "DISABLED": _make_symbol_entry("DISABLED", "binance", "spot", enabled=False),
    }
    return types.MappingProxyType(entries)


class TestGetHeartbeatSymbols:
    """Tests for get_heartbeat_symbols() dynamic registry lookup."""

    def test_returns_only_binance_spot(self):
        """get_heartbeat_symbols() filters to Binance spot only, excluding Exness forex."""
        with patch(
            "opendeviationbar.symbol_registry.get_symbol_entries",
            return_value=_mock_symbol_entries(),
        ):
            from heartbeat.config import get_heartbeat_symbols

            get_heartbeat_symbols.cache_clear()
            symbols = get_heartbeat_symbols()

        # Should include Binance spot, exclude Exness forex and disabled
        assert "BTCUSDT" in symbols
        assert "ETHUSDT" in symbols
        assert "SOLUSDT" in symbols
        assert "ADAUSDT" in symbols
        assert "EURUSD" not in symbols
        assert "GBPUSD" not in symbols
        assert "DISABLED" not in symbols

    def test_returns_all_enabled_binance_spot(self):
        """get_heartbeat_symbols() returns all enabled Binance spot symbols (not just 2)."""
        with patch(
            "opendeviationbar.symbol_registry.get_symbol_entries",
            return_value=_mock_symbol_entries(),
        ):
            from heartbeat.config import get_heartbeat_symbols

            get_heartbeat_symbols.cache_clear()
            symbols = get_heartbeat_symbols()

        # 4 enabled Binance spot in mock, not just BTC+ETH
        assert len(symbols) == 4

    def test_returns_sorted_tuple(self):
        """get_heartbeat_symbols() returns a sorted tuple."""
        with patch(
            "opendeviationbar.symbol_registry.get_symbol_entries",
            return_value=_mock_symbol_entries(),
        ):
            from heartbeat.config import get_heartbeat_symbols

            get_heartbeat_symbols.cache_clear()
            symbols = get_heartbeat_symbols()

        assert isinstance(symbols, tuple)
        assert symbols == tuple(sorted(symbols))

    def test_fallback_on_import_error(self):
        """get_heartbeat_symbols() falls back to (BTCUSDT, ETHUSDT) on ImportError."""
        with patch(
            "opendeviationbar.symbol_registry.get_symbol_entries",
            side_effect=ImportError("no module"),
        ):
            from heartbeat.config import get_heartbeat_symbols

            get_heartbeat_symbols.cache_clear()
            symbols = get_heartbeat_symbols()

        assert symbols == ("BTCUSDT", "ETHUSDT")

    def test_fallback_on_runtime_error(self):
        """get_heartbeat_symbols() falls back on any exception, not just ImportError."""
        with patch(
            "opendeviationbar.symbol_registry.get_symbol_entries",
            side_effect=RuntimeError("registry broken"),
        ):
            from heartbeat.config import get_heartbeat_symbols

            get_heartbeat_symbols.cache_clear()
            symbols = get_heartbeat_symbols()

        assert symbols == ("BTCUSDT", "ETHUSDT")


class TestYesterdayCompletion:
    """Tests for collect_yesterday_completion (TELEM-02)."""

    def test_clean_orphan_match(self):
        """Orphan last_agg_trade_id == crossover_tid - 1 -> True, detail 'clean'."""
        client = _make_client({
            "yesterday()": [[4999]],
        })
        results: dict = {}

        with (
            patch(
                "heartbeat.checks.yesterday.get_crossover_tid",
                return_value=5000,
            ),
            patch(
                "heartbeat.checks.yesterday.get_heartbeat_symbols",
                return_value=("BTCUSDT",),
            ),
        ):
            from heartbeat.checks.yesterday import collect_yesterday_completion

            collect_yesterday_completion(client, results)

        assert results["yesterday_BTCUSDT"] is True
        assert results["yesterday_BTCUSDT_detail"] == "clean"
        assert results["yesterday_completion"] is True

    def test_orphan_mismatch(self):
        """Orphan last_agg_trade_id != crossover_tid - 1 -> False with detail."""
        client = _make_client({
            "yesterday()": [[4990]],
        })
        results: dict = {}

        with (
            patch(
                "heartbeat.checks.yesterday.get_crossover_tid",
                return_value=5000,
            ),
            patch(
                "heartbeat.checks.yesterday.get_heartbeat_symbols",
                return_value=("BTCUSDT",),
            ),
        ):
            from heartbeat.checks.yesterday import collect_yesterday_completion

            collect_yesterday_completion(client, results)

        assert results["yesterday_BTCUSDT"] is False
        assert "expected 4999" in results["yesterday_BTCUSDT_detail"]
        assert "got 4990" in results["yesterday_BTCUSDT_detail"]
        assert results["yesterday_completion"] is False

    def test_no_orphan_bar(self):
        """No orphan bar for yesterday -> 'no_orphan' (not False)."""
        client = _make_client({
            "yesterday()": [[None]],
        })
        results: dict = {}

        with (
            patch(
                "heartbeat.checks.yesterday.get_crossover_tid",
                return_value=5000,
            ),
            patch(
                "heartbeat.checks.yesterday.get_heartbeat_symbols",
                return_value=("BTCUSDT",),
            ),
        ):
            from heartbeat.checks.yesterday import collect_yesterday_completion

            collect_yesterday_completion(client, results)

        assert results["yesterday_BTCUSDT"] == "no_orphan"
        assert results["yesterday_completion"] is True

    def test_crossover_query_failure(self):
        """Crossover query returns None -> 'query_failed', not unhealthy."""
        client = _make_client({})
        results: dict = {}

        with (
            patch(
                "heartbeat.checks.yesterday.get_crossover_tid",
                return_value=None,
            ),
            patch(
                "heartbeat.checks.yesterday.get_heartbeat_symbols",
                return_value=("BTCUSDT",),
            ),
        ):
            from heartbeat.checks.yesterday import collect_yesterday_completion

            collect_yesterday_completion(client, results)

        assert results["yesterday_BTCUSDT"] == "query_failed"
        assert results["yesterday_completion"] is True

    def test_multi_symbol_all_clean(self):
        """Multiple symbols all clean -> yesterday_completion=True."""
        client = _make_client({
            "yesterday()": [[4999]],
        })
        results: dict = {}

        with (
            patch(
                "heartbeat.checks.yesterday.get_crossover_tid",
                return_value=5000,
            ),
            patch(
                "heartbeat.checks.yesterday.get_heartbeat_symbols",
                return_value=("BTCUSDT", "ETHUSDT", "SOLUSDT"),
            ),
        ):
            from heartbeat.checks.yesterday import collect_yesterday_completion

            collect_yesterday_completion(client, results)

        assert results["yesterday_BTCUSDT"] is True
        assert results["yesterday_ETHUSDT"] is True
        assert results["yesterday_SOLUSDT"] is True
        assert results["yesterday_completion"] is True

    def test_multi_symbol_one_mismatch_makes_aggregate_false(self):
        """A single symbol mismatch among many makes yesterday_completion=False."""
        # We need per-symbol crossover TIDs and per-symbol CH results.
        # Use side_effect for crossover and per-symbol query results.
        crossover_map = {"BTCUSDT": 5000, "ETHUSDT": 6000, "SOLUSDT": 7000}

        def _crossover(symbol, date=None):
            return crossover_map.get(symbol)

        # BTCUSDT: clean (4999), ETHUSDT: mismatch (5990 != 5999), SOLUSDT: clean (6999)
        def _query(sql: str, **_kw: object) -> MagicMock:
            result = MagicMock()
            if "BTCUSDT" in sql:
                result.result_rows = [[4999]]
            elif "ETHUSDT" in sql:
                result.result_rows = [[5990]]  # mismatch!
            elif "SOLUSDT" in sql:
                result.result_rows = [[6999]]
            else:
                result.result_rows = []
            return result

        client = MagicMock()
        client.query.side_effect = _query
        results: dict = {}

        with (
            patch(
                "heartbeat.checks.yesterday.get_crossover_tid",
                side_effect=_crossover,
            ),
            patch(
                "heartbeat.checks.yesterday.get_heartbeat_symbols",
                return_value=("BTCUSDT", "ETHUSDT", "SOLUSDT"),
            ),
        ):
            from heartbeat.checks.yesterday import collect_yesterday_completion

            collect_yesterday_completion(client, results)

        assert results["yesterday_BTCUSDT"] is True
        assert results["yesterday_ETHUSDT"] is False
        assert results["yesterday_SOLUSDT"] is True
        assert results["yesterday_completion"] is False


class TestJunctionTelemetry:
    """Tests for collect_junction_telemetry (TELEM-03)."""

    def test_sidecar_health_available(self):
        """Sidecar health endpoint returns data -> junction populated."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "status": "running",
            "symbols": {
                "BTCUSDT": {
                    "rest_last_tid": 12345678,
                    "ws_first_tid": 12345679,
                    "forming_bar": "open=42000 trades=15",
                    "committed_floors": {"250": 12345677},
                }
            },
        }

        results: dict = {}

        with patch("heartbeat.checks.junction.requests.get", return_value=mock_response):
            from heartbeat.checks.junction import collect_junction_telemetry

            collect_junction_telemetry(results)

        assert results["junction_telemetry"] is True
        assert results["junction_rest_last_tid"] != "unavailable"
        assert results["junction_ws_first_tid"] != "unavailable"

    def test_sidecar_down(self):
        """Sidecar unavailable -> junction keys set to 'unavailable'."""
        import requests

        results: dict = {}

        with patch(
            "heartbeat.checks.junction.requests.get",
            side_effect=requests.ConnectionError("refused"),
        ):
            from heartbeat.checks.junction import collect_junction_telemetry

            collect_junction_telemetry(results)

        assert results["junction_telemetry"] is False
        assert results["junction_rest_last_tid"] == "unavailable"
        assert results["junction_ws_first_tid"] == "unavailable"
        assert results["junction_forming_bar"] == "unavailable"
        assert results["junction_committed_floors"] == "unavailable"
