# pyright: reportRedeclaration=false
"""Extended step definitions for v3 Actor YAML schema support.

Covers tool-actor extraction, update-mode overwriting, LSP-as-dict
bindings, and field-propagation scenarios (context_view, memory,
env_vars, response_format).  Core scenarios live in
``actor_v3_schema_steps.py``.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import patch

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

from cleveragents.actor.compiler import ActorCompilationError
from cleveragents.actor.config import ActorConfiguration
from cleveragents.core.exceptions import (
    ConfigurationError,
    ValidationError,
)
from cleveragents.domain.models.core.actor import Actor
from cleveragents.reactive.config_parser import ReactiveConfigParser


def _make_actor_ext(
    *,
    name: str = "local/test-actor",
    provider: str = "custom",
    model: str = "gpt-4",
    config_blob: dict[str, Any] | None = None,
) -> Actor:
    blob = config_blob or {}
    return Actor(
        id=1,
        name=name,
        provider=provider,
        model=model,
        config_blob=blob,
        config_hash=Actor.compute_hash(blob),
        graph_descriptor=None,
        unsafe=False,
        is_built_in=False,
        is_default=False,
        yaml_text=None,
        schema_version="1.0",
        compiled_metadata=None,
    )


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


@given("a v3 tool actor blob without model")
def step_v3_tool_blob_no_model(context: Any) -> None:
    # Tool actors may omit model in v3 schema, but from_blob requires
    # a model for ActorConfiguration.  The v3 extraction returns
    # provider="custom" and model="" which from_blob treats as missing.
    # The proper v3 path goes through ActorRegistry.add() / add_v3().
    context.v3_blob = {
        "name": "local/test-tool",
        "type": "tool",
        "description": "A test tool actor",
        "tools": ["local/file-ops"],
    }


@given("a v3 tool actor YAML text")
def step_v3_tool_yaml_text(context: Any) -> None:
    context.v3_tool_yaml_text = yaml.safe_dump(
        {
            "name": "local/test-tool",
            "type": "tool",
            "description": "A test tool actor",
            "tools": ["local/file-ops"],
        }
    )


@given("a v3 LLM actor config dict with lsp as dict")
def step_v3_llm_config_dict_lsp_dict(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-llm",
        "type": "llm",
        "description": "A test LLM actor",
        "model": "gpt-4",
        "lsp": {"auto": True, "languages": ["python"]},
    }


@given("a v3 LLM actor config dict with context_view and memory")
def step_v3_llm_config_with_context_view(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-llm",
        "type": "llm",
        "description": "A test LLM actor",
        "model": "gpt-4",
        "context_view": "executor",
        "memory": {"enabled": True, "max_messages": 50},
        "context": {"include_files": ["README.md"]},
        "env_vars": {"API_KEY": "test"},
        "response_format": {"type": "object"},
    }


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


@when("I call ActorRegistry.add with update=True for the v3 YAML")
def step_call_registry_add_v3_update(context: Any) -> None:
    context.result_actor = context.registry.add(context.v3_yaml_text, update=True)
    context.upsert_kwargs = context.mock_actor_service.upsert_actor.call_args


@when("I call ActorConfiguration.from_blob with the v3 tool blob")
def step_call_from_blob_v3_tool(context: Any) -> None:
    # from_blob requires model for ActorConfiguration, so tool actors
    # without model will raise ValueError.  This is expected — the
    # proper v3 path is through ActorRegistry.add() / add_v3().
    try:
        context.actor_config = ActorConfiguration.from_blob(
            blob=context.v3_blob,
            name="local/test",
        )
        context.from_blob_error = None
    except ValueError as exc:
        context.from_blob_error = exc
        context.actor_config = None


@when("I call ActorRegistry.add with the v3 tool YAML")
def step_call_registry_add_v3_tool(context: Any) -> None:
    context.result_actor = context.registry.add(context.v3_tool_yaml_text)
    context.upsert_kwargs = context.mock_actor_service.upsert_actor.call_args


@when("I check if the v3 LLM config is v3 format")
def step_check_v3_llm_format(context: Any) -> None:
    context.is_v3 = ReactiveConfigParser._is_v3_format(context.v3_config)


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


@then("the actor should be persisted successfully with update")
def step_actor_persisted_update(context: Any) -> None:
    assert context.result_actor is not None, "Expected actor to be persisted"
    call_kwargs = context.upsert_kwargs
    assert call_kwargs is not None, "upsert_actor was not called"
    # When update=True the duplicate-check branch (get_actor) is skipped,
    # so the actor is upserted directly without raising "already exists".
    # Verify the persisted blob contains expected v3 fields.
    _, kwargs = call_kwargs
    blob = kwargs.get("config_blob", {})
    assert blob.get("type") == "llm", (
        f"Expected type 'llm' in config_blob, got '{blob.get('type')}'"
    )
    assert kwargs.get("name") == "local/test-llm", (
        f"Expected name 'local/test-llm', got '{kwargs.get('name')}'"
    )


@then("the tool actor from_blob should raise ValueError for missing model")
def step_tool_from_blob_raises(context: Any) -> None:
    assert context.from_blob_error is not None, (
        "Expected ValueError for tool actor without model in from_blob"
    )
    error_msg = str(context.from_blob_error).lower()
    assert "model" in error_msg, (
        f"Error should mention 'model': {context.from_blob_error}"
    )
    # Verify the error message guides users to the correct registration path.
    assert "actorregistry.add" in error_msg or "agents actor add" in error_msg, (
        f"Error should mention ActorRegistry.add() or 'agents actor add': "
        f"{context.from_blob_error}"
    )


@then("the tool actor should be persisted with type tool")
def step_tool_actor_persisted(context: Any) -> None:
    call_kwargs = context.upsert_kwargs
    assert call_kwargs is not None, "upsert_actor was not called"
    _, kwargs = call_kwargs
    blob = kwargs.get("config_blob", {})
    assert blob.get("type") == "tool", (
        f"Expected type 'tool' in config_blob, got '{blob.get('type')}'"
    )


@then("the agent config should contain lsp as dict")
def step_agent_config_has_lsp_dict(context: Any) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    lsp = agent.config.get("lsp")
    assert isinstance(lsp, dict), f"Expected lsp as dict, got {type(lsp)}"
    assert lsp.get("auto") is True, f"Expected lsp.auto=True, got {lsp}"


@then("the agent config should contain context_view")
def step_agent_config_has_context_view(context: Any) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    assert agent.config.get("context_view") == "executor", (
        f"Expected context_view 'executor', got '{agent.config.get('context_view')}'"
    )


@then("the agent config should contain memory settings")
def step_agent_config_has_memory(context: Any) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    memory = agent.config.get("memory")
    assert isinstance(memory, dict), f"Expected memory as dict, got {type(memory)}"
    assert memory.get("enabled") is True


@then("the agent config should contain env_vars")
def step_agent_config_has_env_vars(context: Any) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    env_vars = agent.config.get("env_vars")
    assert isinstance(env_vars, dict), (
        f"Expected env_vars as dict, got {type(env_vars)}"
    )
    assert env_vars.get("API_KEY") == "test"


@then("the agent config should contain response_format")
def step_agent_config_has_response_format(context: Any) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    rf = agent.config.get("response_format")
    assert isinstance(rf, dict), f"Expected response_format as dict, got {type(rf)}"
    assert rf.get("type") == "object"


@then("it should be detected as v3")
def step_is_v3(context: Any) -> None:
    assert context.is_v3 is True, "Expected v3 data to be detected as v3"


# ── Cycle 2 review: defensive guard BDD steps ───────────────────────────


@given("a v3 graph actor config dict with invalid entry_node")
def step_v3_graph_invalid_entry(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-graph",
        "type": "graph",
        "description": "Graph with bad entry_node",
        "model": "gpt-4",
        "route": {
            "nodes": [
                {
                    "id": "planner",
                    "type": "agent",
                    "name": "Planner",
                    "description": "Plans",
                    "config": {},
                },
            ],
            "edges": [],
            "entry_node": "nonexistent_node",
            "exit_nodes": ["planner"],
        },
    }


@given("a v3 graph actor config dict with incomplete edges")
def step_v3_graph_incomplete_edges(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-graph",
        "type": "graph",
        "description": "Graph with incomplete edges",
        "model": "gpt-4",
        "route": {
            "nodes": [
                {
                    "id": "planner",
                    "type": "agent",
                    "name": "Planner",
                    "description": "Plans",
                    "config": {},
                },
                {
                    "id": "executor",
                    "type": "tool",
                    "name": "Executor",
                    "description": "Executes",
                    "config": {},
                },
            ],
            "edges": [
                {"from_node": "planner", "to_node": "executor"},
                {"from_node": "planner", "to_node": ""},
                {"from_node": "", "to_node": "executor"},
            ],
            "entry_node": "planner",
            "exit_nodes": ["executor"],
        },
    }
    context.raw_edge_count = 3


@given("a v3 graph actor config dict with non-dict edges")
def step_v3_graph_non_dict_edges(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-graph",
        "type": "graph",
        "description": "Graph with non-dict edges",
        "model": "gpt-4",
        "route": {
            "nodes": [
                {
                    "id": "planner",
                    "type": "agent",
                    "name": "Planner",
                    "description": "Plans",
                    "config": {},
                },
                {
                    "id": "executor",
                    "type": "tool",
                    "name": "Executor",
                    "description": "Executes",
                    "config": {},
                },
            ],
            "edges": [
                {"from_node": "planner", "to_node": "executor"},
                "not-a-dict",
                42,
            ],
            "entry_node": "planner",
            "exit_nodes": ["executor"],
        },
    }


@when("I parse the v3 config through ReactiveConfigParser expecting error")
def step_parse_v3_config_error(context: Any) -> None:
    parser = ReactiveConfigParser()
    try:
        parser._build(context.v3_config)
        context.parse_error = None
    except ConfigurationError as exc:
        context.parse_error = exc


@then("a ConfigurationError should be raised mentioning entry_node")
def step_config_error_entry_node(context: Any) -> None:
    assert context.parse_error is not None, "Expected a ConfigurationError to be raised"
    msg = str(context.parse_error).lower()
    assert "entry_node" in msg, (
        f"Error should mention 'entry_node': {context.parse_error}"
    )


@then("the graph route should have fewer edges than the raw input")
def step_fewer_edges(context: Any) -> None:
    routes = context.reactive_config.routes
    route = next(iter(routes.values()))
    assert len(route.edges) < context.raw_edge_count, (
        f"Expected fewer edges than {context.raw_edge_count}, got {len(route.edges)}"
    )
    assert len(route.edges) == 1, (
        f"Expected exactly 1 valid edge, got {len(route.edges)}"
    )


@then("the graph route should have only valid dict edges")
def step_only_valid_edges(context: Any) -> None:
    routes = context.reactive_config.routes
    route = next(iter(routes.values()))
    assert len(route.edges) == 1, (
        f"Expected 1 valid edge (non-dict filtered), got {len(route.edges)}"
    )


# ── Coverage gap: v3_registry.py ─────────────────────────────────────────


@given("a v3 LLM actor YAML text with bare name")
def step_v3_llm_yaml_bare_name(context: Any) -> None:
    context.v3_yaml_bare = yaml.safe_dump(
        {
            "name": "bare-actor",  # no namespace slash — should get local/ prefix
            "type": "llm",
            "description": "A bare-named LLM actor",
            "model": "gpt-4",
        }
    )


@given("a v3 LLM actor YAML text marked unsafe")
def step_v3_llm_yaml_unsafe(context: Any) -> None:
    context.v3_yaml_unsafe = yaml.safe_dump(
        {
            "name": "local/unsafe-actor",
            "type": "llm",
            "description": "An unsafe LLM actor",
            "model": "gpt-4",
            "unsafe": True,
        }
    )


@when("I call ActorRegistry.add with the bare-name v3 YAML")
def step_call_registry_add_bare_name(context: Any) -> None:
    context.result_actor = context.registry.add(context.v3_yaml_bare)
    context.upsert_kwargs = context.mock_actor_service.upsert_actor.call_args


@when("I call ActorRegistry.add with the unsafe v3 YAML")
def step_call_registry_add_unsafe(context: Any) -> None:
    try:
        context.registry.add(context.v3_yaml_unsafe)
        context.add_error = None
    except ValidationError as exc:
        context.add_error = exc


@when("I call ActorRegistry.add with the v3 YAML expecting error")
def step_call_registry_add_v3_expect_error(context: Any) -> None:
    try:
        context.registry.add(context.v3_yaml_text)
        context.add_error = None
    except ValidationError as exc:
        context.add_error = exc


@when("I call ActorRegistry.add with the v3 graph YAML and compilation fails")
def step_call_registry_add_v3_graph_compile_fail(context: Any) -> None:
    with patch(
        "cleveragents.actor.v3_registry.compile_actor",
        side_effect=ActorCompilationError("simulated compilation failure"),
    ):
        context.result_actor = context.registry.add(context.v3_graph_yaml_text)
        context.upsert_kwargs = context.mock_actor_service.upsert_actor.call_args


@then("the persisted actor name should be prefixed with local")
def step_actor_name_has_local_prefix(context: Any) -> None:
    call_kwargs = context.upsert_kwargs
    assert call_kwargs is not None, "upsert_actor was not called"
    _, kwargs = call_kwargs
    name = kwargs.get("name", "")
    assert name.startswith("local/"), (
        f"Expected name to start with 'local/', got '{name}'"
    )


@then("a ValidationError should be raised mentioning unsafe")
def step_validation_error_unsafe(context: Any) -> None:
    assert context.add_error is not None, "Expected a ValidationError to be raised"
    msg = str(context.add_error).lower()
    assert "unsafe" in msg, f"Error should mention 'unsafe': {context.add_error}"


@then("a ValidationError should be raised mentioning already exists")
def step_validation_error_already_exists(context: Any) -> None:
    assert context.add_error is not None, "Expected a ValidationError to be raised"
    msg = str(context.add_error).lower()
    assert "already exists" in msg, (
        f"Error should mention 'already exists': {context.add_error}"
    )


@then("the actor should still be persisted without compiled metadata")
def step_actor_persisted_no_compiled_metadata(context: Any) -> None:
    assert context.result_actor is not None, "Expected actor to be persisted"
    call_kwargs = context.upsert_kwargs
    assert call_kwargs is not None, "upsert_actor was not called"
    _, kwargs = call_kwargs
    # compiled_metadata should be None because compilation failed
    assert kwargs.get("compiled_metadata") is None, (
        f"Expected compiled_metadata=None after compilation failure, "
        f"got {kwargs.get('compiled_metadata')}"
    )


# ── Coverage gap: registry.py ─────────────────────────────────────────────


@when("I call ActorRegistry.get with a bare actor name")
def step_call_registry_get_bare(context: Any) -> None:
    context.mock_actor_service.get_actor.side_effect = None
    context.mock_actor_service.get_actor.return_value = _make_actor_ext(
        name="local/bare-actor"
    )
    context.registry.get("bare-actor")


@then("the actor service should be queried with local-prefixed name")
def step_actor_service_queried_local(context: Any) -> None:
    call_args = context.mock_actor_service.get_actor.call_args
    assert call_args is not None, "get_actor was not called"
    args, _ = call_args
    queried_name = args[0] if args else ""
    assert queried_name == "local/bare-actor", (
        f"Expected 'local/bare-actor', got '{queried_name}'"
    )


@when("I call ActorRegistry.add with a non-mapping YAML string")
def step_call_registry_add_non_mapping(context: Any) -> None:
    # Patch load_yaml_text to return a list so the isinstance guard fires.
    with patch(
        "cleveragents.actor.registry.ActorConfiguration.load_yaml_text",
        return_value=["not", "a", "dict"],
    ):
        try:
            context.registry.add("irrelevant")
            context.add_error = None
        except ValidationError as exc:
            context.add_error = exc


@then("a ValidationError should be raised mentioning mapping")
def step_validation_error_mapping(context: Any) -> None:
    assert context.add_error is not None, "Expected a ValidationError to be raised"
    msg = str(context.add_error).lower()
    assert "mapping" in msg, f"Error should mention 'mapping': {context.add_error}"


@when("I call ActorRegistry.upsert_actor with an invalid v3 config_blob")
def step_call_registry_upsert_invalid_v3(context: Any) -> None:
    # Provide a blob that triggers is_v3_yaml() but fails ActorConfigSchema.
    invalid_blob: dict[str, Any] = {
        "name": "local/bad-v3",
        "version": "3.0",
        "type": "invalid_type_value",
        "provider": "openai",
        "model": "gpt-4",
    }
    try:
        context.registry.upsert_actor(
            name="local/bad-v3",
            config_blob=invalid_blob,
            provider="openai",
            model="gpt-4",
        )
        context.add_error = None
    except ValidationError as exc:
        context.add_error = exc


@then("a ValidationError should be raised mentioning invalid v3 actor")
def step_validation_error_invalid_v3(context: Any) -> None:
    assert context.add_error is not None, "Expected a ValidationError to be raised"
    msg = str(context.add_error).lower()
    assert "invalid" in msg or "v3" in msg or "validation" in msg, (
        f"Error should mention invalid v3 actor: {context.add_error}"
    )


# ── Coverage gap: config_parser.py ───────────────────────────────────────


@when("I merge a None dict into a base config")
def step_merge_none_dict(context: Any) -> None:
    parser = ReactiveConfigParser()
    base: dict[str, Any] = {"key": "value"}
    parser._merge(base, None)  # type: ignore[arg-type]
    context.merge_base = base


@then("the base config should remain unchanged")
def step_base_config_unchanged(context: Any) -> None:
    assert context.merge_base == {"key": "value"}, (
        f"Expected base unchanged, got {context.merge_base}"
    )


@given("a v3 LLM actor config dict with no model field")
def step_v3_llm_config_no_model(context: Any) -> None:
    context.v3_config = {
        "name": "local/no-model",
        "type": "llm",
        "description": "LLM actor without model",
        # no model field
    }


@then("a ConfigurationError should be raised mentioning model")
def step_config_error_model(context: Any) -> None:
    assert context.parse_error is not None, "Expected a ConfigurationError to be raised"
    msg = str(context.parse_error).lower()
    assert "model" in msg, f"Error should mention 'model': {context.parse_error}"


@given("a v3 LLM actor config dict with lsp_capabilities and lsp_context_enrichment")
def step_v3_llm_config_lsp_caps(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-llm",
        "type": "llm",
        "description": "LLM actor with LSP capabilities",
        "model": "gpt-4",
        "lsp_capabilities": ["hover", "completion"],
        "lsp_context_enrichment": {"include_diagnostics": True},
    }


@then("the agent config should contain lsp_capabilities")
def step_agent_config_has_lsp_caps(context: Any) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    caps = agent.config.get("lsp_capabilities")
    assert caps is not None, "Expected lsp_capabilities in agent config"
    assert caps == ["hover", "completion"], f"Unexpected lsp_capabilities: {caps}"


@then("the agent config should contain lsp_context_enrichment")
def step_agent_config_has_lsp_enrichment(context: Any) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    enrichment = agent.config.get("lsp_context_enrichment")
    assert isinstance(enrichment, dict), (
        f"Expected lsp_context_enrichment as dict, got {type(enrichment)}"
    )
    assert enrichment.get("include_diagnostics") is True


@given("a v3 graph actor config dict without route")
def step_v3_graph_config_no_route(context: Any) -> None:
    context.v3_config = {
        "name": "local/no-route-graph",
        "type": "graph",
        "description": "Graph actor without route",
        "model": "gpt-4",
        # no route key
    }


@then("a ConfigurationError should be raised mentioning route")
def step_config_error_route(context: Any) -> None:
    assert context.parse_error is not None, "Expected a ConfigurationError to be raised"
    msg = str(context.parse_error).lower()
    assert "route" in msg, f"Error should mention 'route': {context.parse_error}"


@given("a v3 graph actor config dict with malformed nodes")
def step_v3_graph_malformed_nodes(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-graph",
        "type": "graph",
        "description": "Graph with malformed nodes",
        "model": "gpt-4",
        "route": {
            "nodes": [
                "not-a-dict",  # non-dict node — should be skipped
                {
                    "id": "",
                    "type": "agent",
                    "config": {},
                },  # empty id — should be skipped
                {
                    "id": "valid-node",
                    "type": "agent",
                    "name": "Valid",
                    "description": "Valid node",
                    "config": {},
                },
            ],
            "edges": [],
            "entry_node": "valid-node",
        },
    }


@then("the reactive config should only have agents for valid nodes")
def step_reactive_only_valid_agents(context: Any) -> None:
    agents = context.reactive_config.agents
    assert "valid-node" in agents, (
        f"Expected 'valid-node' in agents: {list(agents.keys())}"
    )
    # non-dict and empty-id nodes must not appear
    assert len(agents) == 1, (
        f"Expected exactly 1 agent (only valid-node), got {list(agents.keys())}"
    )


@given("a v3 graph actor config dict with skills and lsp")
def step_v3_graph_config_with_skills_lsp(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-graph",
        "type": "graph",
        "description": "Graph actor with skills and lsp",
        "model": "gpt-4",
        "skills": ["local/file-ops", "local/code-search"],
        "lsp": ["local/pyright"],
        "route": {
            "nodes": [
                {
                    "id": "planner",
                    "type": "agent",
                    "name": "Planner",
                    "description": "Plans",
                    "config": {},
                },
                {
                    "id": "executor",
                    "type": "tool",
                    "name": "Executor",
                    "description": "Executes",
                    "config": {},
                },
            ],
            "edges": [{"from_node": "planner", "to_node": "executor"}],
            "entry_node": "planner",
        },
    }


@given("a v3 graph actor config dict with skills and lsp as dict")
def step_v3_graph_config_with_skills_lsp_dict(context: Any) -> None:
    context.v3_config = {
        "name": "local/test-graph",
        "type": "graph",
        "description": "Graph actor with skills and lsp as dict",
        "model": "gpt-4",
        "skills": ["local/file-ops"],
        "lsp": {"auto": True, "languages": ["python"]},  # dict form — hits line 330
        "route": {
            "nodes": [
                {
                    "id": "planner",
                    "type": "agent",
                    "name": "Planner",
                    "description": "Plans",
                    "config": {},
                },
                {
                    "id": "executor",
                    "type": "tool",
                    "name": "Executor",
                    "description": "Executes",
                    "config": {},
                },
            ],
            "edges": [{"from_node": "planner", "to_node": "executor"}],
            "entry_node": "planner",
        },
    }


@then("each graph node agent should have skills propagated")
def step_graph_nodes_have_skills(context: Any) -> None:
    agents = context.reactive_config.agents
    assert "planner" in agents, f"Missing 'planner' agent: {list(agents.keys())}"
    assert "executor" in agents, f"Missing 'executor' agent: {list(agents.keys())}"
    for node_id, agent in agents.items():
        skills = agent.config.get("skills", [])
        assert "local/file-ops" in skills, (
            f"Node '{node_id}' missing 'local/file-ops' in skills: {skills}"
        )


@then("each graph node agent should have lsp propagated")
def step_graph_nodes_have_lsp(context: Any) -> None:
    agents = context.reactive_config.agents
    for node_id, agent in agents.items():
        lsp = agent.config.get("lsp")
        assert lsp is not None, f"Node '{node_id}' missing lsp binding"
        assert "local/pyright" in lsp, (
            f"Node '{node_id}' missing 'local/pyright' in lsp: {lsp}"
        )


@then("each graph node agent should have lsp dict propagated")
def step_graph_nodes_have_lsp_dict(context: Any) -> None:
    agents = context.reactive_config.agents
    assert len(agents) >= 1, "Expected at least one agent"
    for node_id, agent in agents.items():
        lsp = agent.config.get("lsp")
        assert isinstance(lsp, dict), (
            f"Node '{node_id}' expected lsp as dict, got {type(lsp)}: {lsp}"
        )
        assert lsp.get("auto") is True, (
            f"Node '{node_id}' expected lsp.auto=True, got {lsp}"
        )


# ── M5: options block forwarding (#11223) ─────────────────────────────────


@given("a v3 LLM actor config dict with options block")
def step_v3_llm_config_with_options(context: Context) -> None:
    context.v3_config = {
        "name": "local/my-llama-actor",
        "type": "llm",
        "description": "Local llama.cpp actor",
        "provider": "openai",
        "model": "my-local-model",
        "options": {
            "openai_api_base": "http://localhost:8080/v1",
            "openai_api_key": "none",
            "temperature": 1.0,
        },
        "system_prompt": "You are a helpful assistant.",
    }


@given("a v3 LLM actor config dict without options block")
def step_v3_llm_config_without_options(context: Context) -> None:
    context.v3_config = {
        "name": "local/standard-actor",
        "type": "llm",
        "description": "Standard actor without options",
        "provider": "openai",
        "model": "gpt-4",
        "system_prompt": "You are a helpful assistant.",
    }


@then("the agent config should contain the options block")
def step_agent_config_has_options(context: Context) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    options = agent.config.get("options")
    assert options == {
        "openai_api_base": "http://localhost:8080/v1",
        "openai_api_key": "none",
        "temperature": 1.0,
    }, f"Expected exact options dict, got {options}"


@then("the agent config should not contain an options key")
def step_agent_config_no_options(context: Context) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    assert "options" not in agent.config, (
        f"Expected no 'options' key in agent config, got {agent.config}"
    )


@given("a v3 LLM actor config dict with empty options block")
def step_v3_llm_config_with_empty_options(context: Context) -> None:
    context.v3_config = {
        "name": "local/empty-options-actor",
        "type": "llm",
        "description": "Actor with empty options",
        "provider": "openai",
        "model": "gpt-4",
        "options": {},
        "system_prompt": "You are a helpful assistant.",
    }


@then("the agent config should contain an empty options block")
def step_agent_config_has_empty_options(context: Context) -> None:
    agents = context.reactive_config.agents
    agent = next(iter(agents.values()))
    options = agent.config.get("options")
    assert options == {}, f"Expected empty options dict, got {options}"
