"""Step definitions for actor compiler feature tests.

Tests for features/actor_compiler.feature — validates that the actor
compiler correctly translates GRAPH-type ActorConfigSchema definitions
into LangGraph node/edge structures, detects cycles, and produces
compilation metadata.
"""

from __future__ import annotations

import json

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]

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

# ────────────────────────────────────────────────────────────
# Helper builders
# ────────────────────────────────────────────────────────────


def _build_graph_config(
    name: str,
    nodes: list[NodeDefinition],
    edges: list[EdgeDefinition],
    entry_node: str,
    exit_nodes: list[str],
) -> ActorConfigSchema:
    route = RouteDefinition(
        nodes=nodes,
        edges=edges,
        entry_node=entry_node,
        exit_nodes=exit_nodes,
    )
    return ActorConfigSchema(
        name=name,
        type=ActorType.GRAPH,
        description="Test graph actor",
        model="gpt-4",
        route=route,
    )


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


@given("a GRAPH actor config with a linear two-node topology")
def step_given_linear_two_node(context: Context) -> None:
    nodes = [
        NodeDefinition(
            id="planner",
            type=NodeType.AGENT,
            name="Planner",
            description="Plans tasks",
            config={"agent": "planner_agent"},
        ),
        NodeDefinition(
            id="executor",
            type=NodeType.TOOL,
            name="Executor",
            description="Runs tasks",
            config={"tool_name": "exec/run"},
        ),
    ]
    edges = [EdgeDefinition(from_node="planner", to_node="executor")]
    context.actor_config = _build_graph_config(
        "workflows/simple", nodes, edges, "planner", ["executor"]
    )


@given("a GRAPH actor config with an agent and a tool node")
def step_given_agent_and_tool(context: Context) -> None:
    nodes = [
        NodeDefinition(
            id="writer",
            type=NodeType.AGENT,
            name="Writer",
            description="Writes code",
            config={"agent": "writer_agent"},
        ),
        NodeDefinition(
            id="linter",
            type=NodeType.TOOL,
            name="Linter",
            description="Lints code",
            config={"tool_name": "lint/run"},
        ),
    ]
    edges = [EdgeDefinition(from_node="writer", to_node="linter")]
    context.actor_config = _build_graph_config(
        "workflows/lint", nodes, edges, "writer", ["linter"]
    )


@given("a GRAPH actor config with LSP bindings on the agent node")
def step_given_lsp_bindings(context: Context) -> None:
    nodes = [
        NodeDefinition(
            id="coder",
            type=NodeType.AGENT,
            name="Coder",
            description="Writes code",
            config={
                "agent": "coder_agent",
                "lsp_bindings": [
                    {
                        "lsp_server_name": "local/pyright",
                        "languages": ["python"],
                        "auto_detect": True,
                    }
                ],
            },
        ),
    ]
    context.actor_config = _build_graph_config(
        "workflows/lsp", nodes, [], "coder", ["coder"]
    )


@given("a GRAPH actor config with a conditional routing node")
def step_given_conditional(context: Context) -> None:
    nodes = [
        NodeDefinition(
            id="start_node",
            type=NodeType.AGENT,
            name="Agent",
            description="Starts",
            config={"agent": "a"},
        ),
        NodeDefinition(
            id="checker",
            type=NodeType.CONDITIONAL,
            name="Checker",
            description="Checks result",
            config={"function": "check_fn"},
        ),
        NodeDefinition(
            id="end_node",
            type=NodeType.AGENT,
            name="Finisher",
            description="Ends",
            config={"agent": "b"},
        ),
    ]
    edges = [
        EdgeDefinition(from_node="start_node", to_node="checker"),
        EdgeDefinition(
            from_node="checker",
            to_node="end_node",
            condition="state.get('ok')",
        ),
    ]
    context.actor_config = _build_graph_config(
        "workflows/cond", nodes, edges, "start_node", ["end_node"]
    )


@given('a GRAPH actor config with a subgraph node referencing "{ref_name}"')
def step_given_subgraph_ref(context: Context, ref_name: str) -> None:
    nodes = [
        NodeDefinition(
            id="main",
            type=NodeType.AGENT,
            name="Main",
            description="Main agent",
            config={"agent": "main_agent"},
        ),
        NodeDefinition(
            id="sub",
            type=NodeType.SUBGRAPH,
            name="Sub",
            description="Subgraph",
            config={"actor_ref": ref_name},
        ),
    ]
    edges = [EdgeDefinition(from_node="main", to_node="sub")]
    context.actor_config = _build_graph_config(
        "workflows/outer", nodes, edges, "main", ["sub"]
    )
    inner_nodes = [
        NodeDefinition(
            id="inner_agent",
            type=NodeType.AGENT,
            name="Inner",
            description="Inner agent",
            config={"agent": "inner"},
        ),
    ]
    inner = _build_graph_config(
        ref_name, inner_nodes, [], "inner_agent", ["inner_agent"]
    )
    context.actor_resolver = lambda name: inner if name == ref_name else None


