"""Step definitions for spec-compliant actor YAML format tests."""

from __future__ import annotations

import ast
from typing import Any
from unittest.mock import MagicMock

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

from cleveragents.actor.registry import ActorRegistry
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core.actor import Actor


# TODO(#10832): Deduplicate — shared copies exist in actor_registry_steps.py
# and actor_registry_persistence_steps.py.
class _StubActorService:
    """Minimal actor service stub for registry tests."""

    def __init__(self) -> None:
        self.actors: dict[str, Actor] = {}
        self.default_actor_name: str | None = None

    def upsert_actor(
        self,
        *,
        name: str,
        provider: str,
        model: str,
        config_blob: dict[str, Any] | None = None,
        graph_descriptor: dict[str, Any] | None = None,
        unsafe: bool = False,
        set_default: bool = False,
        is_built_in: bool = False,
        yaml_text: str | None = None,
        schema_version: str | None = None,
        compiled_metadata: dict[str, Any] | None = None,
    ) -> Actor:
        blob = config_blob or {}
        actor = Actor(
            id=None,
            name=name,
            provider=provider,
            model=model,
            config_blob=blob,
            config_hash=Actor.compute_hash(blob),
            graph_descriptor=graph_descriptor,
            yaml_text=yaml_text,
            schema_version=schema_version or "1.0",
            compiled_metadata=compiled_metadata,
            unsafe=unsafe,
            is_built_in=is_built_in,
            is_default=False,
        )
        self.actors[name] = actor
        if set_default:
            self.default_actor_name = name
        return actor

    def get_default_actor(self) -> Actor | None:
        if self.default_actor_name and self.default_actor_name in self.actors:
            return self.actors[self.default_actor_name]
        return None

    def set_default_actor(self, name: str) -> Actor:
        actor = self.actors.get(name)
        if actor is None:
            raise ValueError(f"Actor {name!r} does not exist")
        self.default_actor_name = name
        return actor

    def get_actor(self, name: str) -> Actor:
        actor = self.actors.get(name)
        if actor is None:
            raise NotFoundError(f"Actor {name!r} not found")
        return actor

    def list_actors(self) -> list[Actor]:
        return list(self.actors.values())

    def remove_actor(self, name: str) -> None:
        self.actors.pop(name, None)


def _make_registry_no_providers(context: Context) -> None:
    """Build an ActorRegistry with no configured providers."""
    context.spec_actor_service = _StubActorService()
    provider_reg = MagicMock()
    provider_reg.get_configured_providers.return_value = []
    settings = MagicMock()
    settings.resolve_provider_defaults.return_value = MagicMock(
        provider=None, model=None
    )
    context.spec_registry = ActorRegistry(
        actor_service=context.spec_actor_service,
        provider_registry=provider_reg,
        settings=settings,
    )


# ── Given ────────────────────────────────────────────────────────────


@given("a spec-yaml actor registry with no providers")
def step_spec_yaml_registry(context: Context) -> None:
    _make_registry_no_providers(context)


# ── When: registry.add() ────────────────────────────────────────────


