"""Tests proving execute_stream is event-loop safe.

These tests verify that execute_stream can run on a pre-existing event
loop (e.g. qasync) without nesting asyncio.run() or blocking the loop.
"""

import asyncio
import json
import tempfile
from pathlib import Path

import pytest

from porringer.backend.command.sync import SyncCommands
from porringer.schema import (
    ManifestParsedEvent,
    ProgressEvent,
    SetupParameters,
)


def _write_manifest(tmpdir: str, packages: dict[str, list[str]] | None = None) -> Path:
    """Write a minimal porringer.json into *tmpdir* and return the directory."""
    manifest_path = Path(tmpdir) / 'porringer.json'
    data = {'version': '1', 'packages': packages or {'python': ['requests']}}
    manifest_path.write_text(json.dumps(data))
    return Path(tmpdir)


@pytest.mark.mock_packages
class TestEventLoopSafety:
    """Verify execute_stream never calls asyncio.run() internally."""

    @staticmethod
    async def test_execute_stream_no_nested_run() -> None:
        """execute_stream works when driven by an already-running event loop.

        If any code path internally called asyncio.run(), this test would
        raise ``RuntimeError: This event loop is already running``.
        pytest-asyncio provides the running loop — we just await the
        async generator directly.
        """
        with tempfile.TemporaryDirectory() as tmpdir:
            directory = _write_manifest(tmpdir)
            params = SetupParameters(paths=directory, dry_run=True)
            commands = SyncCommands()

            events: list[ProgressEvent] = []
            async for event in commands.execute_stream(params):
                events.append(event)

            # Sanity: we received at least one manifest event
            assert any(isinstance(e, ManifestParsedEvent) for e in events)

    @staticmethod
    async def test_concurrent_streams_on_shared_loop() -> None:
        """Two execute_stream calls run concurrently on a single event loop.

        This proves there is no global lock or event-loop-blocking I/O
        that would serialize the two streams.
        """
        commands = SyncCommands()
        results: dict[str, list[ProgressEvent]] = {'a': [], 'b': []}

        async def _drain(key: str, directory: Path) -> None:
            params = SetupParameters(paths=directory, dry_run=True)
            async for event in commands.execute_stream(params):
                results[key].append(event)

        with (
            tempfile.TemporaryDirectory() as tmpdir_a,
            tempfile.TemporaryDirectory() as tmpdir_b,
        ):
            dir_a = _write_manifest(tmpdir_a, packages={'python': ['requests']})
            dir_b = _write_manifest(tmpdir_b, packages={'python': ['flask']})

            async with asyncio.TaskGroup() as tg:
                tg.create_task(_drain('a', dir_a))
                tg.create_task(_drain('b', dir_b))

        # Both streams produced events
        assert len(results['a']) > 0, 'Stream A produced no events'
        assert len(results['b']) > 0, 'Stream B produced no events'

        # Both had manifest-parsed events
        assert any(isinstance(e, ManifestParsedEvent) for e in results['a'])
        assert any(isinstance(e, ManifestParsedEvent) for e in results['b'])
