# pyright: reportRedeclaration=false
"""Step definitions for the Actor CLI feature."""

from __future__ import annotations

import json
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when
from typer.testing import CliRunner

from cleveragents.cli.commands.actor import app as actor_app
from cleveragents.core.exceptions import (
    BusinessRuleViolation,
    NotFoundError,
    ValidationError,
)
from cleveragents.domain.models.core.actor import Actor


def _make_actor(
    *,
    name: str = "local/test-actor",
    provider: str = "default-provider",
    model: str = "default-model",
    config: dict[str, Any] | None = None,
    graph_descriptor: dict[str, Any] | None = None,
    unsafe: bool = False,
    is_default: bool = False,
    is_built_in: bool = False,
) -> Actor:
    blob = config or {}
    return Actor(
        id=1,
        name=name,
        provider=provider,
        model=model,
        config_blob=blob,
        config_hash=Actor.compute_hash(blob),
        graph_descriptor=graph_descriptor,
        unsafe=unsafe,
        is_built_in=is_built_in,
        is_default=is_default,
    )


def _register_cleanup(context, path: Path) -> None:
    context._cleanup_handlers.append(lambda: path.unlink(missing_ok=True))


@given("an actor CLI runner")
def step_impl(context):
    context.runner = CliRunner()


@given("I have an actor JSON config file")
def step_impl(context):
    context.actor_config_data = {
        "name": "local/test-actor",
        "provider": "openai",
        "model": "gpt-4o-mini",
        "temperature": 0.5,
        "max_tokens": 256,
    }
    with tempfile.NamedTemporaryFile(
        delete=False, suffix=".json", mode="w", encoding="utf-8"
    ) as handle:
        json.dump(context.actor_config_data, handle)
        handle.flush()
    context.actor_config_path = Path(handle.name)
    _register_cleanup(context, context.actor_config_path)


@given("I have an actor JSON config file with options")
def step_impl(context):
    context.actor_config_data = {
        "name": "local/test-actor",
        "provider": "openai",
        "model": "gpt-4o",
        "options": {"temperature": 0.4, "enabled": True},
    }
    with tempfile.NamedTemporaryFile(
        delete=False, suffix=".json", mode="w", encoding="utf-8"
    ) as handle:
        json.dump(context.actor_config_data, handle)
        handle.flush()
    context.actor_config_path = Path(handle.name)
    _register_cleanup(context, context.actor_config_path)


@given("I have an actor list config file")
def step_impl(context):
    context.actor_config_data = [1, 2, 3]
    with tempfile.NamedTemporaryFile(
        delete=False, suffix=".json", mode="w", encoding="utf-8"
    ) as handle:
        json.dump(context.actor_config_data, handle)
        handle.flush()
    context.actor_config_path = Path(handle.name)
    _register_cleanup(context, context.actor_config_path)


@given("I have an actor YAML config file returning empty data")
def step_impl(context):
    context.actor_config_data = {
        "name": "local/existing-actor",
        "provider": "existing-provider",
        "model": "existing-model",
    }
    with tempfile.NamedTemporaryFile(delete=False, suffix=".yaml") as handle:
        handle.write(
            b"name: local/existing-actor\n"
            b"provider: existing-provider\nmodel: existing-model\n"
        )
        handle.flush()
    context.actor_config_path = Path(handle.name)
    _register_cleanup(context, context.actor_config_path)


@given("I have an actor YAML-only config file")
def step_impl(context):
    context.actor_config_data = {
        "name": "local/test-actor",
        "provider": "openai",
        "model": "gpt-4o-mini",
        "yaml_only": True,
    }
    with tempfile.NamedTemporaryFile(
        delete=False, suffix=".yaml", mode="w", encoding="utf-8"
    ) as handle:
        handle.write(
            "name: local/test-actor\nprovider: openai\n"
            "model: gpt-4o-mini\nyaml_only: true\n"
        )
        handle.flush()
    context.actor_config_path = Path(handle.name)
    _register_cleanup(context, context.actor_config_path)