@given("a GRAPH actor config with a single node")
def step_given_single_node(context: Context) -> None:
    nodes = [
        NodeDefinition(
            id="solo",
            type=NodeType.AGENT,
            name="Solo",
            description="Solo agent",
            config={"agent": "solo"},
        ),
    ]
    context.actor_config = _build_graph_config(
        "workflows/solo", nodes, [], "solo", ["solo"]
    )


@given('a GRAPH actor config "{outer_name}" referencing "{inner_name}"')
def step_given_outer_referencing_inner(
    context: Context, outer_name: str, inner_name: str
) -> None:
    nodes = [
        NodeDefinition(
            id="main",
            type=NodeType.AGENT,
            name="Main",
            description="Main agent",
            config={"agent": "main_agent"},
        ),
        NodeDefinition(
            id="sub",
            type=NodeType.SUBGRAPH,
            name="Sub",
            description="Subgraph",
            config={"actor_ref": inner_name},
        ),
    ]
    edges = [EdgeDefinition(from_node="main", to_node="sub")]
    context.actor_config = _build_graph_config(
        outer_name, nodes, edges, "main", ["sub"]
    )
    context.outer_name = outer_name
    context.inner_name = inner_name


@given('a resolver where "{inner_name}" references "{back_ref}"')
def step_given_resolver_cycle(context: Context, inner_name: str, back_ref: str) -> None:
    inner_nodes = [
        NodeDefinition(
            id="child",
            type=NodeType.SUBGRAPH,
            name="Child",
            description="Back-ref",
            config={"actor_ref": back_ref},
        ),
    ]
    inner = _build_graph_config(inner_name, inner_nodes, [], "child", ["child"])

    def resolver(name: str) -> ActorConfigSchema | None:
        if name == inner_name:
            return inner
        return None

    context.actor_resolver = resolver


@given('a resolver where "{inner_name}" has no subgraph nodes')
def step_given_resolver_no_subgraph(context: Context, inner_name: str) -> None:
    inner_nodes = [
        NodeDefinition(
            id="leaf",
            type=NodeType.AGENT,
            name="Leaf",
            description="Leaf agent",
            config={"agent": "leaf"},
        ),
    ]
    inner = _build_graph_config(inner_name, inner_nodes, [], "leaf", ["leaf"])

    def resolver(name: str) -> ActorConfigSchema | None:
        if name == inner_name:
            return inner
        return None

    context.actor_resolver = resolver


@given("an LLM actor config")
def step_given_llm_config(context: Context) -> None:
    context.actor_config = ActorConfigSchema(
        name="assistants/simple",
        type=ActorType.LLM,
        description="Simple LLM",
        model="gpt-4",
    )


@given("a GRAPH actor config with route set to None")
def step_given_graph_no_route(context: Context) -> None:
    context.actor_config = ActorConfigSchema.model_construct(
        name="workflows/broken",
        type=ActorType.GRAPH,
        description="No route",
        model="gpt-4",
        version="1.0",
        tools=[],
        memory=None,
        context=None,
        context_view=None,
        system_prompt=None,
        route=None,
        env_vars={},
    )


@given('a GRAPH actor config with nodes "alpha", "beta", and "gamma"')
def step_given_three_nodes(context: Context) -> None:
    nodes = [
        NodeDefinition(
            id="gamma",
            type=NodeType.AGENT,
            name="Gamma",
            description="G",
            config={"agent": "g"},
        ),
        NodeDefinition(
            id="alpha",
            type=NodeType.AGENT,
            name="Alpha",
            description="A",
            config={"agent": "a"},
        ),
        NodeDefinition(
            id="beta",
            type=NodeType.TOOL,
            name="Beta",
            description="B",
            config={"tool_name": "b/run"},
        ),
    ]
    edges = [
        EdgeDefinition(from_node="alpha", to_node="beta"),
        EdgeDefinition(from_node="beta", to_node="gamma"),
    ]
    context.actor_config = _build_graph_config(
        "workflows/tri", nodes, edges, "alpha", ["gamma"]
    )


@given('a GRAPH actor config with entry "{entry}" and exit "{exit_node}"')
def step_given_entry_exit(context: Context, entry: str, exit_node: str) -> None:
    nodes = [
        NodeDefinition(
            id=entry,
            type=NodeType.AGENT,
            name="Start",
            description="Start",
            config={"agent": "s"},
        ),
        NodeDefinition(
            id=exit_node,
            type=NodeType.AGENT,
            name="End",
            description="End",
            config={"agent": "e"},
        ),
    ]
    edges = [EdgeDefinition(from_node=entry, to_node=exit_node)]
    context.actor_config = _build_graph_config(
        "workflows/ee", nodes, edges, entry, [exit_node]
    )


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


@when("I compile the actor config")
def step_when_compile(context: Context) -> None:
    context.compile_error = None
    try:
        context.compiled = compile_actor(context.actor_config)
    except Exception as exc:
        context.compile_error = exc
        context.compiled = None


