# pyright: reportRedeclaration=false
"""Step definitions for the Actor CLI YAML-first alignment feature."""

from __future__ import annotations

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

from behave import then, when

from cleveragents.cli.commands.actor import app as actor_app
from cleveragents.domain.models.core.actor import Actor

_ENVELOPE_KEYS = {"command", "status", "exit_code", "data", "timing", "messages"}


def _unwrap_envelope(parsed: Any) -> Any:
    """Return the ``data`` field if *parsed* is a spec envelope, else *parsed* as-is."""
    if isinstance(parsed, dict) and _ENVELOPE_KEYS.issubset(parsed.keys()):
        return parsed["data"]
    return parsed


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,
    )


# ------------------------------------------------------------------
# List with format
# ------------------------------------------------------------------


@when("I run actor list with two actors using format json")
def step_list_json(context: Any) -> None:
    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_svc:
        mock_svc.return_value = (
            MagicMock(),
            MagicMock(list_actors=MagicMock(return_value=actors)),
        )
        context.result = context.runner.invoke(actor_app, ["list", "--format", "json"])
    context.actors = actors


@when("I run actor list with two actors using format yaml")
def step_list_yaml(context: Any) -> None:
    actors = [
        _make_actor(name="local/first", provider="p1", model="m1"),
        _make_actor(name="local/second", provider="p2", model="m2"),
    ]
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        mock_svc.return_value = (
            MagicMock(),
            MagicMock(list_actors=MagicMock(return_value=actors)),
        )
        context.result = context.runner.invoke(actor_app, ["list", "--format", "yaml"])
    context.actors = actors


@when("I run actor list with two actors using format plain")
def step_list_plain(context: Any) -> None:
    actors = [
        _make_actor(name="local/first", provider="p1", model="m1"),
        _make_actor(name="local/second", provider="p2", model="m2"),
    ]
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        mock_svc.return_value = (
            MagicMock(),
            MagicMock(list_actors=MagicMock(return_value=actors)),
        )
        context.result = context.runner.invoke(actor_app, ["list", "--format", "plain"])
    context.actors = actors


@then("the actor list output should be valid JSON")
def step_list_json_valid(context: Any) -> None:
    assert context.result.exit_code == 0
    parsed = json.loads(context.result.output.strip())
    data = _unwrap_envelope(parsed)
    assert isinstance(data, list)
    assert len(data) == 2
    assert data[0]["name"] == "local/first"
    assert data[1]["name"] == "local/second"


@then("the actor list output should contain YAML keys")
def step_list_yaml_valid(context: Any) -> None:
    assert context.result.exit_code == 0
    output = context.result.output
    assert "name:" in output
    assert "provider:" in output
    assert "local/first" in output


@then("the actor list output should contain plain text fields")
def step_list_plain_valid(context: Any) -> None:
    assert context.result.exit_code == 0
    output = context.result.output
    assert "name:" in output
    assert "provider:" in output
    assert "local/first" in output


# ------------------------------------------------------------------
# Show with format
# ------------------------------------------------------------------


@when("I run actor show with format json")
def step_show_json(context: Any) -> None:
    actor = _make_actor(name="local/show-json", provider="openai", model="gpt-4")
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        registry = MagicMock()
        registry.get_actor.return_value = actor
        mock_svc.return_value = (MagicMock(), registry)
        context.result = context.runner.invoke(
            actor_app, ["show", actor.name, "--format", "json"]
        )
    context.actor = actor


@when("I run actor show with format yaml")
def step_show_yaml(context: Any) -> None:
    actor = _make_actor(name="local/show-yaml", provider="openai", model="gpt-4")
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        registry = MagicMock()
        registry.get_actor.return_value = actor
        mock_svc.return_value = (MagicMock(), registry)
        context.result = context.runner.invoke(
            actor_app, ["show", actor.name, "--format", "yaml"]
        )
    context.actor = actor


@when("I run actor show with format plain")
def step_show_plain(context: Any) -> None:
    actor = _make_actor(name="local/show-plain", provider="openai", model="gpt-4")
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        registry = MagicMock()
        registry.get_actor.return_value = actor
        mock_svc.return_value = (MagicMock(), registry)
        context.result = context.runner.invoke(
            actor_app, ["show", actor.name, "--format", "plain"]
        )
    context.actor = actor


@then("the actor show output should be valid JSON with actor fields")
def step_show_json_valid(context: Any) -> None:
    assert context.result.exit_code == 0
    parsed = json.loads(context.result.output.strip())
    data = _unwrap_envelope(parsed)
    assert isinstance(data, dict)
    assert data["name"] == "local/show-json"
    assert data["provider"] == "openai"
    assert data["model"] == "gpt-4"
    assert "config_hash" in data
    assert "updated_at" in data