@given("I have an actor graph config file")
def step_impl(context):
    context.actor_config_data = {
        "name": "service/graph-actor",
        "provider": "graph-provider",
        "model": "graph-model",
        "graph": {"nodes": ["start"], "edges": []},
    }
    with tempfile.NamedTemporaryFile(
        delete=False, suffix=".json", mode="w", encoding="utf-8"
    ) as handle:
        json.dump(context.actor_config_data, handle)
        handle.flush()
    context.actor_config_path = Path(handle.name)
    _register_cleanup(context, context.actor_config_path)


@given("I have an empty actor YAML config file")
def step_impl(context):
    context.actor_config_data = {}
    context.expected_error = "Config file must contain a 'name' field"
    with tempfile.NamedTemporaryFile(
        delete=False, suffix=".yaml", mode="w", encoding="utf-8"
    ) as handle:
        handle.write("")
        handle.flush()
    context.actor_config_path = Path(handle.name)
    _register_cleanup(context, context.actor_config_path)


@given("I have an unsafe actor JSON config file")
def step_impl(context):
    context.actor_config_data = {
        "name": "local/test-actor",
        "provider": "openai",
        "model": "gpt-4o-mini",
        "unsafe": True,
    }
    with tempfile.NamedTemporaryFile(
        delete=False, suffix=".json", mode="w", encoding="utf-8"
    ) as handle:
        json.dump(context.actor_config_data, handle)
        handle.flush()
    context.actor_config_path = Path(handle.name)
    _register_cleanup(context, context.actor_config_path)


@when("I run actor add without config")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
        context.result = context.runner.invoke(actor_app, ["add", "local/test-actor"])
        context.mock_actor_registry = mock_actor_registry
    context.expected_error = "Config file is required"


@when("I run actor add with that config")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_registry = MagicMock()
        mock_actor_service = MagicMock()

        if isinstance(context.actor_config_data, dict):
            mock_actor = _make_actor(config=context.actor_config_data)
            # registry.upsert_actor() is the YAML-first path used by the add command
            mock_actor_registry.upsert_actor.return_value = mock_actor
        else:
            mock_actor_registry.upsert_actor.side_effect = ValidationError(
                "Config must be a JSON/YAML object"
            )

        # Return the mocked services
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        actor_name = (
            context.actor_config_data.get("name", "local/test-actor")
            if isinstance(context.actor_config_data, dict)
            else "local/test-actor"
        )
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
            ],
        )
        context.mock_actor_registry = mock_actor_registry
        if not isinstance(context.actor_config_data, dict):
            context.expected_error = "Config must be a JSON/YAML object"
        elif isinstance(
            context.actor_config_data, dict
        ) and context.actor_config_data.get("unsafe"):
            context.expected_error = "Actor config is marked unsafe"
        else:
            context.expected_error = None


@when("I run actor add with that config and options overrides")
def step_impl(context):
    overrides = {"temperature": 0.9, "max_tokens": 128, "mode": "fast"}
    option_args: list[str] = []
    for key, value in overrides.items():
        formatted = json.dumps(value) if not isinstance(value, str) else value
        option_args.extend(["--option", f"{key}={formatted}"])

    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_registry = MagicMock()
        mock_actor_service = MagicMock()

        mock_actor = _make_actor(config=context.actor_config_data)
        # registry.upsert_actor() is the YAML-first path used by the add command
        mock_actor_registry.upsert_actor.return_value = mock_actor

        # Return the mocked services
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        actor_name = context.actor_config_data.get("name", "local/test-actor")
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
                *option_args,
            ],
        )
        context.mock_actor_registry = mock_actor_registry
        context.expected_error = None


@when("I run actor add with boolean option override")
def step_impl(context):
    option_args = ["--option", "enabled=false"]
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_registry = MagicMock()
        mock_actor_service = MagicMock()

        mock_actor = _make_actor(config=context.actor_config_data)
        # registry.upsert_actor() is the YAML-first path used by the add command
        mock_actor_registry.upsert_actor.return_value = mock_actor

        # Return the mocked services
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        actor_name = context.actor_config_data.get("name", "local/test-actor")
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
                *option_args,
            ],
        )
        context.mock_actor_registry = mock_actor_registry
        context.expected_error = None