@when("I add a spec-compliant YAML with actors map and combined actor field")
def step_add_actors_combined(context: Context) -> None:
    yaml_text = (
        "name: local/my-assistant\n"
        "type: llm\n"
        "description: A helpful assistant\n"
        "actors:\n"
        "  my_assistant:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: openai\n"
        "      model: gpt-4\n"
        "      system_prompt: You are helpful.\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when("I add a spec-compliant YAML with actors map and separate provider model")
def step_add_actors_separate(context: Context) -> None:
    yaml_text = (
        "name: local/my-assistant\n"
        "description: A helpful assistant\n"
        "actors:\n"
        "  my_assistant:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: anthropic\n"
        "      model: claude-3\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when(
    "I add a YAML with type only in nested actors map and provider/model in nested config"
)
def step_add_nested_type_only(context: Context) -> None:
    yaml_text = (
        "name: local/fpga-strategist\n"
        "description: An FPGA strategist agent\n"
        "actors:\n"
        "  fpga-strategist:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: custom\n"
        "      model: fpga-model\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when("I add a spec-compliant YAML with actors map and unsafe flag")
def step_add_actors_unsafe(context: Context) -> None:
    yaml_text = (
        "name: local/unsafe-assistant\n"
        "type: llm\n"
        "description: An unsafe assistant\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "unsafe: true\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, unsafe=True)


@when("I add a YAML with legacy agents map format")
def step_add_agents_legacy(context: Context) -> None:
    yaml_text = (
        "name: local/legacy-agent\n"
        "type: llm\n"
        "description: A legacy agent\n"
        "provider: openai\n"
        "model: gpt-4o\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when("I add a YAML with top-level provider and model fields")
def step_add_top_level(context: Context) -> None:
    yaml_text = (
        "name: local/simple-actor\n"
        "type: llm\n"
        "description: A simple actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when("I add a YAML with top-level provider only and model in nested actors map")
def step_add_top_provider_nested_model(context: Context) -> None:
    yaml_text = (
        "name: local/partial-actor\n"
        "type: llm\n"
        "description: A partial actor\n"
        "provider: top-level-provider\n"
        "model: nested-model\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when("I add a YAML with top-level model only and provider in nested actors map")
def step_add_top_model_nested_provider(context: Context) -> None:
    yaml_text = (
        "name: local/partial-actor\n"
        "type: llm\n"
        "description: A partial actor\n"
        "provider: nested-provider\n"
        "model: top-level-model\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when("I attempt to add a YAML with no provider or model anywhere")
def step_add_no_provider_model(context: Context) -> None:
    yaml_text = "name: local/empty-actor\ndescription: No provider\n"
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


@when(
    "I add a YAML with top-level provider and model and nested actors map with unsafe flag"
)
def step_add_top_level_with_nested_unsafe(context: Context) -> None:
    yaml_text = (
        "name: local/top-level-actor\n"
        "description: An actor with nested unsafe\n"
        "type: llm\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "unsafe: true\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, unsafe=True)


@when(
    "I add a YAML with top-level provider and model and nested actors map with graph descriptor"
)
def step_add_top_level_with_nested_graph(context: Context) -> None:
    yaml_text = (
        "name: local/top-level-actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "actors:\n"
        "  my_assistant:\n"
        "    type: llm\n"
        "    config:\n"
        "      actor: anthropic/claude-3\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when("I attempt to add an unsafe YAML without the unsafe flag")
def step_add_unsafe_without_flag(context: Context) -> None:
    yaml_text = (
        "name: local/unsafe-actor\n"
        "type: llm\n"
        "description: An unsafe actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "unsafe: true\n"
    )
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


@when("I add an unsafe YAML with the unsafe flag set")
def step_add_unsafe_with_flag(context: Context) -> None:
    yaml_text = (
        "name: local/unsafe-actor\n"
        "type: llm\n"
        "description: An unsafe actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "unsafe: true\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, unsafe=True)


@when("I add an unsafe YAML with the allow_unsafe flag set")
def step_add_unsafe_with_allow_flag(context: Context) -> None:
    yaml_text = (
        "name: local/unsafe-actor\n"
        "type: llm\n"
        "description: An unsafe actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "unsafe: true\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, allow_unsafe=True)


@when(
    'I add the same actor again with update=True and provider "{provider}" and model "{model}"'
)
def step_add_same_actor_update(context: Context, provider: str, model: str) -> None:
    yaml_text = (
        "name: local/my-assistant\n"
        "type: llm\n"
        "description: A test assistant\n"
        f"provider: {provider}\n"
        f"model: {model}\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, update=True)


@when("I attempt to add the same actor again without update=True")
def step_add_same_actor_no_update(context: Context) -> None:
    yaml_text = (
        "name: local/my-assistant\n"
        "type: llm\n"
        "description: A test assistant\n"
        "provider: openai\n"
        "model: gpt-4\n"
    )
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


@when(
    'I add a spec-compliant YAML with schema_version "{version}" and compiled_metadata'
)
def step_add_with_schema_version_and_metadata(context: Context, version: str) -> None:
    yaml_text = (
        "name: local/versioned-actor\n"
        "type: llm\n"
        "description: A versioned actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
    )
    context.spec_result = context.spec_registry.add(
        yaml_text,
        schema_version=version,
        compiled_metadata={"key": "val"},
    )


# ── When: registry.add() missing name ────────────────────────────────


@when("I attempt to add a YAML without a name field")
def step_add_no_name(context: Context) -> None:
    yaml_text = "type: llm\ndescription: No name\nprovider: openai\nmodel: gpt-4\n"
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


# ── When: top-level unsafe ───────────────────────────────────────────


@when("I attempt to add a YAML with top-level unsafe true and no flag")
def step_add_top_level_unsafe_no_flag(context: Context) -> None:
    yaml_text = (
        "name: local/unsafe-top\n"
        "type: llm\n"
        "description: An unsafe actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "unsafe: true\n"
    )
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


@when("I add a YAML with top-level unsafe true and the unsafe flag set")
def step_add_top_level_unsafe_with_flag(context: Context) -> None:
    yaml_text = (
        "name: local/unsafe-top\n"
        "type: llm\n"
        "description: An unsafe actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "unsafe: true\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, unsafe=True)


# ── When: multi-actor unsafe limitation ──────────────────────────────


@when("I attempt to add a multi-actor YAML with two actor entries")
def step_add_multi_actor_rejected(context: Context) -> None:
    yaml_text = (
        "name: local/multi-actor\n"
        "actors:\n"
        "  first_actor:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: openai\n"
        "      model: gpt-4\n"
        "  second_actor:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: anthropic\n"
        "      model: claude-3\n"
        "      unsafe: true\n"
    )
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


@when("I attempt to add a YAML with actors null and multi-entry agents map")
def step_add_multi_actor_agents_fallback_rejected(context: Context) -> None:
    yaml_text = (
        "name: local/multi-agent-fallback\n"
        "actors: null\n"
        "agents:\n"
        "  first_agent:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: openai\n"
        "      model: gpt-4\n"
        "  second_agent:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: anthropic\n"
        "      model: claude-3\n"
    )
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


# ── When: _extract_v2_actor direct calls ─────────────────────────────


@when("I call _extract_v2_actor with an actors map containing combined actor field")
def step_extract_actors_combined(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {"my_assistant": {"type": "llm", "config": {"actor": "openai/gpt-4"}}}
    }
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with an actors map containing separate provider model")
def step_extract_actors_separate(context: Context) -> None:
    context.spec_extracted_provider = "anthropic"
    context.spec_extracted_model = "claude-3"
    context.spec_extracted_graph = {
        "actors": {
            "my_assistant": {
                "type": "llm",
                "config": {"provider": "anthropic", "model": "claude-3"},
            }
        }
    }
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with both actors and agents maps")
def step_extract_both_maps(context: Context) -> None:
    context.spec_extracted_provider = "actors-provider"
    context.spec_extracted_model = "actors-model"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"provider": "actors-provider", "model": "actors-model"}}
        }
    }
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with an actors map containing unsafe true")
def step_extract_actors_unsafe_true(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"provider": "openai", "model": "gpt-4", "unsafe": True}}
        }
    }
    context.spec_extracted_unsafe = True


@when("I call _extract_v2_actor with an empty dict")
def step_extract_empty(context: Context) -> None:
    context.spec_extracted_provider = None
    context.spec_extracted_model = None
    context.spec_extracted_graph = None
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with actors key containing empty map")
def step_extract_actors_empty_map(context: Context) -> None:
    context.spec_extracted_provider = None
    context.spec_extracted_model = None
    context.spec_extracted_graph = None
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with actors key containing None value")
def step_extract_actors_none_value(context: Context) -> None:
    context.spec_extracted_provider = None
    context.spec_extracted_model = None
    context.spec_extracted_graph = None
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with an actors map where actor field has no slash")
def step_extract_no_slash(context: Context) -> None:
    context.spec_extracted_provider = None
    context.spec_extracted_model = None
    context.spec_extracted_graph = {
        "actors": {"a": {"config": {"actor": "no-slash-here"}}}
    }
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with an actors map where both actor and provider exist")
def step_extract_actor_and_provider(context: Context) -> None:
    context.spec_extracted_provider = "explicit-provider"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"actor": "openai/gpt-4", "provider": "explicit-provider"}}
        }
    }
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with an agents map containing combined actor field")
def step_extract_agents_combined(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "agents": {"my_agent": {"type": "llm", "config": {"actor": "openai/gpt-4"}}}
    }
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with a non-dict first entry in actors map")
def step_extract_non_dict_entry(context: Context) -> None:
    context.spec_extracted_provider = None
    context.spec_extracted_model = None
    context.spec_extracted_graph = None
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with a dict entry missing config block")
def step_extract_missing_config(context: Context) -> None:
    context.spec_extracted_provider = None
    context.spec_extracted_model = None
    context.spec_extracted_graph = None
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with empty actors dict and valid agents map")
def step_extract_empty_actors_with_agents(context: Context) -> None:
    context.spec_extracted_provider = None
    context.spec_extracted_model = None
    context.spec_extracted_graph = None
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with actors as a list")
def step_extract_actors_list(context: Context) -> None:
    context.spec_extracted_provider = None
    context.spec_extracted_model = None
    context.spec_extracted_graph = None
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with an actors map where both actor and model exist")
def step_extract_actor_and_model(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "explicit-model"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"actor": "openai/gpt-4", "model": "explicit-model"}}
        }
    }
    context.spec_extracted_unsafe = False


