"""User-perspective tests for ``mgf.common.progress`` + the
async sibling.

Surfaces exercised:
  - :class:`CancellationToken` (sync)
  - :class:`AsyncCancellationToken` (async sibling)
  - :class:`NullReporter` / :class:`RecordingReporter`
  - :class:`CancellationError` + ``raise_if_cancelled``
"""

from __future__ import annotations

import asyncio

import pytest

from mgf.common.exceptions import CancellationError, OperationError
from mgf.common.progress import (
    CancellationToken,
    NullReporter,
    RecordingReporter,
)
from mgf.common.progress.asyncio import AsyncCancellationToken
from mgf.common.typing import CancellationProtocol, ReporterProtocol


class TestCancellationToken:
    def test_default_not_cancelled(self) -> None:
        tok = CancellationToken()
        assert tok.is_cancelled() is False

    def test_cancel_flips_state(self) -> None:
        tok = CancellationToken()
        tok.cancel()
        assert tok.is_cancelled() is True

    def test_raise_if_cancelled_raises_after_cancel(self) -> None:
        tok = CancellationToken()
        tok.cancel()
        with pytest.raises(CancellationError):
            tok.raise_if_cancelled()

    def test_cancellation_error_caught_as_operation_error(self) -> None:
        """Existing ``except OperationError:`` clauses catch cancellation."""
        tok = CancellationToken()
        tok.cancel()
        try:
            tok.raise_if_cancelled()
        except OperationError:
            return
        raise AssertionError("CancellationError not caught as OperationError")

    def test_implements_cancellation_protocol(self) -> None:
        tok = CancellationToken()
        assert isinstance(tok, CancellationProtocol)


class TestReporters:
    def test_null_reporter_silent(self) -> None:
        r = NullReporter()
        # Doesn't raise on any call shape:
        r.report("doing thing")
        r.report("with progress", current=5, total=10)
        assert r.is_cancelled() is False

    def test_recording_reporter_captures_calls(self) -> None:
        r = RecordingReporter()
        r.report("step 1")
        r.report("step 2", current=1, total=2)
        assert len(r.calls) == 2
        assert r.calls[0] == ("step 1", 0, 0)
        assert r.calls[1] == ("step 2", 1, 2)

    def test_reporter_implements_protocol(self) -> None:
        r = RecordingReporter()
        assert isinstance(r, ReporterProtocol)


class TestAsyncCancellation:
    @pytest.mark.asyncio
    async def test_async_token_basic(self) -> None:
        tok = AsyncCancellationToken()
        assert tok.is_cancelled() is False
        tok.cancel()
        assert tok.is_cancelled() is True

    @pytest.mark.asyncio
    async def test_async_wait_returns_when_cancelled(self) -> None:
        tok = AsyncCancellationToken()

        async def _canceller() -> None:
            await asyncio.sleep(0.01)
            tok.cancel()

        task = asyncio.create_task(_canceller())
        await tok.wait()  # blocks until cancelled
        assert tok.is_cancelled() is True
        await task