@when("I run actor add with uppercase boolean option override")
def step_impl(context):
    option_args = ["--option", "enabled=FALSE"]
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_registry = MagicMock()
        mock_actor_service = MagicMock()

        mock_actor = _make_actor(config=context.actor_config_data)
        # registry.upsert_actor() is the YAML-first path used by the add command
        mock_actor_registry.upsert_actor.return_value = mock_actor

        # Return the mocked services
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        actor_name = context.actor_config_data.get("name", "local/test-actor")
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
                *option_args,
            ],
        )
        context.mock_actor_registry = mock_actor_registry
        context.expected_error = None


@when("I run actor add with malformed option")
def step_impl(context):
    actor_name = (
        context.actor_config_data.get("name", "local/test-actor")
        if isinstance(context.actor_config_data, dict)
        else "local/test-actor"
    )
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_get_services.return_value = (MagicMock(), MagicMock())
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
                "--option",
                "invalid",
            ],
        )
    context.expected_error = "Options must be provided as key=value pairs"


@when("I run actor add with empty option key")
def step_impl(context):
    actor_name = (
        context.actor_config_data.get("name", "local/test-actor")
        if isinstance(context.actor_config_data, dict)
        else "local/test-actor"
    )
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_get_services.return_value = (MagicMock(), MagicMock())
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
                "--option",
                "=true",
            ],
        )
    context.expected_error = "Option keys cannot be empty"


@when("I run actor add with that config and unsafe flag")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_registry = MagicMock()
        mock_actor_service = MagicMock()

        if isinstance(context.actor_config_data, dict):
            mock_actor = _make_actor(config=context.actor_config_data, unsafe=True)
            # registry.upsert_actor() is the YAML-first path used by the add command
            mock_actor_registry.upsert_actor.return_value = mock_actor
        else:
            mock_actor_registry.upsert_actor.side_effect = ValidationError(
                "Config must be a JSON/YAML object"
            )

        # Return the mocked services
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        actor_name = (
            context.actor_config_data.get("name", "local/test-actor")
            if isinstance(context.actor_config_data, dict)
            else "local/test-actor"
        )
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
                "--unsafe",
            ],
        )
        context.mock_actor_registry = mock_actor_registry
        if not isinstance(context.actor_config_data, dict):
            context.expected_error = "Config must be a JSON/YAML object"
        elif context.actor_config_data == {}:
            context.expected_error = "provider is required"
        else:
            context.expected_error = None


@when("I run actor add via service with that config")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        actor_service = MagicMock()
        graph_descriptor = context.actor_config_data.get("graph")
        actor_service.upsert_actor.return_value = _make_actor(
            name="service/graph-actor",
            provider=context.actor_config_data.get("provider", "openai"),
            model=context.actor_config_data.get("model", "gpt-4o"),
            config=context.actor_config_data,
            graph_descriptor=graph_descriptor,
        )
        mock_get_services.return_value = (actor_service, None)

        actor_name = context.actor_config_data.get("name", "service/graph-actor")
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
            ],
        )

    context.actor_service = actor_service
    context.graph_descriptor = graph_descriptor


@when("I run actor add with missing config path")
def step_impl(context):
    missing_path = Path(tempfile.gettempdir()) / "missing-actor-config.json"
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_get_services.return_value = (MagicMock(), MagicMock())
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                "local/test-actor",
                "--config",
                str(missing_path),
            ],
        )
    context.expected_error = "Config file not found"


@when("I run actor add with business rule violation")
def step_impl(context):
    config_data = {
        "name": "local/test-actor",
        "provider": "openai",
        "model": "gpt-4o",
    }
    with tempfile.NamedTemporaryFile(
        delete=False, suffix=".json", mode="w", encoding="utf-8"
    ) as handle:
        json.dump(config_data, handle)
        handle.flush()
    config_path = Path(handle.name)
    _register_cleanup(context, config_path)

    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        # The add command now uses registry.upsert_actor() (YAML-first path)
        mock_actor_registry.upsert_actor.side_effect = BusinessRuleViolation("invalid")
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                "local/test-actor",
                "--config",
                str(config_path),
            ],
        )
    context.expected_error = "Error:"


@when("I run actor add via service without unsafe flag")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        actor_service = MagicMock()
        mock_get_services.return_value = (actor_service, None)

        actor_name = (
            context.actor_config_data.get("name", "local/test-actor")
            if isinstance(context.actor_config_data, dict)
            else "local/test-actor"
        )
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
            ],
        )
    context.actor_service = actor_service
    context.expected_error = "Actor config is marked unsafe"