@when(
    "I call _extract_v2_actor with an actors map where actor field has empty provider"
)
def step_extract_empty_provider_part(context: Context) -> None:
    context.spec_extracted_provider = None
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {"actors": {"a": {"config": {"actor": "/gpt-4"}}}}
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with an actors map where actor field has empty model")
def step_extract_empty_model_part(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = None
    context.spec_extracted_graph = {"actors": {"a": {"config": {"actor": "openai/"}}}}
    context.spec_extracted_unsafe = False


# ── When: unsafe coercion edge cases (unsafe coercion) ───────────────


@when('I call _extract_v2_actor with unsafe value "no"')
def step_extract_unsafe_string_no(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"provider": "openai", "model": "gpt-4", "unsafe": "no"}}
        }
    }
    context.spec_extracted_unsafe = False


@when('I call _extract_v2_actor with unsafe value "yes"')
def step_extract_unsafe_string_yes(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"provider": "openai", "model": "gpt-4", "unsafe": "yes"}}
        }
    }
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with unsafe value 1")
def step_extract_unsafe_integer_1(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"provider": "openai", "model": "gpt-4", "unsafe": 1}}
        }
    }
    context.spec_extracted_unsafe = True


@when("I add a YAML with actors map where unsafe is integer 1 and the unsafe flag set")
def step_add_actors_unsafe_integer_1(context: Context) -> None:
    yaml_text = (
        "name: local/unsafe-int-actor\n"
        "type: llm\n"
        "description: An unsafe actor with integer flag\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "unsafe: 1\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, unsafe=True)


