"""Tests for `retry_target` / `fallback_retry_target` (REQ-EXEC-RETRY-TARGET).

When EXIT is reached with unmet goal gates, the engine jumps to a
configured retry target instead of finalizing INCOMPLETE. Priority
order: per-node retry_target → per-node fallback_retry_target →
graph-level retry_target → graph-level fallback_retry_target.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from attractor.engine import Engine, EngineEvent, NodeStarted, RunStatus
from attractor.workflow import parse, validate
from attractor.workflow.validate import ValidationFailed


@pytest.mark.asyncio
# [int->REQ-EXEC-RETRY-TARGET]
async def test_retry_target_jumps_when_gate_unsatisfied(
    seeded_repo: Path,
) -> None:
    """An unmet goal_gate at EXIT with retry_target wired causes a jump
    to the target instead of INCOMPLETE finalization.

    Workflow: verify always fails (`exit 1`); its FAILURE edge routes
    to exit; at exit the gate is unmet so retry_target=fix fires; fix
    runs and loops back to verify. With max_visits=2 the second
    verify attempt also fails and the run ends INCOMPLETE — but
    `fix` has been started, proving the retry jump happened.
    """
    src = """\
digraph RetryFlow {
    graph [ default_max_visits = 2 ]
    start [shape=Mdiamond]
    exit  [shape=Msquare]
    verify [
        shape        = parallelogram,
        goal_gate    = true,
        max_visits   = 2,
        retry_target = fix,
        script       = "exit 1"
    ]
    fix [
        shape  = parallelogram,
        script = "exit 0"
    ]
    start  -> verify
    verify -> exit [label="SUCCESS"]
    verify -> exit [label="FAILURE"]
    fix    -> verify
}
"""
    events: list[EngineEvent] = []
    engine = Engine(seeded_repo)
    graph = validate(parse(src))
    await engine.run(graph, events=events.append)

    started_ids = [e.node_id for e in events if isinstance(e, NodeStarted)]
    # fix ran via retry_target. No FAILURE edge from verify pointed at
    # fix — the only way fix could have started is the retry jump.
    assert "fix" in started_ids, (
        f"expected fix to start via retry_target; got {started_ids}"
    )
    # verify was visited twice (visit 1, then visit 2 via retry chain).
    assert started_ids.count("verify") == 2


@pytest.mark.asyncio
# [int->REQ-EXEC-RETRY-TARGET]
async def test_retry_target_max_visits_caps_chain(seeded_repo: Path) -> None:
    """If the retry chain can never satisfy the gate, max_visits caps it
    and the run finalizes incomplete.
    """
    src = """\
digraph RetryCap {
    graph [ default_max_visits = 3 ]
    start [shape=Mdiamond]
    exit  [shape=Msquare]
    verify [
        shape        = parallelogram,
        goal_gate    = true,
        max_visits   = 3,
        retry_target = fix,
        script       = "exit 1"
    ]
    fix [
        shape  = parallelogram,
        script = "exit 0"
    ]
    start  -> verify
    verify -> exit [label="SUCCESS"]
    verify -> exit [label="FAILURE"]
    fix    -> verify
}
"""
    engine = Engine(seeded_repo)
    graph = validate(parse(src))
    status = await engine.run(graph)
    # Never satisfied → max_visits exhausted → INCOMPLETE.
    assert status == RunStatus.INCOMPLETE


@pytest.mark.asyncio
# [int->REQ-EXEC-RETRY-TARGET]
async def test_graph_level_retry_target(seeded_repo: Path) -> None:
    """Graph-level retry_target serves as a global fallback when no
    node-level target is configured.
    """
    src = """\
digraph GraphLevelRetry {
    graph [
        default_max_visits = 2,
        retry_target = global_fix
    ]
    start [shape=Mdiamond]
    exit  [shape=Msquare]
    verify [
        shape        = parallelogram,
        goal_gate    = true,
        max_visits   = 2,
        script       = "exit 1"
    ]
    global_fix [
        shape  = parallelogram,
        script = "exit 0"
    ]
    start      -> verify
    verify     -> exit [label="SUCCESS"]
    verify     -> exit [label="FAILURE"]
    global_fix -> verify
}
"""
    events: list[EngineEvent] = []
    engine = Engine(seeded_repo)
    graph = validate(parse(src))
    await engine.run(graph, events=events.append)
    started_ids = [e.node_id for e in events if isinstance(e, NodeStarted)]
    assert "global_fix" in started_ids, (
        f"expected global_fix to start via graph-level retry_target; "
        f"got {started_ids}"
    )


# [unit->REQ-EXEC-RETRY-TARGET]
def test_validator_rejects_unknown_retry_target_on_node() -> None:
    src = """\
digraph BadRetryTarget {
    start [shape=Mdiamond]
    exit  [shape=Msquare]
    verify [
        shape       = parallelogram,
        goal_gate   = true,
        retry_target = does_not_exist,
        script      = "exit 0"
    ]
    start -> verify -> exit
}
"""
    with pytest.raises(ValidationFailed) as ei:
        validate(parse(src))
    msgs = [e.message for e in ei.value.errors]
    assert any(
        "retry_target=does_not_exist" in m and "unknown node id" in m
        for m in msgs
    )


# [unit->REQ-EXEC-RETRY-TARGET]
def test_validator_rejects_unknown_retry_target_on_graph() -> None:
    src = """\
digraph BadGraphRetry {
    graph [ retry_target = ghost_node ]
    start [shape=Mdiamond]
    exit  [shape=Msquare]
    start -> exit
}
"""
    with pytest.raises(ValidationFailed) as ei:
        validate(parse(src))
    msgs = [e.message for e in ei.value.errors]
    assert any(
        "retry_target=ghost_node" in m and "unknown node id" in m
        for m in msgs
    )
