"""Step definitions for actor_list_empty.feature (bug #592).

Regression tests verifying that ``agents actor list`` handles empty
provider lists and multi-slash model names without validation errors.

Root cause (fixed)
~~~~~~~~~~~~~~~~~~
``ActorRegistry._actor_name()`` built names via
``f"{provider}/{model}"``.  For providers whose default model already
contains a ``/`` (e.g. OpenRouter's ``anthropic/claude-sonnet-4-20250514``),
this yielded names with 2+ slashes.  ``ActorService.upsert_actor()`` then
called ``_normalize_name()`` which raised ``ValidationError("Actor names
must include exactly one '/' separator")``.

The fix sanitises both provider and model names by replacing ``/`` with
``-`` and lowercasing the result.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

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

from cleveragents.actor.registry import ActorRegistry
from cleveragents.cli.commands.actor import app as actor_app
from cleveragents.domain.models.core.actor import Actor
from features.mocks.fake_provider import FakeProviderInfo, FakeProviderRegistry

runner = CliRunner()

# Patch targets
_PATCH_GET_SERVICES = "cleveragents.cli.commands.actor._get_services"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_real_registry(
    providers: list[FakeProviderInfo],
) -> tuple[MagicMock, Any]:
    """Build a real ``ActorRegistry`` with mocked service/settings.

    Actors created by ``upsert_actor`` are captured so that
    ``list_actors`` returns them — matching the pattern in
    ``features/mocks/fake_provider.make_registry()``.
    """
    provider_reg = FakeProviderRegistry(providers=providers)

    captured_actors: list[Actor] = []

    def _capturing_upsert(**kwargs: Any) -> Actor:
        actor = Actor(
            name=kwargs.get("name", "mock/actor"),
            provider=kwargs.get("provider", "mock"),
            model=kwargs.get("model", "actor"),
            config_blob={},
            config_hash=Actor.compute_hash({}),
        )
        captured_actors.append(actor)
        return actor

    mock_service = MagicMock()
    mock_service.list_actors.side_effect = lambda: list(captured_actors)
    mock_service.get_default_actor.return_value = None
    mock_service.upsert_actor.side_effect = _capturing_upsert

    mock_settings = MagicMock()
    mock_settings.resolve_provider_defaults.return_value = MagicMock(
        provider=None, model=None
    )

    registry = ActorRegistry(
        actor_service=mock_service,
        provider_registry=provider_reg,
        settings=mock_settings,
    )
    return mock_service, registry


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given("the actor registry has no configured providers for actor-list-empty")
def step_no_providers(context: Any) -> None:
    """Set up a real ``ActorRegistry`` with zero configured providers.

    Uses ``_make_real_registry([])`` so the production code path through
    ``ensure_built_in_actors()`` is exercised (returns ``[]`` naturally)
    rather than a fully-mocked registry that bypasses it.
    """
    service, registry = _make_real_registry([])
    context.actor_empty_service = service
    context.actor_empty_registry = registry


@given('the actor registry has a provider with model "{model}" for actor-list-empty')
def step_custom_model_provider(context: Any, model: str) -> None:
    """Set up a real registry with a provider using an arbitrary model name."""
    fake_provider = FakeProviderInfo(
        name="EdgeCaseProvider",
        default_model=model,
    )
    service, registry = _make_real_registry([fake_provider])
    context.actor_empty_service = service
    context.actor_empty_registry = registry


@given(
    "the actor registry has a provider with a multi-slash model for actor-list-empty"
)
def step_multi_slash_provider(context: Any) -> None:
    """Set up a real registry that will attempt to build an actor name
    with multiple slashes (e.g. ``Openrouter/anthropic/claude-...``).

    We use the real ``ActorRegistry`` to exercise the actual code path.
    The actor service and provider registry are mocked to isolate the
    name-construction logic.
    """
    fake_provider = FakeProviderInfo(
        name="Openrouter",
        default_model="anthropic/claude-sonnet-4-20250514",
    )
    service, registry = _make_real_registry([fake_provider])
    context.actor_empty_service = service
    context.actor_empty_registry = registry


# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------


@when("I run actor list via the actor-list-empty CLI")
def step_run_actor_list(context: Any) -> None:
    """Invoke ``actor list`` with the prepared mocks.

    Built-in actors are populated via ``ensure_built_in_actors()``
    **before** the list call because ``list_actors()`` is now read-only
    and no longer triggers writes (bug #797 fix).
    """
    context.actor_empty_registry.ensure_built_in_actors()
    with patch(
        _PATCH_GET_SERVICES,
        return_value=(
            context.actor_empty_service,
            context.actor_empty_registry,
        ),
    ):
        context.actor_empty_result = runner.invoke(actor_app, ["list"])


# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------


@then('the actor-list-empty output should contain "{text}"')
def step_output_contains(context: Any, text: str) -> None:
    result = context.actor_empty_result
    assert result is not None, "actor list was not invoked"
    output = result.output
    assert text.lower() in output.lower(), (
        f"Expected '{text}' in output but got:\n{output}"
    )


@then('the actor-list-empty output should not contain "{text}"')
def step_output_not_contains(context: Any, text: str) -> None:
    result = context.actor_empty_result
    assert result is not None, "actor list was not invoked"
    output = result.output
    assert text.lower() not in output.lower(), (
        f"Did NOT expect '{text}' in output but found it:\n{output}"
    )


@then("the actor-list-empty exit code should be {code:d}")
def step_exit_code(context: Any, code: int) -> None:
    result = context.actor_empty_result
    assert result is not None, "actor list was not invoked"
    actual = result.exit_code
    assert actual == code, (
        f"Expected exit code {code}, got {actual}. Output:\n{result.output}"
    )


@then("the upserted actor-list-empty actor name should contain exactly one slash")
def step_upserted_name_single_slash(context: Any) -> None:
    """Verify that every name passed to ``upsert_actor`` has exactly one ``/``."""
    service = context.actor_empty_service
    assert service.upsert_actor.call_count > 0, "upsert_actor was never called"
    for call in service.upsert_actor.call_args_list:
        name = call.kwargs.get("name") or call.args[0]
        slash_count = name.count("/")
        assert slash_count == 1, (
            f"Expected exactly 1 slash in actor name '{name}', found {slash_count}"
        )


@then('the upserted actor-list-empty actor name should be "{expected_name}"')
def step_upserted_name_exact(context: Any, expected_name: str) -> None:
    """Verify that the first ``upsert_actor`` call used the expected name."""
    service = context.actor_empty_service
    assert service.upsert_actor.call_count > 0, "upsert_actor was never called"
    first_call = service.upsert_actor.call_args_list[0]
    actual = first_call.kwargs.get("name") or first_call.args[0]
    assert actual == expected_name, (
        f"Expected actor name '{expected_name}', got '{actual}'"
    )
