"""Integration tests for §11 human-gate semantics.

Covers:
- pause → emit HumanGate → return PAUSED
- respond(unknown choice) → UnknownChoice
- respond when not paused → NotPausedAtGate
- approve → resume to exit (COMPLETED)
- §11.1 rule 1: unlabeled edges from a hexagon are unreachable
- §11.1 rule 3: unmatched choice errors with the valid list
- §11.1 rule 4: stale-response after graph edit re-validates against snapshot
"""

from __future__ import annotations

from pathlib import Path

import pytest

from attractor.engine import (
    Engine,
    EngineEvent,
    HumanGate,
    NotPausedAtGate,
    PausedAtGate,
    RunStatus,
    UnknownChoice,
)
from attractor.workflow import parse, validate


@pytest.mark.asyncio
# [int->REQ-HUMAN-GATE]
async def test_human_gate_pauses_run(
    seeded_repo: Path, human_gate_workflow: str
) -> None:
    """Reaching a hexagon emits HumanGate and returns PAUSED."""
    engine = Engine(seeded_repo)
    graph = validate(parse(human_gate_workflow))
    events: list[EngineEvent] = []
    status = await engine.run(graph, inputs=(), events=events.append)
    assert status == RunStatus.PAUSED

    gate_events = [e for e in events if isinstance(e, HumanGate)]
    assert len(gate_events) == 1
    gate = gate_events[0]
    assert gate.node_id == "decide"
    assert set(gate.choices) == {"approve", "revise"}


@pytest.mark.asyncio
# [int->REQ-HUMAN-GATE]
async def test_human_gate_approve_path_resumes_to_completed(
    seeded_repo: Path, human_gate_workflow: str
) -> None:
    """Pause → respond("approve") → resume → COMPLETED."""
    engine = Engine(seeded_repo)
    graph = validate(parse(human_gate_workflow))

    # 1. Run to pause.
    events: list[EngineEvent] = []
    status1 = await engine.run(graph, inputs=(), events=events.append)
    assert status1 == RunStatus.PAUSED

    # Find the paused run-id via list().
    handles = engine.list()
    assert len(handles) == 1
    run_id = handles[0].run_id
    assert handles[0].status == RunStatus.PAUSED

    # 2. Respond.
    await engine.respond(run_id=run_id, choice="approve")

    # 3. Resume.
    status2 = await engine.resume(run_id=run_id, events=events.append)
    assert status2 == RunStatus.COMPLETED


@pytest.mark.asyncio
# [int->REQ-HUMAN-GATE]
async def test_human_gate_revise_path_loops_back(
    seeded_repo: Path, human_gate_workflow: str
) -> None:
    """revise → prep → decide → approve still reaches exit."""
    engine = Engine(seeded_repo)
    graph = validate(parse(human_gate_workflow))

    status1 = await engine.run(graph)
    assert status1 == RunStatus.PAUSED
    run_id = engine.list()[0].run_id

    # Revise → engine should re-pause at the gate after re-running prep.
    await engine.respond(run_id, "revise")
    status2 = await engine.resume(run_id)
    assert status2 == RunStatus.PAUSED

    # Now approve.
    await engine.respond(run_id, "approve")
    status3 = await engine.resume(run_id)
    assert status3 == RunStatus.COMPLETED


@pytest.mark.asyncio
# [int->REQ-HUMAN-GATE]
async def test_respond_unknown_choice_raises_with_valid_list(
    seeded_repo: Path, human_gate_workflow: str
) -> None:
    """§11.1 rule 3: unknown choice → UnknownChoice with valid_choices."""
    engine = Engine(seeded_repo)
    graph = validate(parse(human_gate_workflow))
    await engine.run(graph)
    run_id = engine.list()[0].run_id

    with pytest.raises(UnknownChoice) as exc_info:
        await engine.respond(run_id, "ship-it")
    assert set(exc_info.value.valid_choices) == {"approve", "revise"}


@pytest.mark.asyncio
# [int->REQ-HUMAN-GATE]
async def test_respond_when_not_paused_raises_not_paused(
    seeded_repo: Path, tool_only_workflow: str
) -> None:
    """`respond` on a completed run → NotPausedAtGate."""
    engine = Engine(seeded_repo)
    graph = validate(parse(tool_only_workflow))
    await engine.run(graph)
    # tool-only completes immediately; find any state ref.
    import pygit2

    repo = pygit2.Repository(str(seeded_repo))
    state_branches = [
        b for b in repo.references if b.endswith("/state")
        and "attractor/run/" in b
    ]
    assert state_branches
    run_id = state_branches[0].split("/")[-2]

    with pytest.raises(NotPausedAtGate):
        await engine.respond(run_id, "anything")


# [unit->REQ-HUMAN-GATE]
def test_human_gate_unlabeled_edge_excluded_from_choices() -> None:
    """§11.1 rule 1: unlabeled outgoing edges from a hexagon are unreachable.

    The workflow's parser accepts the graph; the engine simply omits
    unlabeled hexagon edges from `HumanGate.choices` so the host can't
    surface an unreachable option.
    """
    source = """\
digraph U {
    graph [ default_max_visits = 1 ]
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]
    decide [shape=hexagon, label="Hmm"]
    start -> decide
    decide -> exit
    decide -> exit [label="approve"]
}
"""
    graph = validate(parse(source))
    from attractor.engine.routing import human_gate_choices
    choices = human_gate_choices(graph, "decide")
    assert choices == ["approve"]  # unlabeled edge excluded


@pytest.mark.asyncio
# [int->REQ-HUMAN-GATE]
async def test_paused_at_gate_journal_entry_captures_snapshot_choices(
    seeded_repo: Path, human_gate_workflow: str
) -> None:
    """§11.1 rule 4: the PausedAtGate entry stores the choices verbatim
    so respond() can re-validate against the snapshot even if the file
    is edited."""
    engine = Engine(seeded_repo)
    graph = validate(parse(human_gate_workflow))
    await engine.run(graph)
    run_id = engine.list()[0].run_id

    summary = engine.show(run_id)
    assert summary.status == RunStatus.PAUSED
    assert set(summary.paused_choices) == {"approve", "revise"}

    # Sanity: PausedAtGate journal entry is present in the engine's
    # internal load_journal path.
    from attractor.checkpoint import Author, BranchStore

    repo = engine._open_repo()  # pyright: ignore[reportPrivateUsage]
    store = BranchStore(repo, Author())
    store_entries = engine._load_journal(  # pyright: ignore[reportPrivateUsage]
        store, run_id
    )
    pausing = [e for e in store_entries if isinstance(e, PausedAtGate)]
    assert pausing
    assert set(pausing[-1].choices) == {"approve", "revise"}