@then("the actor show output should contain YAML actor keys")
def step_show_yaml_valid(context: Any) -> None:
    assert context.result.exit_code == 0
    output = context.result.output
    assert "name:" in output
    assert "local/show-yaml" in output
    assert "provider:" in output


@then("the actor show output should contain plain actor fields")
def step_show_plain_valid(context: Any) -> None:
    assert context.result.exit_code == 0
    output = context.result.output
    assert "name:" in output
    assert "local/show-plain" in output
    assert "provider:" in output


# ------------------------------------------------------------------
# Add with --update flag
# ------------------------------------------------------------------


@when("I run actor add with update flag")
def step_add_update(context: Any) -> None:
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        mock_registry = MagicMock()
        mock_service = MagicMock()
        mock_actor = _make_actor(config=context.actor_config_data)
        # registry.add() is the YAML-first path used by the add command
        mock_registry.add.return_value = mock_actor
        mock_svc.return_value = (mock_service, mock_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),
                "--update",
            ],
        )
        context.mock_actor_registry = mock_registry
        context.expected_error = None


@then("the output should say Actor updated")
def step_output_actor_updated(context: Any) -> None:
    assert context.result.exit_code == 0
    assert "Actor updated" in context.result.output


# ------------------------------------------------------------------
# Add with --format
# ------------------------------------------------------------------


@when("I run actor add with format json")
def step_add_format_json(context: Any) -> None:
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        mock_registry = MagicMock()
        mock_service = MagicMock()
        mock_actor = _make_actor(config=context.actor_config_data)
        # registry.add() is the YAML-first path used by the add command
        mock_registry.add.return_value = mock_actor
        mock_svc.return_value = (mock_service, mock_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),
                "--format",
                "json",
            ],
        )


@when("I run actor add with format yaml")
def step_add_format_yaml(context: Any) -> None:
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        mock_registry = MagicMock()
        mock_service = MagicMock()
        mock_actor = _make_actor(config=context.actor_config_data)
        # registry.add() is the YAML-first path used by the add command
        mock_registry.add.return_value = mock_actor
        mock_svc.return_value = (mock_service, mock_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),
                "--format",
                "yaml",
            ],
        )


@then("the actor add output should contain JSON data")
def step_add_json_valid(context: Any) -> None:
    assert context.result.exit_code == 0
    parsed = json.loads(context.result.output.strip())
    data = _unwrap_envelope(parsed)
    assert isinstance(data, dict)
    assert "name" in data


@then("the actor add output should contain YAML data")
def step_add_yaml_valid(context: Any) -> None:
    assert context.result.exit_code == 0
    output = context.result.output
    assert "name:" in output
    assert "provider:" in output


# ------------------------------------------------------------------
# Remove by namespaced name
# ------------------------------------------------------------------


@when("I run actor remove with namespaced name")
def step_remove_namespaced(context: Any) -> None:
    with (
        patch("cleveragents.cli.commands.actor._get_services") as mock_svc,
        patch("cleveragents.cli.commands.actor._compute_actor_impact") as mock_impact,
    ):
        mock_registry = MagicMock()
        mock_svc.return_value = (MagicMock(), mock_registry)
        mock_impact.return_value = (0, 0, 0)
        context.result = context.runner.invoke(
            actor_app, ["remove", "local/my-custom-actor"]
        )
        context.mock_actor_registry = mock_registry


@then("the actor remove should succeed for namespaced name")
def step_remove_namespaced_ok(context: Any) -> None:
    assert context.result.exit_code == 0
    context.mock_actor_registry.remove_actor.assert_called_once_with(
        "local/my-custom-actor"
    )
    assert (
        "Actor removed" in context.result.output
        or "Removed actor" in context.result.output
    )


# ------------------------------------------------------------------
# Update with --format
# ------------------------------------------------------------------


@when("I run actor update with format json")
def step_update_format_json(context: Any) -> None:
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        mock_registry = MagicMock()
        mock_service = MagicMock()
        current = _make_actor(
            name="local/update-json",
            provider="openai",
            model="gpt-4",
            config={"provider": "openai", "model": "gpt-4"},
        )
        mock_registry.get_actor.return_value = current
        updated = _make_actor(
            name=current.name,
            provider=current.provider,
            model=current.model,
            config=current.config_blob,
        )
        mock_registry.upsert_actor.return_value = updated
        mock_svc.return_value = (mock_service, mock_registry)
        context.result = context.runner.invoke(
            actor_app,
            ["update", current.name, "--format", "json"],
        )


@then("the actor update output should contain JSON data")
def step_update_json_valid(context: Any) -> None:
    assert context.result.exit_code == 0
    parsed = json.loads(context.result.output.strip())
    data = _unwrap_envelope(parsed)
    assert isinstance(data, dict)
    assert data["name"] == "local/update-json"