@when("I compile the actor config with a resolver")
def step_when_compile_with_resolver(context: Context) -> None:
    resolver = getattr(context, "actor_resolver", None)
    context.compile_error = None
    try:
        context.compiled = compile_actor(context.actor_config, actor_resolver=resolver)
    except Exception as exc:
        context.compile_error = exc
        context.compiled = None


@when("I attempt to compile the non-graph actor")
def step_when_compile_non_graph(context: Context) -> None:
    context.compile_error = None
    try:
        context.compiled = compile_actor(context.actor_config)
    except Exception as exc:
        context.compile_error = exc
        context.compiled = None


@when("I attempt to compile the routeless actor")
def step_when_compile_routeless(context: Context) -> None:
    context.compile_error = None
    try:
        context.compiled = compile_actor(context.actor_config)
    except Exception as exc:
        context.compile_error = exc
        context.compiled = None


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


@then("the compilation should succeed")
def step_then_succeed(context: Context) -> None:
    assert context.compile_error is None, (
        f"Expected compilation to succeed but got: {context.compile_error}"
    )
    assert context.compiled is not None


@then("the compiled actor should have {count:d} nodes")
def step_then_node_count(context: Context, count: int) -> None:
    assert context.compiled is not None
    assert len(context.compiled.nodes) == count, (
        f"Expected {count} nodes, got {len(context.compiled.nodes)}"
    )


@then('the compiled actor entry point should be "{entry}"')
def step_then_entry_point(context: Context, entry: str) -> None:
    assert context.compiled is not None
    assert context.compiled.entry_point == entry


@then('the compilation metadata should list node IDs "{id1}" and "{id2}"')
def step_then_metadata_node_ids(context: Context, id1: str, id2: str) -> None:
    assert context.compiled is not None
    node_ids = context.compiled.metadata.node_ids
    assert id1 in node_ids, f"{id1} not in {node_ids}"
    assert id2 in node_ids, f"{id2} not in {node_ids}"


@then("the compilation metadata should have {count:d} tool node")
def step_then_tool_node_count(context: Context, count: int) -> None:
    assert context.compiled is not None
    assert len(context.compiled.metadata.tool_nodes) == count


@then("the compilation metadata should have {count:d} LSP binding")
def step_then_lsp_binding_count(context: Context, count: int) -> None:
    assert context.compiled is not None
    assert len(context.compiled.metadata.lsp_bindings) == count


@then('the LSP binding should reference server "{server}"')
def step_then_lsp_server(context: Context, server: str) -> None:
    assert context.compiled is not None
    servers = [b.lsp_server_name for b in context.compiled.metadata.lsp_bindings]
    assert server in servers, f"{server} not in {servers}"


@then('the compilation metadata subgraph refs should map "{node}" to "{ref}"')
def step_then_subgraph_ref(context: Context, node: str, ref: str) -> None:
    assert context.compiled is not None
    refs = context.compiled.metadata.subgraph_refs
    assert refs.get(node) == ref, f"Expected {node}->{ref}, got {refs}"


@then("the compilation should fail with SubgraphCycleError")
def step_then_fail_cycle(context: Context) -> None:
    assert context.compile_error is not None
    assert isinstance(context.compile_error, SubgraphCycleError), (
        f"Expected SubgraphCycleError, got {type(context.compile_error).__name__}"
    )


@then("the compilation should fail with ActorCompilationError")
def step_then_fail_compilation(context: Context) -> None:
    assert context.compile_error is not None
    assert isinstance(context.compile_error, ActorCompilationError), (
        f"Expected ActorCompilationError, got {type(context.compile_error).__name__}"
    )


@then('the compiler error message should contain "{text}"')
def step_then_compiler_error_contains(context: Context, text: str) -> None:
    assert context.compile_error is not None
    msg = str(context.compile_error)
    assert text.lower() in msg.lower(), f"'{text}' not in '{msg}'"


@then("the metadata node IDs should be sorted alphabetically")
def step_then_sorted_ids(context: Context) -> None:
    assert context.compiled is not None
    ids = context.compiled.metadata.node_ids
    assert ids == sorted(ids), f"Node IDs not sorted: {ids}"


@then('the metadata entry node should be "{entry}"')
def step_then_metadata_entry(context: Context, entry: str) -> None:
    assert context.compiled is not None
    assert context.compiled.metadata.entry_node == entry


@then('the metadata exit nodes should contain "{exit_node}"')
def step_then_metadata_exit(context: Context, exit_node: str) -> None:
    assert context.compiled is not None
    assert exit_node in context.compiled.metadata.exit_nodes


@then("the compilation metadata should be JSON-serializable")
def step_then_json_serializable(context: Context) -> None:
    assert context.compiled is not None
    data = context.compiled.metadata.model_dump(mode="json")
    serialized = json.dumps(data)
    assert isinstance(serialized, str)
