"""Integration test for agent progress events (REQ-AGENT-PROGRESS-EVENTS).

Proves that when an agent node's session invokes a tool, the engine
emits an :class:`AgentToolUse` event on the EngineEvent callback —
between :class:`NodeStarted` and :class:`NodeOutcome` for that node —
without needing a live Anthropic API key.

The mock pattern mirrors `test_node_scope.py`: monkeypatch the
engine module's bound `run_agent_node` reference (NOT the source
module — the engine imports the symbol at module-load time) with
a stub that exercises the `on_tool_use` callback the engine wires
into AgentConfig.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import pytest

from attractor.agent.config import AgentConfig, AgentOutcome, OutcomeStatus
from attractor.engine import (
    AgentToolUse,
    Engine,
    EngineEvent,
    NodeOutcome,
    NodeStarted,
    RunStatus,
)
from attractor.workflow import parse, validate

# ─── minimal one-agent workflow ─────────────────────────────────────────

_AGENT_WORKFLOW = """\
digraph ProgressEvents {
    start  [shape=Mdiamond, label="Start"]
    exit   [shape=Msquare,  label="Exit"]
    worker [label="Worker", prompt="do a task"]

    start -> worker -> exit [label="SUCCESS"]
}
"""


# ─── helpers ────────────────────────────────────────────────────────────


def _make_tool_calling_stub(
    tool_calls: list[tuple[str, str]],
) -> Any:
    """Stub run_agent_node that invokes `on_tool_use` for each entry in
    `tool_calls`, then returns SUCCESS.
    """

    async def _stub(config: AgentConfig) -> AgentOutcome:
        # The engine wires this when constructing the AgentConfig —
        # if it's None, the wiring regressed.
        assert config.on_tool_use is not None, (
            "engine did not wire on_tool_use on the AgentConfig"
        )
        for name, args in tool_calls:
            config.on_tool_use(name, args)
        return AgentOutcome(
            status=OutcomeStatus.SUCCESS, captured_output="stub ok"
        )

    return _stub


# ─── tests ──────────────────────────────────────────────────────────────


@pytest.mark.asyncio
# [int->REQ-AGENT-PROGRESS-EVENTS]
async def test_engine_emits_agent_tool_use_per_invocation(
    seeded_repo: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Each on_tool_use call from the runner produces one AgentToolUse
    event on the engine's event stream.
    """
    tool_calls = [
        ("Read", "file_path=SPEC.md"),
        ("Bash", "command=uv run pytest -k routing"),
        ("Edit", "file_path=src/attractor/engine/engine.py"),
    ]
    monkeypatch.setattr(
        "attractor.engine.engine.run_agent_node",
        _make_tool_calling_stub(tool_calls),
    )

    events: list[EngineEvent] = []
    engine = Engine(seeded_repo)
    graph = validate(parse(_AGENT_WORKFLOW))
    status = await engine.run(graph, events=events.append)

    assert status == RunStatus.COMPLETED

    tool_use_events = [e for e in events if isinstance(e, AgentToolUse)]
    assert len(tool_use_events) == len(tool_calls)

    for (expected_name, expected_args), evt in zip(
        tool_calls, tool_use_events, strict=True
    ):
        assert evt.tool_name == expected_name
        assert evt.args_preview == expected_args
        assert evt.node_id == "worker"
        assert evt.run_id  # non-empty run_id


@pytest.mark.asyncio
# [int->REQ-AGENT-PROGRESS-EVENTS]
async def test_agent_tool_use_arrives_between_started_and_outcome(
    seeded_repo: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """SPEC §12 ordering: AgentToolUse events for a node land strictly
    between that node's NodeStarted and NodeOutcome.
    """
    monkeypatch.setattr(
        "attractor.engine.engine.run_agent_node",
        _make_tool_calling_stub([("Read", "file_path=goal.md")]),
    )

    events: list[EngineEvent] = []
    engine = Engine(seeded_repo)
    graph = validate(parse(_AGENT_WORKFLOW))
    await engine.run(graph, events=events.append)

    # Find the indices of the three event types for the `worker` node.
    started_idx = next(
        i for i, e in enumerate(events)
        if isinstance(e, NodeStarted) and e.node_id == "worker"
    )
    tool_idx = next(
        i for i, e in enumerate(events)
        if isinstance(e, AgentToolUse) and e.node_id == "worker"
    )
    outcome_idx = next(
        i for i, e in enumerate(events)
        if isinstance(e, NodeOutcome) and e.node_id == "worker"
    )
    assert started_idx < tool_idx < outcome_idx


@pytest.mark.asyncio
# [int->REQ-AGENT-PROGRESS-EVENTS]
async def test_no_events_callback_means_no_wiring_overhead(
    seeded_repo: Path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """When the host does not register an events callback, the engine
    must not wire on_tool_use either — so the SDK iteration loop has
    nothing to call into. Lets fully unobserved runs keep their v0.1
    fast path.
    """
    captured: list[AgentConfig] = []

    async def _capture_stub(config: AgentConfig) -> AgentOutcome:
        captured.append(config)
        return AgentOutcome(status=OutcomeStatus.SUCCESS, captured_output="ok")

    monkeypatch.setattr(
        "attractor.engine.engine.run_agent_node",
        _capture_stub,
    )

    engine = Engine(seeded_repo)
    graph = validate(parse(_AGENT_WORKFLOW))
    # No `events=` kwarg.
    await engine.run(graph)

    assert len(captured) == 1
    assert captured[0].on_tool_use is None