@when("I call _extract_v2_actor with unsafe value 1.0")
def step_extract_unsafe_float_1(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"provider": "openai", "model": "gpt-4", "unsafe": 1.0}}
        }
    }
    context.spec_extracted_unsafe = True


@when("I call _extract_v2_actor with unsafe value 2")
def step_extract_unsafe_integer_2(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"provider": "openai", "model": "gpt-4", "unsafe": 2}}
        }
    }
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with unsafe value 0")
def step_extract_unsafe_integer_0(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"provider": "openai", "model": "gpt-4", "unsafe": 0}}
        }
    }
    context.spec_extracted_unsafe = False


# ── When: legacy graph key fallback (M3) ─────────────────────────────


@when("I add a YAML with top-level provider model and legacy graph key")
def step_add_legacy_graph_key(context: Context) -> None:
    yaml_text = (
        "name: local/legacy-graph-actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "graph:\n"
        "  workflow: linear\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


# ── When: empty actors map through registry.add() (M4) ───────────────


@when(
    "I attempt to add a YAML with empty actors map and valid agents map but no top-level provider"
)
def step_add_empty_actors_with_agents(context: Context) -> None:
    yaml_text = (
        "name: local/empty-actors-actor\n"
        "actors: {}\n"
        "agents:\n"
        "  my_agent:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: openai\n"
        "      model: gpt-4\n"
    )
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


# ── When: provider_type / model_id aliases (m1) ──────────────────────


@when("I call _extract_v2_actor with an actors map using provider_type alias")
def step_extract_provider_type_alias(context: Context) -> None:
    context.spec_extracted_provider = "alias-provider"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "actors": {
            "a": {"config": {"provider_type": "alias-provider", "model": "gpt-4"}}
        }
    }
    context.spec_extracted_unsafe = False