@when("I run actor add via service with unsafe canonical blob")
def step_impl(context):
    with (
        patch(
            "cleveragents.cli.commands.actor._canonicalize_actor_config",
            return_value=(
                MagicMock(
                    provider="openai",
                    model="gpt-4o",
                    graph_descriptor=None,
                    unsafe=True,
                    options=None,
                ),
                {
                    "provider": "openai",
                    "model": "gpt-4o",
                    "unsafe": True,
                },
                False,
            ),
        ),
        patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
    ):
        actor_service = MagicMock()
        mock_get_services.return_value = (actor_service, None)

        actor_name = (
            context.actor_config_data.get("name", "local/test-actor")
            if isinstance(context.actor_config_data, dict)
            else "local/test-actor"
        )
        context.result = context.runner.invoke(
            actor_app,
            [
                "add",
                actor_name,
                "--config",
                str(context.actor_config_path),
            ],
        )

    context.actor_service = actor_service
    context.expected_error = "Actor config is marked unsafe"


@when("I run actor update with safe flag and yaml config")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_registry = MagicMock()
        mock_actor_service = MagicMock()
        current_actor = _make_actor(
            name="local/existing-actor",
            provider="existing-provider",
            model="existing-model",
            config={"existing": True},
            unsafe=True,
        )
        mock_actor_registry.get_actor.return_value = current_actor
        updated_actor = _make_actor(
            name=current_actor.name,
            provider=current_actor.provider,
            model=current_actor.model,
            config=context.actor_config_data,
            unsafe=False,
        )
        mock_actor_registry.upsert_actor.return_value = updated_actor
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(
            actor_app,
            [
                "update",
                current_actor.name,
                "--config",
                str(context.actor_config_path),
                "--safe",
            ],
        )
        context.mock_actor_registry = mock_actor_registry
        context.current_actor = current_actor


@when("I run actor update with unsafe flag")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_registry = MagicMock()
        mock_actor_service = MagicMock()
        current_actor = _make_actor(
            name="local/unsafe-actor",
            provider="unsafe-provider",
            model="unsafe-model",
            config={"existing": True},
            unsafe=False,
        )
        mock_actor_registry.get_actor.return_value = current_actor
        updated_actor = _make_actor(
            name=current_actor.name,
            provider=current_actor.provider,
            model=current_actor.model,
            config=current_actor.config_blob,
            unsafe=True,
        )
        mock_actor_registry.upsert_actor.return_value = updated_actor
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(
            actor_app,
            [
                "update",
                current_actor.name,
                "--unsafe",
            ],
        )
        context.mock_actor_registry = mock_actor_registry
        context.current_actor = current_actor


@when("I run actor update with validation error")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        current_actor = _make_actor(name="local/invalid-update")
        mock_actor_registry.get_actor.return_value = current_actor
        mock_actor_registry.upsert_actor.side_effect = ValidationError("bad update")
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(
            actor_app,
            [
                "update",
                current_actor.name,
            ],
        )
        context.mock_actor_registry = mock_actor_registry
    context.expected_error = "Error:"


@when("I run actor update for missing actor")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_actor_registry.get_actor.side_effect = NotFoundError(
            resource_type="actor", resource_id="local/missing"
        )
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(
            actor_app,
            [
                "update",
                "missing-actor",
            ],
        )
        context.mock_actor_registry = mock_actor_registry
    context.expected_error = "Actor not found"


@when("I run actor update with conflicting flags")
def step_impl(context):
    context.result = context.runner.invoke(
        actor_app,
        [
            "update",
            "local/conflict",
            "--unsafe",
            "--safe",
        ],
    )
    context.expected_error = "Choose only one of --unsafe or --safe"


@when("I run actor update requiring unsafe confirmation")
def step_impl(context):
    with (
        patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
        patch(
            "cleveragents.cli.commands.actor._canonicalize_actor_config"
        ) as mock_canonicalize,
    ):
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        current_actor = _make_actor(name="local/needs-confirm")
        mock_actor_registry.get_actor.return_value = current_actor
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        mock_resolved = MagicMock(unsafe=True)
        mock_canonicalize.return_value = (
            mock_resolved,
            {"provider": current_actor.provider, "model": current_actor.model},
            True,
        )

        context.result = context.runner.invoke(
            actor_app, ["update", current_actor.name]
        )
    context.expected_error = "Actor config is marked unsafe"


