"""Engine.start() + RunSession live-handle behaviour. See SPEC.md §12.1.

Pins the contract that distinguishes `start()` from `run()`: the
`run_id` is available the moment `start()` returns, before traversal
completes, so a host can address the in-flight run via `engine.show()`
or `engine.list()` while it's still running. Also covers
`session.cancel()` and resume-after-cancel.
"""

# [unit->REQ-API-RUN-SESSION]

from __future__ import annotations

import asyncio
import re
from pathlib import Path

import pytest

from attractor.engine import Engine, RunSession, RunStatus
from attractor.workflow import parse, validate

_UUID4_RE = re.compile(
    r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
)


@pytest.mark.asyncio
async def test_start_returns_session_with_run_id_immediately(
    seeded_repo: Path, tool_only_workflow: str
) -> None:
    """`session.run_id` is set before `wait()` is called."""
    engine = Engine(seeded_repo)
    graph = validate(parse(tool_only_workflow))

    session = await engine.start(graph)

    assert isinstance(session, RunSession)
    assert _UUID4_RE.match(session.run_id), (
        f"run_id is not a UUID v4: {session.run_id!r}"
    )
    assert session.done() is False  # task just scheduled, not yet awaited

    # Clean up the in-flight task so pytest doesn't warn about
    # un-awaited coroutines.
    status = await session.wait()
    assert status is RunStatus.COMPLETED


@pytest.mark.asyncio
async def test_show_works_during_in_flight_run(
    seeded_repo: Path, tool_only_workflow: str
) -> None:
    """`engine.show(session.run_id)` succeeds before `wait()` returns.

    Proves the eager setup in `start()` (worktree create + first
    journal entry) makes the run addressable while traversal is still
    a background task.
    """
    engine = Engine(seeded_repo)
    graph = validate(parse(tool_only_workflow))

    session = await engine.start(graph)

    # Before awaiting wait(), the run already has at least the
    # RunInitialized journal entry, so show() must succeed.
    summary = engine.show(session.run_id)
    assert summary.run_id == session.run_id

    status = await session.wait()
    assert status is RunStatus.COMPLETED


@pytest.mark.asyncio
async def test_run_equivalent_to_start_then_wait(
    seeded_repo: Path, tool_only_workflow: str
) -> None:
    """`engine.run(...)` reaches the same terminal status as start + wait."""
    engine = Engine(seeded_repo)
    graph = validate(parse(tool_only_workflow))

    run_status = await engine.run(graph)
    assert run_status is RunStatus.COMPLETED


@pytest.mark.asyncio
async def test_session_done_flips_after_wait(
    seeded_repo: Path, tool_only_workflow: str
) -> None:
    """`done()` is False while the task is in flight, True once it terminates."""
    engine = Engine(seeded_repo)
    graph = validate(parse(tool_only_workflow))

    session = await engine.start(graph)
    assert session.done() is False
    await session.wait()
    assert session.done() is True


@pytest.mark.asyncio
async def test_cancel_before_first_step_raises_cancelled(
    seeded_repo: Path, tool_only_workflow: str
) -> None:
    """cancel() right after start() causes wait() to raise CancelledError.

    Deterministic: `start()` returns with the traversal task scheduled
    but not yet stepped (no `await` between `create_task` and `return`),
    so `cancel()` lands before the task executes any work. The first
    step of the task throws CancelledError into the coroutine.
    """
    engine = Engine(seeded_repo)
    graph = validate(parse(tool_only_workflow))

    session = await engine.start(graph)
    assert session.cancel() is True

    with pytest.raises(asyncio.CancelledError):
        await session.wait()

    assert session.done() is True


@pytest.mark.asyncio
async def test_cancel_after_done_returns_false(
    seeded_repo: Path, tool_only_workflow: str
) -> None:
    """cancel() on a terminated session is a no-op and returns False."""
    engine = Engine(seeded_repo)
    graph = validate(parse(tool_only_workflow))

    session = await engine.start(graph)
    await session.wait()
    assert session.done() is True
    assert session.cancel() is False


@pytest.mark.asyncio
async def test_resume_after_cancel_completes(
    seeded_repo: Path, tool_only_workflow: str
) -> None:
    """After cancel, engine.resume(run_id) reaches the terminal status.

    Same recovery path as SPEC §6.4: if cancel landed between nodes
    we re-enter at next_node (case a); if mid-node we re-enter the
    same node (case b). Either way the journal stays consistent and
    the run terminates COMPLETED on the second attempt.
    """
    engine = Engine(seeded_repo)
    graph = validate(parse(tool_only_workflow))

    session = await engine.start(graph)
    session.cancel()
    with pytest.raises(asyncio.CancelledError):
        await session.wait()

    status = await engine.resume(session.run_id)
    assert status is RunStatus.COMPLETED
