"""Tests for notification multiplexer."""

import pytest
import asyncio
from unittest.mock import AsyncMock

from skop_runner.notification_multiplexer import NotificationMultiplexer


class TestNotificationMultiplexer:
    """Test notification fanout to multiple senders."""

    @pytest.mark.asyncio
    async def test_send_to_all_senders(self):
        """All registered senders receive the notification."""
        sender_a = AsyncMock()
        sender_b = AsyncMock()

        mux = NotificationMultiplexer()
        mux.add_sender(sender_a)
        mux.add_sender(sender_b)

        await mux.send("kernel.output", {"cell_id": "c1"})

        sender_a.assert_called_once_with("kernel.output", {"cell_id": "c1"})
        sender_b.assert_called_once_with("kernel.output", {"cell_id": "c1"})

    @pytest.mark.asyncio
    async def test_failed_sender_does_not_block_others(self):
        """A failing sender should not prevent other senders from receiving."""
        sender_a = AsyncMock(side_effect=Exception("relay down"))
        sender_b = AsyncMock()

        mux = NotificationMultiplexer()
        mux.add_sender(sender_a)
        mux.add_sender(sender_b)

        # Should not raise
        await mux.send("kernel.output", {"cell_id": "c1"})

        sender_a.assert_called_once()
        sender_b.assert_called_once_with("kernel.output", {"cell_id": "c1"})

    @pytest.mark.asyncio
    async def test_no_senders_is_noop(self):
        """send() with no registered senders returns immediately."""
        mux = NotificationMultiplexer()
        # Should not raise
        await mux.send("kernel.output", {"cell_id": "c1"})

    @pytest.mark.asyncio
    async def test_remove_sender(self):
        """Removed sender no longer receives notifications."""
        sender_a = AsyncMock()
        sender_b = AsyncMock()

        mux = NotificationMultiplexer()
        mux.add_sender(sender_a)
        mux.add_sender(sender_b)

        mux.remove_sender(sender_a)

        await mux.send("kernel.output", {"cell_id": "c1"})

        sender_a.assert_not_called()
        sender_b.assert_called_once()

    @pytest.mark.asyncio
    async def test_remove_nonexistent_sender(self):
        """Removing a sender that was never added does not raise."""
        mux = NotificationMultiplexer()
        mux.remove_sender(AsyncMock())  # Should not raise

    @pytest.mark.asyncio
    async def test_senders_called_concurrently(self):
        """Senders run concurrently, not sequentially."""
        call_order = []

        async def slow_sender(method, params):
            call_order.append("slow_start")
            await asyncio.sleep(0.05)
            call_order.append("slow_end")

        async def fast_sender(method, params):
            call_order.append("fast_start")
            call_order.append("fast_end")

        mux = NotificationMultiplexer()
        mux.add_sender(slow_sender)
        mux.add_sender(fast_sender)

        await mux.send("test", {})

        # fast_sender should complete before slow_sender finishes
        assert "fast_end" in call_order
        assert call_order.index("fast_end") < call_order.index("slow_end")