@when("I run actor update via service without unsafe flag")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        actor_service = MagicMock()
        current_actor = _make_actor(
            name="local/service-update",
            provider="service-provider",
            model="service-model",
            config={"provider": "service-provider", "model": "service-model"},
        )
        actor_service.get_actor.return_value = current_actor
        mock_get_services.return_value = (actor_service, None)

        context.result = context.runner.invoke(
            actor_app,
            [
                "update",
                current_actor.name,
                "--config",
                str(context.actor_config_path),
            ],
        )

    context.actor_service = actor_service
    context.expected_error = "Actor config is marked unsafe"


@when("I run actor update with option overrides")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        current_actor = _make_actor(
            name="local/option-update",
            provider="registry-provider",
            model="registry-model",
            config={
                "provider": "registry-provider",
                "model": "registry-model",
                "options": {"keep": "existing"},
            },
        )
        mock_actor_registry.get_actor.return_value = current_actor
        updated_actor = _make_actor(
            name=current_actor.name,
            provider=current_actor.provider,
            model=current_actor.model,
            config=current_actor.config_blob,
        )
        mock_actor_registry.upsert_actor.return_value = updated_actor
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(
            actor_app,
            [
                "update",
                current_actor.name,
                "--option",
                "retries=2",
                "--option",
                "enabled=false",
            ],
        )

    context.current_actor = current_actor
    context.mock_actor_registry = mock_actor_registry
    context.expected_option_overrides = {"retries": 2, "enabled": False}


@when("I run actor update with uppercase boolean option override")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        current_actor = _make_actor(
            name="local/option-upper",
            provider="registry-provider",
            model="registry-model",
            config={
                "provider": "registry-provider",
                "model": "registry-model",
                "options": {"keep": "existing"},
            },
        )
        mock_actor_registry.get_actor.return_value = current_actor
        updated_actor = _make_actor(
            name=current_actor.name,
            provider=current_actor.provider,
            model=current_actor.model,
            config=current_actor.config_blob,
        )
        mock_actor_registry.upsert_actor.return_value = updated_actor
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(
            actor_app,
            [
                "update",
                current_actor.name,
                "--option",
                "enabled=TRUE",
            ],
        )

    context.current_actor = current_actor
    context.mock_actor_registry = mock_actor_registry
    context.expected_option_overrides = {"enabled": True}


@when("I run actor update via service with unsafe canonical blob")
def step_impl(context):
    with (
        patch(
            "cleveragents.cli.commands.actor._canonicalize_actor_config",
            return_value=(
                MagicMock(
                    provider="service-provider",
                    model="service-model",
                    graph_descriptor=None,
                    unsafe=True,
                    options=None,
                ),
                {
                    "provider": "service-provider",
                    "model": "service-model",
                    "unsafe": True,
                },
                False,
            ),
        ),
        patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
    ):
        actor_service = MagicMock()
        current_actor = _make_actor(
            name="local/service-canonical",
            provider="service-provider",
            model="service-model",
            config={"provider": "service-provider", "model": "service-model"},
        )
        actor_service.get_actor.return_value = current_actor
        mock_get_services.return_value = (actor_service, None)

        context.result = context.runner.invoke(
            actor_app,
            [
                "update",
                current_actor.name,
                "--config",
                str(context.actor_config_path),
            ],
        )

    context.actor_service = actor_service
    context.expected_error = "Actor config is marked unsafe"


@when("I run actor update via service with safe config")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        actor_service = MagicMock()
        current_actor = _make_actor(
            name="local/service-safe",
            provider="service-provider",
            model="service-model",
            config={"provider": "service-provider", "model": "service-model"},
        )
        actor_service.get_actor.return_value = current_actor
        updated_actor = _make_actor(
            name=current_actor.name,
            provider=context.actor_config_data.get("provider", "openai"),
            model=context.actor_config_data.get("model", "gpt-4o-mini"),
            config=context.actor_config_data,
            unsafe=False,
        )
        actor_service.upsert_actor.return_value = updated_actor
        mock_get_services.return_value = (actor_service, None)

        context.result = context.runner.invoke(
            actor_app,
            [
                "update",
                current_actor.name,
                "--config",
                str(context.actor_config_path),
            ],
        )

    context.actor_service = actor_service
    context.current_actor = current_actor


