"""Step definitions for cross-actor subgraph cycle detection tests.

Tests for features/actor_subgraph_cycle_detection.feature — validates that
the actor compiler correctly reads actor_ref from the top-level
NodeDefinition field (not from node.config) when detecting cross-actor
subgraph cycles.

This is the regression test for issue #1431: _detect_subgraph_cycles()
was reading node.config.get("actor_ref", "") instead of node.actor_ref,
causing cycle detection to always return an empty string and never detect
cross-actor cycles.
"""

from __future__ import annotations

from behave import given, then, when
from behave.runner import Context

from cleveragents.actor.compiler import (
    SubgraphCycleError,
    compile_actor,
)
from cleveragents.actor.schema import (
    ActorConfigSchema,
    ActorType,
    EdgeDefinition,
    NodeDefinition,
    NodeType,
    RouteDefinition,
)


# ────────────────────────────────────────────────────────────
# Helpers
# ────────────────────────────────────────────────────────────


def _make_graph_actor(
    name: str,
    nodes: list[NodeDefinition],
    edges: list[EdgeDefinition],
    entry_node: str,
    exit_nodes: list[str],
) -> ActorConfigSchema:
    """Build a minimal valid GRAPH actor for testing."""
    route = RouteDefinition(
        nodes=nodes,
        edges=edges,
        entry_node=entry_node,
        exit_nodes=exit_nodes,
    )
    return ActorConfigSchema(
        name=name,
        type=ActorType.GRAPH,
        description=f"Test actor {name}",
        provider="openai",
        model="gpt-4",
        route=route,
    )


def _make_agent_node(node_id: str) -> NodeDefinition:
    """Build a minimal AGENT node."""
    return NodeDefinition(
        id=node_id,
        type=NodeType.AGENT,
        name=node_id.title(),
        description=f"Agent {node_id}",
        config={"agent": "default"},
    )


def _make_subgraph_node(node_id: str, actor_ref: str) -> NodeDefinition:
    """Build a SUBGRAPH node using the top-level actor_ref field.

    The actor_ref is set as a first-class field on NodeDefinition, NOT
    inside config.  This is the correct way to reference a subgraph actor.
    """
    return NodeDefinition(
        id=node_id,
        type=NodeType.SUBGRAPH,
        name=node_id.title(),
        description=f"Subgraph {node_id}",
        config={},
        actor_ref=actor_ref,
    )


# ────────────────────────────────────────────────────────────
# Given steps
# ────────────────────────────────────────────────────────────


@given("the actor compiler is available")
def step_compiler_available(context: Context) -> None:
    """Ensure the actor compiler module is importable."""
    context.actor_registry: dict[str, ActorConfigSchema] = {}


@given('actor "{name}" has a subgraph node with actor_ref "{ref}"')
def step_actor_has_subgraph_ref(context: Context, name: str, ref: str) -> None:
    """Create a GRAPH actor with one agent node and one subgraph node."""
    agent = _make_agent_node("start")
    sub = _make_subgraph_node("sub", actor_ref=ref)
    actor = _make_graph_actor(
        name,
        nodes=[agent, sub],
        edges=[EdgeDefinition(from_node="start", to_node="sub")],
        entry_node="start",
        exit_nodes=["sub"],
    )
    context.actor_registry[name] = actor
    context.actor_to_compile = name


@given('actor "{name}" has no subgraph nodes')
def step_actor_has_no_subgraph(context: Context, name: str) -> None:
    """Create a GRAPH actor with only an agent node (no subgraph references)."""
    agent = _make_agent_node("leaf")
    actor = _make_graph_actor(
        name,
        nodes=[agent],
        edges=[],
        entry_node="leaf",
        exit_nodes=["leaf"],
    )
    context.actor_registry[name] = actor


@given("a registry containing both actors")
def step_registry_contains_both(context: Context) -> None:
    """Set up the resolver from the accumulated registry."""
    registry = context.actor_registry

    def resolver(actor_name: str) -> ActorConfigSchema | None:
        return registry.get(actor_name)

    context.resolver = resolver


# ────────────────────────────────────────────────────────────
# When steps
# ────────────────────────────────────────────────────────────


@when('I compile actor "{name}" with the registry resolver')
def step_compile_actor(context: Context, name: str) -> None:
    """Compile the named actor using the registry resolver."""
    context.compile_error = None
    context.compiled = None
    actor = context.actor_registry[name]
    try:
        context.compiled = compile_actor(actor, actor_resolver=context.resolver)
    except Exception as exc:
        context.compile_error = exc


# ────────────────────────────────────────────────────────────
# Then steps
# ────────────────────────────────────────────────────────────


@then("the compilation should raise SubgraphCycleError")
def step_raises_cycle_error(context: Context) -> None:
    """Assert that compilation raised SubgraphCycleError."""
    assert context.compile_error is not None, (
        "Expected SubgraphCycleError but compilation succeeded"
    )
    assert isinstance(context.compile_error, SubgraphCycleError), (
        f"Expected SubgraphCycleError, got {type(context.compile_error).__name__}: "
        f"{context.compile_error}"
    )


@then("the actor compilation should succeed")
def step_actor_compilation_succeeds(context: Context) -> None:
    """Assert that compilation succeeded without errors."""
    assert context.compile_error is None, (
        f"Expected compilation to succeed but got: {context.compile_error}"
    )
    assert context.compiled is not None


@then('the cycle error message should mention "{text}"')
def step_cycle_error_mentions(context: Context, text: str) -> None:
    """Assert that the cycle error message contains the given text."""
    assert context.compile_error is not None
    msg = str(context.compile_error)
    assert text.lower() in msg.lower(), (
        f"Expected '{text}' in error message, got: {msg}"
    )


@then('the actor subgraph_refs should map "{node}" to "{ref}"')
def step_actor_subgraph_refs_map(context: Context, node: str, ref: str) -> None:
    """Assert that the compiled metadata maps the node to the expected actor ref."""
    assert context.compiled is not None
    refs = context.compiled.metadata.subgraph_refs
    assert refs.get(node) == ref, (
        f"Expected subgraph_refs[{node!r}] == {ref!r}, got {refs}"
    )
