"""Fixtures for SDK integration tests using celery.contrib.pytest.

This conftest registers the celery.contrib.pytest plugin which provides
``celery_app`` and ``celery_worker`` fixtures that run a real Celery
worker in a background thread (solo pool, in-memory broker).

These fixtures are separate from the unit test conftest.py to avoid
shadowing the simpler ``celery_app`` fixture used by unit tests.
"""

from __future__ import annotations

import threading
import time
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock

if TYPE_CHECKING:
    from collections.abc import Iterator

import pytest

from sluice._bootstep import _connect_event_handlers
from sluice._buffer import EventBuffer
from sluice._config import auto_configure
from sluice._transport import SluiceTransport

# Register celery.contrib.pytest so celery_app/celery_worker fixtures
# are available. This MUST be at module level before any fixtures.
pytest_plugins = ["celery.contrib.pytest"]


# ---------------------------------------------------------------------------
# celery.contrib.pytest fixture overrides
# ---------------------------------------------------------------------------


@pytest.fixture(scope="session")
def celery_config() -> dict[str, Any]:
    """Configure the test Celery app for celery.contrib.pytest fixtures."""
    return {
        "broker_url": "memory://",
        "result_backend": "cache+memory://",
        "task_always_eager": False,
        "task_send_sent_event": True,
        "worker_send_task_events": True,
        "task_track_started": True,
    }


@pytest.fixture(scope="session")
def celery_includes() -> list[str]:
    """Tell celery_worker to import the integration task module."""
    return ["tests_integration.tasks"]


@pytest.fixture(scope="session")
def celery_worker_parameters() -> dict[str, Any]:
    """Disable ping check — in-memory broker doesn't support inspect."""
    return {
        "perform_ping_check": False,
    }


# ---------------------------------------------------------------------------
# Event collector — thread-safe mock transport
# ---------------------------------------------------------------------------


class EventCollector:
    """Thread-safe collector for events sent through the transport.

    Replaces ``SluiceTransport.send_batch`` so we can inspect exactly
    what the SDK would forward to the Sluice API.

    Uses a single ``threading.Condition`` as both lock and notification
    primitive to avoid missed-wakeup bugs.
    """

    def __init__(self) -> None:
        self._events: list[dict[str, Any]] = []
        self._cond = threading.Condition()

    def send_batch(self, events: list[dict[str, Any]]) -> bool:
        """Accept a batch — called by the EventBuffer flush loop."""
        with self._cond:
            self._events.extend(events)
            self._cond.notify_all()
        return True

    @property
    def events(self) -> list[dict[str, Any]]:
        with self._cond:
            return list(self._events)

    def clear(self) -> None:
        with self._cond:
            self._events.clear()

    def wait_for_events(
        self,
        *,
        min_count: int = 1,
        timeout: float = 10.0,
    ) -> list[dict[str, Any]]:
        """Block until at least ``min_count`` events are collected."""
        deadline = time.monotonic() + timeout
        with self._cond:
            while len(self._events) < min_count:
                remaining = deadline - time.monotonic()
                if remaining <= 0:
                    break
                self._cond.wait(timeout=remaining)
            return list(self._events)


@pytest.fixture()
def event_collector() -> EventCollector:
    return EventCollector()


# ---------------------------------------------------------------------------
# SDK pipeline fixture — wires buffer + signal handlers
# ---------------------------------------------------------------------------


def _clear_task_signals() -> None:
    """Disconnect all task signal handlers to prevent leaking between tests."""
    from celery.signals import (
        task_failure,
        task_postrun,
        task_prerun,
        task_received,
        task_rejected,
        task_retry,
        task_revoked,
        task_sent,
    )

    for sig in (
        task_sent,
        task_received,
        task_prerun,
        task_postrun,
        task_failure,
        task_retry,
        task_revoked,
        task_rejected,
    ):
        sig.receivers.clear()


@pytest.fixture()
def sdk_pipeline(
    celery_app: Any,
    event_collector: EventCollector,
) -> Iterator[tuple[EventBuffer, EventCollector]]:
    """Install the Sluice SDK pipeline on the celery_app.

    Wires: auto_configure -> buffer (with fast flush) -> mock transport
    -> event_collector. Connects real Celery signal handlers.

    Yields (buffer, collector) for test assertions.
    """
    # Clear any leaked handlers from a previous test that may have crashed
    # before its teardown ran (session-scoped worker survives across tests).
    _clear_task_signals()

    auto_configure(celery_app)

    mock_transport = MagicMock(spec=SluiceTransport)
    mock_transport.send_batch.side_effect = event_collector.send_batch

    buf = EventBuffer(
        mock_transport,
        max_batch_size=10,
        flush_interval_s=0.2,
    )
    buf.start()

    # Keep a reference so the handlers stay in scope through teardown.
    _handlers = _connect_event_handlers(buf)

    yield buf, event_collector

    buf.stop()
    _clear_task_signals()
