"""Unit + integration tests for the per-handler dispatch logic.

Covers:
- REQ-EXEC-NODE-LIFECYCLE (each kind goes through the same lifecycle)
- REQ-EXEC-OUTCOMES-DYNAMIC (tool exit code → success/failure)
- REQ-EXEC-OUTCOMES-STRUCTURAL (start → SUCCESS, exit → terminal)
- REQ-EXEC-WORKTREE-PERSIST (no revert between visits)
- REQ-EXEC-CONCURRENCY (two runs share the same repo, each isolated worktree)
"""

from __future__ import annotations

from pathlib import Path

import pytest

from attractor.engine import Engine, RunStatus
from attractor.workflow import parse, validate

# A workflow where a tool node fails on its first visit (file not
# present), succeeds on its second (file was written by the prior
# AGENT visit). Used to prove that worktree state persists.
_PERSIST = """\
digraph P {
    graph [ default_max_visits = 4 ]
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]
    write [
        shape  = parallelogram,
        label  = "Write",
        script = "echo hi > pers.log"
    ]
    check [
        shape     = parallelogram,
        label     = "Check",
        script    = "cat pers.log",
        goal_gate = true
    ]
    start -> write -> check -> exit
}
"""


_TOOL_FAIL = """\
digraph TF {
    graph [ default_max_visits = 1 ]
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]
    t     [shape=parallelogram, script="exit 7", goal_gate=true]
    start -> t -> exit
}
"""


@pytest.mark.asyncio
# [unit->REQ-EXEC-NODE-LIFECYCLE]
# [unit->REQ-EXEC-OUTCOMES-DYNAMIC]
async def test_tool_node_exit_code_zero_is_success(seeded_repo: Path) -> None:
    """Tool exit 0 → SUCCESS → traversal continues."""
    engine = Engine(seeded_repo)
    graph = validate(parse(_PERSIST))
    status = await engine.run(graph)
    assert status == RunStatus.COMPLETED


@pytest.mark.asyncio
# [unit->REQ-EXEC-NODE-LIFECYCLE]
# [unit->REQ-EXEC-OUTCOMES-DYNAMIC]
async def test_tool_node_nonzero_exit_is_failure(seeded_repo: Path) -> None:
    """Tool exit non-zero → FAILURE → run ends INCOMPLETE (no FAILURE
    edge in this workflow)."""
    engine = Engine(seeded_repo)
    graph = validate(parse(_TOOL_FAIL))
    status = await engine.run(graph)
    assert status == RunStatus.INCOMPLETE


@pytest.mark.asyncio
# [unit->REQ-EXEC-OUTCOMES-STRUCTURAL]
async def test_start_always_succeeds_and_routes(seeded_repo: Path) -> None:
    """SPEC §6.2: start always SUCCESS, no script."""
    engine = Engine(seeded_repo)
    # Trivial workflow: start → exit.
    graph = validate(
        parse(
            """\
digraph S {
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]
    start -> exit
}
"""
        )
    )
    status = await engine.run(graph)
    assert status == RunStatus.COMPLETED


@pytest.mark.asyncio
# [unit->REQ-EXEC-WORKTREE-PERSIST]
async def test_worktree_state_persists_across_node_boundaries(seeded_repo: Path) -> None:
    """A file written by one tool node is still present when the next
    tool node runs — SPEC §6.3 / §6.7."""
    engine = Engine(seeded_repo)
    graph = validate(parse(_PERSIST))
    status = await engine.run(graph)
    assert status == RunStatus.COMPLETED


@pytest.mark.asyncio
# [unit->REQ-EXEC-CONCURRENCY]
async def test_two_runs_in_same_repo_are_isolated(seeded_repo: Path) -> None:
    """Two `Engine.run` calls in the same repo each get their own
    worktree directory + state branch."""
    src = """\
digraph C {
    graph [ default_max_visits = 1 ]
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]
    t     [shape=parallelogram, script="echo hi > out.log"]
    start -> t -> exit
}
"""
    engine = Engine(seeded_repo)
    graph = validate(parse(src))
    s1 = await engine.run(graph)
    s2 = await engine.run(graph)
    assert s1 == RunStatus.COMPLETED
    assert s2 == RunStatus.COMPLETED

    # Each created its own state branch.
    import pygit2
    repo = pygit2.Repository(str(seeded_repo))
    state_refs = [
        r for r in repo.references
        if r.endswith("/state") and "attractor/run/" in r
    ]
    assert len(state_refs) == 2
    # And the worktree directories are removed for both (COMPLETED).
    assert engine.list() == []
