"""Tests for bounded Telegram log sink thread pool (Issue #242, STAB-01/02).

Verifies that _TelegramLogSink uses a bounded ThreadPoolExecutor instead of
unbounded threading.Thread, preventing thread exhaustion during high-volume
logging (e.g., kintsugi 30-shard batch repairs with SSL errors).
"""

from __future__ import annotations

import inspect
import threading
import time
from unittest.mock import MagicMock, patch

import pytest


@pytest.fixture()
def _clear_executor():
    """Reset the module-level executor between tests."""
    import opendeviationbar.logging as log_mod

    old = getattr(log_mod, "_telegram_log_executor", None)
    yield
    # Restore / shutdown
    current = getattr(log_mod, "_telegram_log_executor", None)
    if current is not None and current is not old:
        current.shutdown(wait=False)
    log_mod._telegram_log_executor = old


@pytest.fixture()
def sink():
    """Create a _TelegramLogSink instance."""
    from opendeviationbar.logging import _TelegramLogSink

    return _TelegramLogSink("test-service")


def _make_record(level: str = "WARNING", message: str = "test warning") -> MagicMock:
    """Build a minimal loguru-style message object for the sink."""
    record = {
        "level": MagicMock(name=level),
        "message": message,
        "file": MagicMock(path="test/file.py"),
        "line": 42,
        "function": "test_func",
        "extra": {},
        "exception": None,
    }
    record["level"].name = level
    msg = MagicMock()
    msg.record = record
    return msg


@pytest.mark.usefixtures("_clear_executor")
class TestBoundedThreadPool:
    """Test that the Telegram log sink uses a bounded thread pool."""

    def test_bounded_thread_pool(self, sink: object) -> None:
        """Fire 50 WARNING logs with slow send_telegram; thread count stays bounded.

        Previously, each log would spawn a new thread. With ThreadPoolExecutor(max_workers=2),
        at most 2 worker threads should exist regardless of log volume.
        """
        slow_event = threading.Event()

        def slow_send(*args, **kwargs):
            slow_event.wait(timeout=5)

        baseline = threading.active_count()

        # Patch send_telegram where _send() imports it from
        with patch("opendeviationbar.notify.telegram.send_telegram", slow_send):
            for i in range(50):
                # Use unique messages to bypass rate limiter
                msg = _make_record(message=f"unique warning number {i}")
                sink(msg)

            # Give executor time to start workers
            time.sleep(0.3)

            current = threading.active_count()
            # Should be at most baseline + 2 (the pool workers) + small margin
            assert current <= baseline + 4, (
                f"Thread count {current} exceeded baseline {baseline} + 4. "
                f"Expected bounded pool of 2 workers, not unbounded threads."
            )

        # Unblock workers so they finish cleanly
        slow_event.set()
        time.sleep(0.1)

    def test_executor_submit_not_thread_start(self) -> None:
        """Verify logging.py uses executor.submit(), not threading.Thread().start().

        Grep the source for threading.Thread -- must not appear in the emit/call path.
        """
        import opendeviationbar.logging as log_mod

        source = inspect.getsource(log_mod._TelegramLogSink.__call__)
        assert "threading.Thread" not in source, (
            "_TelegramLogSink.__call__ still uses threading.Thread"
        )
        assert "submit" in source, (
            "_TelegramLogSink.__call__ should use executor.submit()"
        )

    def test_failed_telegram_does_not_crash_pipeline(self, sink: object) -> None:
        """Mock send_telegram to raise ConnectionError; pipeline must not crash."""
        def exploding_send(*args, **kwargs):
            raise ConnectionError("SSL handshake failed")

        with patch("opendeviationbar.notify.telegram.send_telegram", exploding_send):
            for i in range(10):
                msg = _make_record(message=f"error message {i}")
                # Should not raise -- exceptions caught in _send()
                sink(msg)

        # Brief wait for executor tasks to complete (they should catch exceptions)
        time.sleep(0.3)

    def test_log_sink_timeout_config(self) -> None:
        """NetworkConfig must have log_sink_timeout_s defaulting to 3."""
        from opendeviationbar.config.network import NetworkConfig

        config = NetworkConfig()
        assert hasattr(config, "log_sink_timeout_s"), (
            "NetworkConfig missing log_sink_timeout_s field"
        )
        assert config.log_sink_timeout_s == 3, (
            f"log_sink_timeout_s should default to 3, got {config.log_sink_timeout_s}"
        )
