"""Integration test for the SPEC §6.6 verify/fixup graph idiom.

The verify/fixup pattern composes existing primitives:
- A tool node (`verify`) runs a check with `goal_gate=true`.
- On SUCCESS, route to `exit`.
- On FAILURE, route to a fix-up agent (`fixup`) that loops back.
- `max_visits` caps the loop.

This test uses a MOCKED agent so no live LLM is involved. The mock's
behaviour is: on the first FAILURE-edge entry, fix the file the verify
script expects to find; on subsequent visits, just succeed.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import pytest

from attractor.agent import AgentOutcome, OutcomeStatus
from attractor.engine import Engine, RunStatus
from attractor.workflow import parse, validate

# A workflow modeling the §6.6 pattern. `verify` runs first; it fails
# because `target.txt` doesn't exist yet. The FAILURE edge routes to
# the `fixup` AGENT node, which (per our mock) creates the file. Then
# the loop returns to `verify`, which now succeeds, and the run reaches
# exit.
_VERIFY_FIXUP = """\
digraph VerifyFixup {
    graph [
        goal = "Ensure target.txt exists."
        default_max_visits = 4
    ]
    start [shape=Mdiamond, label="S"]
    exit  [shape=Msquare,  label="E"]

    verify [
        shape      = parallelogram,
        label      = "Verify",
        script     = "cat target.txt",
        goal_gate  = true,
        max_visits = 4
    ]
    fixup [
        label  = "Fix",
        prompt = "Create target.txt with the content 'fixed'."
    ]

    start  -> verify
    verify -> exit  [label="SUCCESS"]
    verify -> fixup [label="FAILURE"]
    fixup  -> verify
}
"""


@pytest.fixture()
def mock_fixup_agent(monkeypatch: pytest.MonkeyPatch) -> list[dict[str, Any]]:
    """Monkeypatch `run_agent_node` to create `target.txt` and return SUCCESS.

    Records each call (so the test can assert how many fixup attempts
    happened) and writes the file the verify script expects. The
    agent module is referenced by the engine via `attractor.agent.run_agent_node`,
    so we patch the import binding inside `attractor.engine.engine`.
    """
    calls: list[dict[str, Any]] = []

    async def fake_run_agent_node(config: Any) -> AgentOutcome:
        # The agent runs inside the worktree dir; create the file.
        target = Path(config.cwd) / "target.txt"
        target.write_text("fixed\n", encoding="utf-8")
        calls.append(
            {
                "model": config.model,
                "prompt": config.node_prompt,
                "revisit_context": dict(config.revisit_context)
                if config.revisit_context is not None
                else None,
            }
        )
        return AgentOutcome(
            status=OutcomeStatus.SUCCESS,
            captured_output="created target.txt",
            usage_input_tokens=12,
            usage_output_tokens=4,
        )

    monkeypatch.setattr(
        "attractor.engine.engine.run_agent_node", fake_run_agent_node
    )
    return calls


@pytest.mark.asyncio
# [int->REQ-EXEC-VERIFY-FIXUP-IDIOM]
# [int->REQ-EXEC-GOAL-GATES]
# [int->REQ-EXEC-EDGE-ROUTING]
async def test_verify_fixup_loop_completes(
    seeded_repo: Path,
    mock_fixup_agent: list[dict[str, Any]],
) -> None:
    """verify FAILS → fixup runs (creates target.txt) → verify SUCCEEDS → exit."""
    engine = Engine(seeded_repo)
    graph = validate(parse(_VERIFY_FIXUP))
    status = await engine.run(graph)
    assert status == RunStatus.COMPLETED
    # The fixup agent ran exactly once.
    assert len(mock_fixup_agent) == 1


@pytest.mark.asyncio
# [int->REQ-EXEC-VERIFY-FIXUP-IDIOM]
# [int->REQ-EXEC-REVISIT-CONTEXT]
async def test_verify_fixup_revisit_context_passed_to_agent(
    seeded_repo: Path,
    mock_fixup_agent: list[dict[str, Any]],
) -> None:
    """Fixup's visit-1 entry via verify's FAILURE edge surfaces predecessor context.

    SPEC §6.3 "entry-edge context" is the §6.6 fix-up pattern's reason
    for existing: fixup must be able to read verify's captured output
    even on its first visit. Verify the engine threads that through.
    """
    engine = Engine(seeded_repo)
    graph = validate(parse(_VERIFY_FIXUP))
    await engine.run(graph)
    assert len(mock_fixup_agent) == 1
    ctx = mock_fixup_agent[0]["revisit_context"]
    assert ctx is not None, "fixup entered via FAILURE edge — must see predecessor context"
    assert ctx["predecessor_node"] == "verify"
    assert ctx["entry_edge_label"] == "FAILURE"
    assert ctx["predecessor_outcome"] == "FAILURE"
    # `captured_output` is verify's output; the
    # field's PRESENCE is the contract the §6.6 idiom depends on.
    assert "captured_output" in ctx
    # Prior-self block is absent on visit 1.
    assert "visit_n" not in ctx
    assert "prior_outcome" not in ctx