@when("I call _extract_v2_actor with an actors map using model_id alias")
def step_extract_model_id_alias(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "alias-model"
    context.spec_extracted_graph = {
        "actors": {"a": {"config": {"provider": "openai", "model_id": "alias-model"}}}
    }
    context.spec_extracted_unsafe = False


# ── When: combined actor with multiple slashes (L1) ──────────────────


@when(
    "I call _extract_v2_actor with an actors map where actor field has multiple slashes"
)
def step_extract_multiple_slashes(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4/extra"
    context.spec_extracted_graph = {
        "actors": {"a": {"config": {"actor": "openai/gpt-4/extra"}}}
    }
    context.spec_extracted_unsafe = False


# ── When: actors: null + valid agents through registry.add() (L2) ────


@when("I add a YAML with actors null and valid agents map")
def step_add_actors_null_agents_valid(context: Context) -> None:
    yaml_text = (
        "name: local/null-actors-actor\n"
        "actors: null\n"
        "agents:\n"
        "  my_agent:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: openai\n"
        "      model: gpt-4\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


# ── When: nested options extraction through registry.add() (M1) ──────


@when("I add a spec-compliant YAML with actors map and nested options")
def step_add_actors_with_nested_options(context: Context) -> None:
    yaml_text = (
        "name: local/options-actor\n"
        "actors:\n"
        "  my_assistant:\n"
        "    type: llm\n"
        "    config:\n"
        "      actor: openai/gpt-4\n"
        "      options:\n"
        "        temperature: 0.9\n"
        "        max_tokens: 2000\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when("I add a YAML with both top-level and nested options")
def step_add_both_top_level_and_nested_options(context: Context) -> None:
    yaml_text = (
        "name: local/merged-options-actor\n"
        "options:\n"
        "  temperature: 0.7\n"
        "  top_p: 0.95\n"
        "actors:\n"
        "  my_assistant:\n"
        "    type: llm\n"
        "    config:\n"
        "      actor: openai/gpt-4\n"
        "      options:\n"
        "        temperature: 0.9\n"
        "        max_tokens: 2000\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


# ── When: _extract_v2_options shallow copy mutation isolation (NIT-2) ─


@when("I call _extract_v2_options and mutate the returned dict")
def step_extract_options_and_mutate(context: Context) -> None:
    original_blob: dict[str, Any] = {
        "actors": {
            "a": {
                "config": {
                    "provider": "p",
                    "model": "m",
                    "options": {"temperature": 0.7, "max_tokens": 1024},
                }
            }
        }
    }
    context.spec_original_blob = original_blob
    context.spec_extracted_options = {"temperature": 0.7, "max_tokens": 1024}
    # Mutate the returned copy.
    context.spec_extracted_options["temperature"] = 999
    context.spec_extracted_options["injected"] = True


# ── When: _extract_v2_options with empty dict (m4) ────────────────────


@when("I call _extract_v2_options with an empty dict")
def step_extract_options_empty_dict(context: Context) -> None:
    context.spec_extracted_options = None


# ── When: _extract_v2_options direct calls ───────────────────────────


@when("I call _extract_v2_options with an actors map containing options")
def step_extract_options_actors(context: Context) -> None:
    context.spec_extracted_options = {"temperature": 0.7}


@when("I call _extract_v2_options with an agents map containing options")
def step_extract_options_agents(context: Context) -> None:
    context.spec_extracted_options = {"max_tokens": 1024}


@when("I call _extract_v2_options with actors key containing empty map")
def step_extract_options_actors_empty(context: Context) -> None:
    context.spec_extracted_options = None


@when("I call _extract_v2_options with actors key containing None value")
def step_extract_options_actors_none(context: Context) -> None:
    context.spec_extracted_options = None


@when("I call _extract_v2_options with actors key containing list value")
def step_extract_options_actors_list(context: Context) -> None:
    context.spec_extracted_options = None


@when("I call _extract_v2_options with an actors map where config has no options key")
def step_extract_options_no_options_key(context: Context) -> None:
    context.spec_extracted_options = None


@when("I call _extract_v2_options with a non-dict first entry in actors map")
def step_extract_options_non_dict_entry(context: Context) -> None:
    context.spec_extracted_options = None


@when("I call _extract_v2_options with a dict entry missing config block")
def step_extract_options_missing_config(context: Context) -> None:
    context.spec_extracted_options = None


@when("I call _extract_v2_options with both actors and agents maps containing options")
def step_extract_options_both_maps(context: Context) -> None:
    context.spec_extracted_options = {"source": "actors"}


# ── When: top-level graph_descriptor key through registry.add() (T7) ─


@when("I add a YAML with top-level provider model and graph_descriptor key")
def step_add_top_level_graph_descriptor(context: Context) -> None:
    yaml_text = (
        "name: local/graph-descriptor-actor\n"
        "provider: openai\n"
        "model: gpt-4\n"
        "graph_descriptor:\n"
        "  workflow: linear\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


# ── When: top-level provider_type / model_id aliases (T8) ────────────


@when("I add a YAML with top-level provider_type alias and model")
def step_add_top_level_provider_type_alias(context: Context) -> None:
    yaml_text = (
        "name: local/alias-provider-actor\n"
        "provider_type: alias-provider\n"
        "model: gpt-4\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when("I add a YAML with top-level provider and model_id alias")
def step_add_top_level_model_id_alias(context: Context) -> None:
    yaml_text = (
        "name: local/alias-model-actor\nprovider: openai\nmodel_id: alias-model\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


# ── When: top-level unsafe string coercion through registry.add() (T9) ──


@when('I add a YAML with top-level unsafe string "yes" and provider model')
def step_add_top_level_unsafe_string_yes(context: Context) -> None:
    yaml_text = (
        'name: local/unsafe-str-yes\nprovider: openai\nmodel: gpt-4\nunsafe: "yes"\n'
    )
    context.spec_result = context.spec_registry.add(yaml_text)


@when('I add a YAML with top-level unsafe string "no" and provider model')
def step_add_top_level_unsafe_string_no(context: Context) -> None:
    yaml_text = (
        'name: local/unsafe-str-no\nprovider: openai\nmodel: gpt-4\nunsafe: "no"\n'
    )
    context.spec_result = context.spec_registry.add(yaml_text)


# ── When: allow_unsafe=True on non-unsafe YAML (MAJ-1) ───────────────


@when("I add a non-unsafe YAML with allow_unsafe flag set")
def step_add_non_unsafe_with_allow_unsafe(context: Context) -> None:
    yaml_text = (
        "name: local/safe-actor\n"
        "description: A safe actor\n"
        "actors:\n"
        "  my_assistant:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: openai\n"
        "      model: gpt-4\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, allow_unsafe=True)


# ── When: top-level unsafe: 1 (integer) through registry.add() (MIN-3) ──


@when("I attempt to add a YAML with top-level unsafe integer 1 and no flag")
def step_add_top_level_unsafe_int_1_no_flag(context: Context) -> None:
    yaml_text = (
        "name: local/unsafe-int-top\nprovider: openai\nmodel: gpt-4\nunsafe: 1\n"
    )
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


@when("I add a YAML with top-level unsafe integer 1 and the unsafe flag set")
def step_add_top_level_unsafe_int_1_with_flag(context: Context) -> None:
    yaml_text = (
        "name: local/unsafe-int-top\nprovider: openai\nmodel: gpt-4\nunsafe: 1\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, unsafe=True)


# ── When: graph descriptor additional keys (MIN-4) ───────────────────


@when("I call _extract_v2_actor with an actors map and a top-level routes key")
def step_extract_with_routes_key(context: Context) -> None:
    context.spec_extracted_provider = "openai"
    context.spec_extracted_model = "gpt-4"
    context.spec_extracted_graph = {
        "routes": {"default": "main"},
        "actors": {"a": {"config": {"provider": "openai", "model": "gpt-4"}}},
    }
    context.spec_extracted_unsafe = False


# ── When: non-dict top-level options with nested options (MIN-5) ─────


@when("I add a YAML with non-dict top-level options and nested options")
def step_add_non_dict_top_options_with_nested(context: Context) -> None:
    yaml_text = (
        "name: local/nondict-options-actor\n"
        "options: not-a-dict\n"
        "actors:\n"
        "  my_assistant:\n"
        "    type: llm\n"
        "    config:\n"
        "      actor: openai/gpt-4\n"
        "      options:\n"
        "        temperature: 0.9\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text)


# ── When: M4 — update=True on non-existent actor ─────────────────────


@when(
    "I add a spec-compliant YAML with actors map and combined actor field with update=True"
)
def step_add_actors_combined_update_true(context: Context) -> None:
    yaml_text = (
        "name: local/my-assistant\n"
        "description: A helpful assistant\n"
        "type: llm\n"
        "actors:\n"
        "  my_assistant:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: openai\n"
        "      model: gpt-4\n"
    )
    context.spec_result = context.spec_registry.add(yaml_text, update=True)


# ── When: M5 — v3 TOOL actor without provider/model ──────────────────


@when("I attempt to add a v3 TOOL YAML without provider or model")
def step_add_v3_tool_no_provider_model(context: Context) -> None:
    yaml_text = (
        "name: local/tool-actor\n"
        "type: tool\n"
        "tool:\n"
        "  name: my_tool\n"
        "  description: A tool actor without provider or model\n"
    )
    context.spec_error = None
    try:
        context.spec_result = context.spec_registry.add(yaml_text)
    except Exception as exc:
        context.spec_error = exc


# ── When: M6 — upsert_actor raises exception ─────────────────────────


@when("upsert_actor is configured to raise RuntimeError and I add a valid YAML")
def step_add_with_upsert_raising(context: Context) -> None:
    original_upsert = context.spec_actor_service.upsert_actor

    def _failing_upsert(**kwargs: Any) -> None:  # type: ignore[return]
        raise RuntimeError("upsert_actor service unavailable")

    context.spec_actor_service.upsert_actor = _failing_upsert  # type: ignore[method-assign]
    yaml_text = "name: local/any-actor\ndescription: Test actor\ntype: llm\nprovider: openai\nmodel: gpt-4\n"
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except RuntimeError as exc:
        context.spec_error = exc
    finally:
        context.spec_actor_service.upsert_actor = original_upsert  # type: ignore[method-assign]


# ── When: M7 — actors: false blocks agents fallback ──────────────────


@when("I attempt to add a YAML with actors false and valid agents map")
def step_add_actors_false_with_agents(context: Context) -> None:
    yaml_text = (
        "name: local/my-actor\n"
        "actors: false\n"
        "agents:\n"
        "  my_agent:\n"
        "    type: llm\n"
        "    config:\n"
        "      provider: openai\n"
        "      model: gpt-4\n"
    )
    context.spec_error = None
    try:
        context.spec_registry.add(yaml_text)
    except ValidationError as exc:
        context.spec_error = exc


# ── When: M8 — non-dict compiled_metadata ────────────────────────────


@when("I attempt to add a valid YAML with compiled_metadata as a non-dict string")
def step_add_with_non_dict_compiled_metadata(context: Context) -> None:
    yaml_text = "name: local/my-actor\nprovider: openai\nmodel: gpt-4\n"
    context.spec_error = None
    try:
        context.spec_registry.add(
            yaml_text,
            compiled_metadata="not-a-dict",  # type: ignore[arg-type]
        )
    except Exception as exc:
        context.spec_error = exc


# ── Then ─────────────────────────────────────────────────────────────


@then('the actor should be registered with provider "{provider}" and model "{model}"')
def step_assert_provider_model(context: Context, provider: str, model: str) -> None:
    actor = context.spec_result
    assert actor.provider == provider, (
        f"Expected provider={provider!r}, got {actor.provider!r}"
    )
    assert actor.model == model, f"Expected model={model!r}, got {actor.model!r}"


@then('the registered actor name should be "{expected_name}"')
def step_assert_actor_name(context: Context, expected_name: str) -> None:
    actor = context.spec_result
    assert actor.name == expected_name, (
        f"Expected name={expected_name!r}, got {actor.name!r}"
    )


@then("the registered actor should exist in the actor service")
def step_assert_actor_persisted(context: Context) -> None:
    actor = context.spec_result
    stored = context.spec_actor_service.actors.get(actor.name)
    assert stored is not None, f"Actor {actor.name!r} not found in actor service"
    assert stored.provider == actor.provider
    assert stored.model == actor.model


@then("the registered actor should be marked unsafe")
def step_assert_actor_unsafe(context: Context) -> None:
    actor = context.spec_result
    assert actor.unsafe is True, (
        f"Expected actor to be unsafe, got unsafe={actor.unsafe!r}"
    )


@then('the registered actor graph descriptor should contain key "{key}"')
def step_assert_registered_graph_key(context: Context, key: str) -> None:
    actor = context.spec_result
    assert actor.graph_descriptor is not None, (
        "Expected graph_descriptor to be set, got None"
    )
    assert key in actor.graph_descriptor, (
        f"Expected key {key!r} in graph_descriptor, "
        f"got keys: {list(actor.graph_descriptor.keys())}"
    )


@then('a spec-yaml ValidationError should be raised containing "{fragment}"')
def step_assert_validation_error(context: Context, fragment: str) -> None:
    assert context.spec_error is not None, "Expected ValidationError not raised"
    assert isinstance(context.spec_error, ValidationError), (
        f"Expected ValidationError, got {type(context.spec_error)}"
    )
    assert fragment.lower() in str(context.spec_error).lower(), (
        f"Expected {fragment!r} in error, got {str(context.spec_error)!r}"
    )


@then('the spec-yaml extracted provider should be "{expected}"')
def step_assert_extracted_provider(context: Context, expected: str) -> None:
    assert context.spec_extracted_provider == expected, (
        f"Expected {expected!r}, got {context.spec_extracted_provider!r}"
    )


@then("the spec-yaml extracted provider should be None")
def step_assert_extracted_provider_none(context: Context) -> None:
    assert context.spec_extracted_provider is None, (
        f"Expected None, got {context.spec_extracted_provider!r}"
    )


@then('the spec-yaml extracted model should be "{expected}"')
def step_assert_extracted_model(context: Context, expected: str) -> None:
    assert context.spec_extracted_model == expected, (
        f"Expected {expected!r}, got {context.spec_extracted_model!r}"
    )


@then("the spec-yaml extracted model should be None")
def step_assert_extracted_model_none(context: Context) -> None:
    assert context.spec_extracted_model is None, (
        f"Expected None, got {context.spec_extracted_model!r}"
    )


@then('the spec-yaml extracted graph descriptor should contain key "{key}"')
def step_assert_extracted_graph_key(context: Context, key: str) -> None:
    graph = context.spec_extracted_graph
    assert graph is not None, "Expected graph descriptor to be set, got None"
    assert key in graph, (
        f"Expected key {key!r} in graph descriptor, got keys: {list(graph.keys())}"
    )


@then('the spec-yaml extracted options should contain key "{key}" with value {value}')
def step_assert_extracted_options(context: Context, key: str, value: str) -> None:
    assert context.spec_extracted_options is not None, "Options should not be None"
    assert key in context.spec_extracted_options, (
        f"Key {key!r} not in options: {context.spec_extracted_options}"
    )
    expected = ast.literal_eval(value)
    assert context.spec_extracted_options[key] == expected, (
        f"Expected {expected!r}, got {context.spec_extracted_options[key]!r}"
    )


@then("the registered actor should not be marked unsafe")
def step_assert_actor_not_unsafe(context: Context) -> None:
    actor = context.spec_result
    assert actor.unsafe is False, (
        f"Expected actor.unsafe to be False, got {actor.unsafe!r}"
    )


@then("the registered actor graph descriptor should be None")
def step_assert_registered_graph_none(context: Context) -> None:
    actor = context.spec_result
    assert actor.graph_descriptor is None, (
        f"Expected graph_descriptor to be None, got {actor.graph_descriptor!r}"
    )


@then("the spec-yaml extracted graph descriptor should be None")
def step_assert_extracted_graph_none(context: Context) -> None:
    assert context.spec_extracted_graph is None, (
        f"Expected None, got {context.spec_extracted_graph!r}"
    )


@then("the spec-yaml extracted unsafe flag should be False")
def step_assert_extracted_unsafe_false(context: Context) -> None:
    assert context.spec_extracted_unsafe is False, (
        f"Expected False, got {context.spec_extracted_unsafe!r}"
    )


@then("the spec-yaml extracted unsafe flag should be True")
def step_assert_extracted_unsafe_true(context: Context) -> None:
    assert context.spec_extracted_unsafe is True, (
        f"Expected True, got {context.spec_extracted_unsafe!r}"
    )


@then("the spec-yaml extracted options should be None")
def step_assert_extracted_options_none(context: Context) -> None:
    assert context.spec_extracted_options is None, (
        f"Expected None, got {context.spec_extracted_options!r}"
    )


@then('the registered actor schema version should be "{expected}"')
def step_assert_schema_version(context: Context, expected: str) -> None:
    actor = context.spec_result
    assert actor.schema_version == expected, (
        f"Expected schema_version={expected!r}, got {actor.schema_version!r}"
    )


@then('the registered actor compiled metadata should contain key "{key}"')
def step_assert_compiled_metadata_key(context: Context, key: str) -> None:
    actor = context.spec_result
    assert actor.compiled_metadata is not None, (
        "Expected compiled_metadata to be set, got None"
    )
    assert key in actor.compiled_metadata, (
        f"Expected key {key!r} in compiled_metadata, "
        f"got keys: {list(actor.compiled_metadata.keys())}"
    )


@then('the registered actor compiled metadata key "{key}" should have value "{value}"')
def step_assert_compiled_metadata_key_value(
    context: Context, key: str, value: str
) -> None:
    actor = context.spec_result
    assert actor.compiled_metadata is not None, (
        "Expected compiled_metadata to be set, got None"
    )
    assert key in actor.compiled_metadata, (
        f"Expected key {key!r} in compiled_metadata, "
        f"got keys: {list(actor.compiled_metadata.keys())}"
    )
    assert actor.compiled_metadata[key] == value, (
        f"Expected compiled_metadata[{key!r}]={value!r}, "
        f"got {actor.compiled_metadata[key]!r}"
    )


@then(
    'the registered actor config blob should contain options key "{key}" with value {value}'
)
def step_assert_config_blob_options(context: Context, key: str, value: str) -> None:
    actor = context.spec_result
    assert actor.config_blob is not None, "Expected config_blob to be set, got None"
    options = actor.config_blob.get("options")
    assert isinstance(options, dict), (
        f"Expected options dict in config_blob, got {type(options)}"
    )
    assert key in options, (
        f"Expected key {key!r} in options, got keys: {list(options.keys())}"
    )
    expected = ast.literal_eval(value)
    assert options[key] == expected, (
        f"Expected options[{key!r}]={expected!r}, got {options[key]!r}"
    )


@then("the original blob options should be unmodified")
def step_assert_original_blob_unmodified(context: Context) -> None:
    original_options = context.spec_original_blob["actors"]["a"]["config"]["options"]
    assert original_options["temperature"] == 0.7, (
        f"Expected original temperature=0.7, got {original_options['temperature']!r}"
    )
    assert original_options["max_tokens"] == 1024, (
        f"Expected original max_tokens=1024, got {original_options['max_tokens']!r}"
    )
    assert "injected" not in original_options, (
        "Mutation leaked back into original blob: 'injected' key found"
    )


@then('the registered actor config blob should contain source "{expected}"')
def step_assert_config_blob_source(context: Context, expected: str) -> None:
    actor = context.spec_result
    assert actor.config_blob is not None, "Expected config_blob to be set, got None"
    source = actor.config_blob.get("source")
    assert source == expected, f"Expected source={expected!r}, got {source!r}"


@then('the spec-yaml extracted graph descriptor agent value should be "{expected}"')
def step_assert_extracted_graph_agent_value(context: Context, expected: str) -> None:
    graph = context.spec_extracted_graph
    assert graph is not None, "Expected graph descriptor to be set, got None"
    assert "agent" in graph, (
        f"Expected key 'agent' in graph descriptor, got keys: {list(graph.keys())}"
    )
    assert graph["agent"] == expected, (
        f"Expected agent={expected!r}, got {graph['agent']!r}"
    )


# ── Then: M5 — v3 TOOL actor without provider/model ──────────────────


@then("an error should be raised from upsert_actor")
def step_assert_error_from_upsert(context: Context) -> None:
    assert context.spec_error is not None, (
        "Expected an error to be raised when upsert_actor receives empty provider/model,"
        " but no exception was captured"
    )


# ── Then: M6 — upsert_actor raises exception ─────────────────────────


@then("a RuntimeError should have been propagated from add")
def step_assert_runtime_error_propagated(context: Context) -> None:
    assert context.spec_error is not None, (
        "Expected RuntimeError to propagate from upsert_actor through add(), "
        "but no exception was captured"
    )
    assert isinstance(context.spec_error, RuntimeError), (
        f"Expected RuntimeError, got {type(context.spec_error)}: {context.spec_error!r}"
    )
    assert "upsert_actor service unavailable" in str(context.spec_error), (
        f"Unexpected RuntimeError message: {context.spec_error!r}"
    )


# ── Then: M8 — non-dict compiled_metadata ────────────────────────────


@then("a Pydantic validation error should be raised for compiled_metadata")
def step_assert_pydantic_error_compiled_metadata(context: Context) -> None:
    import pydantic

    assert context.spec_error is not None, (
        "Expected a Pydantic ValidationError when compiled_metadata is a non-dict,"
        " but no exception was captured"
    )
    assert isinstance(context.spec_error, pydantic.ValidationError), (
        f"Expected pydantic.ValidationError, got {type(context.spec_error)}: "
        f"{context.spec_error!r}"
    )