@when("I run actor remove successfully")
def step_impl(context):
    with (
        patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
        patch("cleveragents.cli.commands.actor._compute_actor_impact") as mock_impact,
    ):
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
        mock_impact.return_value = (0, 0, 0)

        context.result = context.runner.invoke(actor_app, ["remove", "local/removable"])
        context.mock_actor_registry = mock_actor_registry


@when("I run actor remove and it fails validation")
def step_impl(context):
    with (
        patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
        patch("cleveragents.cli.commands.actor._compute_actor_impact") as mock_impact,
    ):
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_actor_registry.remove_actor.side_effect = ValidationError("invalid")
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
        mock_impact.return_value = (0, 0, 0)

        context.result = context.runner.invoke(actor_app, ["remove", "local/removable"])
        context.mock_actor_registry = mock_actor_registry
    context.expected_error = "Error:"


@when("I run actor remove via service path")
def step_impl(context):
    with (
        patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
        patch("cleveragents.cli.commands.actor._compute_actor_impact") as mock_impact,
    ):
        actor_service = MagicMock()
        mock_get_services.return_value = (actor_service, None)
        mock_impact.return_value = (0, 0, 0)

        context.result = context.runner.invoke(actor_app, ["remove", "local/removable"])

    context.actor_service = actor_service


@when("I run actor list with no actors")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_actor_registry.list_actors.return_value = []
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(actor_app, ["list"])


@when("I run actor list with two actors")
def step_impl(context):
    actors = [
        _make_actor(name="local/first", provider="p1", model="m1"),
        _make_actor(name="local/second", provider="p2", model="m2", unsafe=True),
    ]
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_actor_registry.list_actors.return_value = actors
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(actor_app, ["list"])
    context.actors = actors


@when("I run actor show successfully")
def step_impl(context):
    actor = _make_actor(name="local/show", provider="provider", model="model")
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_actor_registry.get_actor.return_value = actor
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(actor_app, ["show", actor.name])
    context.actor = actor


@when("I run actor show with validation error")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_actor_registry.get_actor.side_effect = ValidationError("bad")
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(actor_app, ["show", "local/show"])
    context.expected_error = "Error:"


@when("I run set-default actor successfully")
def step_impl(context):
    actor = _make_actor(name="provider/model", provider="provider", model="model")
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_actor_registry.set_default_actor.return_value = actor
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(actor_app, ["set-default", actor.name])
    context.actor = actor


@when("I run set-default actor with violation")
def step_impl(context):
    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_actor_service = MagicMock()
        mock_actor_registry = MagicMock()
        mock_actor_registry.set_default_actor.side_effect = BusinessRuleViolation(
            "nope"
        )
        mock_get_services.return_value = (mock_actor_service, mock_actor_registry)

        context.result = context.runner.invoke(actor_app, ["set-default", "local/fail"])
    context.expected_error = "Error:"


@then("the actor add should succeed with default config")
def step_impl(context):
    assert context.result.exit_code == 0
    context.mock_actor_registry.upsert_actor.assert_called_once()


@then("the actor add should pass the loaded config")
def step_impl(context):
    assert context.result.exit_code == 0
    # The add command uses registry.upsert_actor() with yaml_text threaded through
    # to preserve the original YAML text, schema_version, and compiled_metadata.
    context.mock_actor_registry.upsert_actor.assert_called_once()
    call_kwargs = context.mock_actor_registry.upsert_actor.call_args.kwargs
    yaml_text_arg = call_kwargs.get("yaml_text", "")
    assert isinstance(yaml_text_arg, str) and yaml_text_arg.strip()


@then("the service actor add should include graph descriptor")
def step_impl(context):
    assert context.result.exit_code == 0
    context.actor_service.upsert_actor.assert_called_once()
    call_kwargs = context.actor_service.upsert_actor.call_args.kwargs
    assert call_kwargs.get("graph_descriptor") == context.graph_descriptor
    assert (
        call_kwargs.get("config_blob", {}).get("graph_descriptor")
        == context.graph_descriptor
    )


