"""Step definitions for actor add combined config-actor format.

Tests for features/actor_add_combined_config_format.feature — validates that
the ``agents actor add --config`` command correctly handles the spec-compliant
combined ``config.actor`` format (issue #11189).
"""

from __future__ import annotations

import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

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

from cleveragents.actor.schema import (
    ActorConfigSchema,
    _detect_nested_config_actor,
    _flatten_config_actor,
)
from cleveragents.cli.commands.actor import app as actor_app
from cleveragents.core.exceptions import NotFoundError
from cleveragents.domain.models.core.actor import Actor

# ──── Given / When / Then steps ------------------------------------------------


@given(
    'a v3 LLM actor YAML file with combined config.actor format "{actor_string}"'
    ' and name "{name}"'
)
def step_llm_combined_string(context: Context, actor_string: str, name: str) -> None:
    """Create a valid v3 LLM actor YAML using combined-format string.

    The YAML places all actor metadata inside ``config → actor`` as a compact
    ``"<provider>/<model>"`` string (issue #11189).
    """
    yaml_content = f"""\
name: {name}
type: llm
config:
  actor: "{actor_string}"
description: Combined-format LLM actor
"""
    context.config_file = _write_yaml_file(
        context, name.replace("/", "_"), yaml_content
    )


@given(
    'a v3 LLM actor YAML file with nested config.actor format "{actor_string}"'
    ' and name "{name}"'
)
def step_llm_nested_dict_format(context: Context, actor_string: str, name: str) -> None:
    """Create a v3 LLM actor YAML using the nested-dict config-actor structure.

    The YAML nests ``type``, ``provider``, and ``model`` inside ``config::actor``
    as a dictionary (issue #11189).
    """
    parts = actor_string.split("/", 1)
    provider = parts[0] if len(parts) == 2 else "custom"
    model = parts[1] if len(parts) == 2 else parts[0]

    yaml_content = f"""\
name: {name}
type: llm
config:
  actor:
    type: llm
    provider: {provider}
    model: {model}
description: Nested-dict config-actor LLM
"""
    context.config_file = _write_yaml_file(
        context, name.replace("/", "_"), yaml_content
    )


@given(
    "a YAML config file with nested config format and combined string "
    "without explicit type"
)
def step_nested_combined_no_type(context: Context) -> None:
    """Create a YAML where ``config.actor`` is a compact string but there's no v3 ``type`` at top level."""
    yaml_content = """\
name: local/nested-combined-llm
description: Nested config with combined actor string
config:
  actor: "gcp/gemini"
"""
    context.config_file = _write_yaml_file(context, "nested-combined", yaml_content)


@given("a YAML config file with nested config and top-level name")
def step_nested_with_top_level_name(context: Context) -> None:
    """Create a YAML where ``config.actor`` holds metadata but the top-level ``name`` field is used."""
    yaml_content = """\
name: local/config-with-name
description: Actor with nested config and explicit name
config:
  actor: "aws/bedrock"
"""
    context.config_file = _write_yaml_file(context, "config-with-name", yaml_content)


@given("a YAML config file with nested config but no name anywhere")
def step_nested_no_name(context: Context) -> None:
    """Create a YAML that uses nested config format but has NO ``name`` field at all."""
    yaml_content = """\
description: Actor missing a name
config:
  actor: "azure/vllm"
"""
    context.config_file = _write_yaml_file(context, "nested-no-name", yaml_content)


@given("I run the actor add command")
def step_run_actor_add_generic(context: Context) -> None:
    """Run the actor-add CLI with a config file stored in context."""
    if context.config_file is None:
        raise ValueError("No config file set")

    runner = getattr(context, "runner", CliRunner())
    name = getattr(context, "actor_add_name", "local/nested-no-name")

    mock_actor = Actor(
        id=1,
        name=name,
        provider="custom",
        model="default",
        config_blob={"name": name},
        config_hash=Actor.compute_hash({"name": name}),
        graph_descriptor=None,
        unsafe=False,
        is_built_in=False,
        is_default=False,
    )
    mock_registry = MagicMock()
    mock_registry.upsert_actor.return_value = mock_actor
    mock_registry.get_actor.side_effect = NotFoundError("not found")
    mock_service = MagicMock()

    with (
        patch("cleveragents.cli.commands.actor._get_services") as mock_get_services,
        patch.object(
            ActorConfigSchema,
            "model_validate",
            wraps=ActorConfigSchema.model_validate,
        ) as schema_spy,
    ):
        mock_get_services.return_value = (mock_service, mock_registry)
        result = runner.invoke(
            actor_app,
            ["add", name, "--config", str(context.config_file)],
            catch_exceptions=True,
        )

    context.schema_validate_spy = schema_spy
    context.cli_result = result
    success = result.exit_code == 0
    context.command_result = {
        "success": success,
        "exit_code": result.exit_code,
        "output": result.output,
    }
    if success:
        assert result.exception is None, f"Unexpected exception: {result.exception}"
        context.command_error = None
        context.actor_registered = True
        # Capture metadata sent to upsert_actor so the shared assertion
        # step ("the actor should be registered with type ...") defined in
        # actor_add_v3_schema_validation_steps.py can read it.
        if mock_registry.upsert_actor.called:
            call_kwargs = mock_registry.upsert_actor.call_args
            blob = call_kwargs.kwargs.get("config_blob") if call_kwargs.kwargs else None
            if isinstance(blob, dict):
                context.registered_actor_type = blob.get("type")
                context.registered_actor_provider = blob.get("provider")
                context.registered_actor_model = blob.get("model")
            else:
                context.registered_actor_type = None
                context.registered_actor_provider = None
                context.registered_actor_model = None
        else:
            context.registered_actor_type = None
            context.registered_actor_provider = None
            context.registered_actor_model = None
    else:
        assert isinstance(result.exception, SystemExit), (
            f"Expected SystemExit, got: {result.exception}"
        )
        context.command_error = result.output
        context.actor_registered = False
        context.registered_actor_type = None
        context.registered_actor_provider = None
        context.registered_actor_model = None