@then("the actor command should fail with bad parameter")
def step_impl(context):
    import re

    assert context.result.exit_code != 0
    expected = getattr(context, "expected_error", None)
    if expected:
        # Strip ANSI escape codes and normalize whitespace to handle
        # Rich formatting and text wrapping in narrow CI terminals
        raw = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", context.result.output)
        normalized = " ".join(raw.split())
        assert expected in normalized, f"Expected '{expected}' in output: {normalized}"


@then("the actor update should set safe and merge config")
def step_impl(context):
    assert context.result.exit_code == 0
    expected_config = dict(context.actor_config_data)
    expected_config.setdefault("unsafe", False)
    context.mock_actor_registry.upsert_actor.assert_called_once_with(
        name=context.current_actor.name,
        provider=None,
        model=None,
        config_blob=expected_config,
        graph_descriptor=context.current_actor.graph_descriptor,
        unsafe=False,
        set_default=False,
        is_built_in=context.current_actor.is_built_in,
        allow_unsafe=False,
    )


@then("the actor update should set unsafe flag")
def step_impl(context):
    assert context.result.exit_code == 0
    expected_config = dict(context.current_actor.config_blob)
    expected_config.setdefault("provider", context.current_actor.provider)
    expected_config.setdefault("model", context.current_actor.model)
    expected_config.setdefault("unsafe", True)
    context.mock_actor_registry.upsert_actor.assert_called_once_with(
        name=context.current_actor.name,
        provider=context.current_actor.provider,
        model=context.current_actor.model,
        config_blob=expected_config,
        graph_descriptor=context.current_actor.graph_descriptor,
        unsafe=True,
        set_default=False,
        is_built_in=context.current_actor.is_built_in,
        allow_unsafe=True,
    )


@then("the actor update should include option overrides")
def step_impl(context):
    assert context.result.exit_code == 0
    context.mock_actor_registry.upsert_actor.assert_called_once()
    call_kwargs = context.mock_actor_registry.upsert_actor.call_args.kwargs
    assert call_kwargs.get("option_overrides") == context.expected_option_overrides
    options_blob = call_kwargs.get("config_blob", {}).get("options", {})
    for key, value in context.expected_option_overrides.items():
        assert options_blob.get(key) == value


@then("the actor service update should succeed")
def step_impl(context):
    assert context.result.exit_code == 0
    context.actor_service.upsert_actor.assert_called_once()
    call_kwargs = context.actor_service.upsert_actor.call_args.kwargs
    assert call_kwargs.get("name") == context.current_actor.name
    assert call_kwargs.get("unsafe") is False
    assert call_kwargs.get("provider") == context.actor_config_data.get("provider")
    assert call_kwargs.get("model") == context.actor_config_data.get("model")
    config_blob = call_kwargs.get("config_blob", {})
    assert config_blob.get("provider") == context.actor_config_data.get("provider")
    assert config_blob.get("model") == context.actor_config_data.get("model")


@then("the actor command should abort for missing actor")
def step_impl(context):
    assert context.result.exit_code != 0
    assert context.expected_error in context.result.output


@then("the actor remove should succeed")
def step_impl(context):
    assert context.result.exit_code == 0
    context.mock_actor_registry.remove_actor.assert_called_once_with("local/removable")


@then("the actor service remove should be called")
def step_impl(context):
    assert context.result.exit_code == 0
    context.actor_service.remove_actor.assert_called_once_with("local/removable")


@then("the actor command should abort with error")
def step_impl(context):
    assert context.result.exit_code != 0
    expected = getattr(context, "expected_error", None)
    if expected:
        assert expected in context.result.output


@then("the actor service should not be called")
def step_impl(context):
    assert context.actor_service.upsert_actor.call_count == 0


@then("the actor list should report empty state")
def step_impl(context):
    assert context.result.exit_code == 0
    assert "No actors configured." in context.result.output


@then("the actor list should render rows")
def step_impl(context):
    assert context.result.exit_code == 0
    for actor in context.actors:
        assert actor.name in context.result.output


@then("the actor show should display details")
def step_impl(context):
    assert context.result.exit_code == 0
    assert context.actor.name in context.result.output


@then("the set default actor should display details")
def step_impl(context):
    assert context.result.exit_code == 0
    assert context.actor.name in context.result.output