@when("I run the actor add command without a name argument")
def step_run_actor_add_no_name_arg(context: Context) -> None:
    """Run the actor add CLI WITHOUT a name positional argument.

    Exercises the CLI's "Actor name is required" validation path —
    distinct from the v3-schema step which always supplies a name.
    """
    if context.config_file is None:
        raise ValueError("No config file set")

    runner = getattr(context, "runner", CliRunner())
    mock_registry = MagicMock()
    mock_registry.get_actor.side_effect = NotFoundError("not found")
    mock_service = MagicMock()

    with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
        mock_get_services.return_value = (mock_service, mock_registry)
        result = runner.invoke(
            actor_app,
            ["add", "--config", str(context.config_file)],
            catch_exceptions=True,
        )

    context.cli_result = result
    success = result.exit_code == 0
    context.command_result = {
        "success": success,
        "exit_code": result.exit_code,
        "output": result.output,
    }
    context.command_error = None if success else result.output
    context.actor_registered = success


# ──── Then steps ---------------------------------------------------------------
# Note: ``v3actor the command should succeed/fail`` and ``v3actor the error
# message should contain`` are defined in actor_add_v3_schema_validation_steps.py
# and shared across the v3-actor feature suite. Don't redefine them here —
# behave's step registry is global and duplicate registrations raise
# AmbiguousStep at suite-load time.


@then("the name should be available at top-level after flattening")
def step_name_available_flat(context: Context) -> None:
    """Assert that _flatten_config_actor produced a flat dict with a 'name' field."""
    raw_blob = {"config": {"actor": {"name": "local/flat-actor"}}}
    flat = _flatten_config_actor(raw_blob)
    assert "name" in flat, f"Name should be top-level after flattening; blob: {flat}"
    assert flat["name"] == "local/flat-actor", f"Unexpected name: {flat['name']}"


@then("the config block should be removed after flattening")
def step_config_removed_flat(context: Context) -> None:
    """Assert that _flatten_config_actor removes the top-level ``config`` key."""
    raw_blob = {"config": {"actor": "aws/mistral"}, "name": "local/test"}
    flat = _flatten_config_actor(raw_blob)
    assert "config" not in flat, f"'config' should be removed; flat: {flat}"


# ──── is_v3_yaml / detection unit steps ----------------------------------------


@given('a config blob with combined-format string "{actor_string}"')
def step_config_blob_combined_string(context: Context, actor_string: str) -> None:
    """Store a config blob with combined-format ``config.actor`` string."""
    context.config_blob_under_test = {
        "name": "local/combined",
        "config": {"actor": actor_string},
    }


@when("I detect nested config-actor on the config blob")
def step_detect_nested_actor(context: Context) -> None:
    """Call _detect_nested_config_actor()."""
    context.detect_result = _detect_nested_config_actor(context.config_blob_under_test)


@then("is_v3_yaml should return True for combined-format string")
def step_is_v3_combined_true(context: Context) -> None:
    """Assert is_v3_yaml() returned True for a combined-format blob."""
    # Use the helper directly.
    assert _detect_nested_config_actor(context.config_blob_under_test) is True


@given('a config blob with nested-dict actor "{actor_type}"')
def step_config_blob_nested_dict(context: Context, actor_type: str) -> None:
    """Store a config blob with a ``config.actor`` dict."""
    context.config_blob_under_test = {
        "name": "local/nested",
        "type": actor_type,
        "config": {"actor": {"provider": "aws", "model": "gpt-4"}},
    }


@then("is_v3_yaml should return True for nested-dict config.actor")
def step_is_v3_nested_true(context: Context) -> None:
    """Assert is_v3_yaml() returns True for a nested-dict config-actor blob."""
    assert _detect_nested_config_actor(context.config_blob_under_test) is True, (
        "Should detect nested config-actor structure"
    )


@given("a config blob without any combined-format actor")
def step_config_blob_no_combined(context: Context) -> None:
    """Store a plain flat config blob (no ``config.actor``)."""
    context.config_blob_under_test = {
        "name": "local/flat-config",
        "type": "llm",
        "provider": "openai",
        "model": "gpt-4",
    }


@then("is_v3_yaml should return False when there is no combined-format actor to detect")
def step_detect_nested_false(context: Context) -> None:
    """Assert _detect_nested_config_actor() returns False for a plain blob."""
    assert not _detect_nested_config_actor(context.config_blob_under_test), (
        "Should NOT detect config-actor in a flat blob"
    )


# ──── Helpers ------------------------------------------------------------------


def _write_yaml_file(context: Context, name: str, content: str) -> Path:
    """Write a YAML file to the temporary directory."""
    if not hasattr(context, "temp_dir") or context.temp_dir is None:
        context.temp_dir = tempfile.TemporaryDirectory()
        result_context_add_cleanup(context, context.temp_dir.cleanup)

    safe_name = name.replace("/", "_")
    file_path = Path(context.temp_dir.name) / f"{safe_name}.yaml"
    file_path.write_text(content)
    return file_path


def result_context_add_cleanup(ctx, fn):
    """Add a cleanup handler to the context."""
    if not hasattr(ctx, "_cleanup_handlers"):
        ctx._cleanup_handlers = []
    ctx._cleanup_handlers.append(fn)
