"""Step definitions for actor add --update flag enforcement (issue #2609).

Tests that `agents actor add` rejects re-adding an existing actor without
the --update flag, and succeeds when --update is provided.
"""

from __future__ import annotations

import json
import tempfile
from datetime import datetime
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 NotFoundError
from cleveragents.domain.models.core.actor import Actor


def _make_actor(
    *,
    name: str = "local/existing-actor",
    provider: str = "openai",
    model: str = "gpt-4",
    config: dict[str, Any] | None = None,
    updated_at: datetime | None = None,
) -> Actor:
    blob = config or {}
    return Actor(
        id=1,
        name=name,
        provider=provider,
        model=model,
        config_blob=blob,
        config_hash=Actor.compute_hash(blob),
        unsafe=False,
        is_built_in=False,
        is_default=False,
        updated_at=updated_at or datetime(2026, 2, 7, 14, 22, 0),
    )


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


def _write_actor_config(name: str = "local/existing-actor") -> Path:
    config_data = {
        "name": name,
        "provider": "openai",
        "model": "gpt-4",
    }
    with tempfile.NamedTemporaryFile(
        delete=False, suffix=".json", mode="w", encoding="utf-8"
    ) as handle:
        json.dump(config_data, handle)
        handle.flush()
    return Path(handle.name)


@given("an actor add CLI runner where the actor already exists")
def step_given_actor_exists(context: Any) -> None:
    context.runner = CliRunner()
    context.actor_name = "local/existing-actor"
    context.actor_config_path = _write_actor_config(context.actor_name)
    _register_cleanup(context, context.actor_config_path)

    # The existing actor that will be returned by get_actor
    context.existing_actor = _make_actor(
        name=context.actor_name,
        updated_at=datetime(2026, 2, 7, 14, 22, 0),
    )
    # The actor returned after a successful upsert (for --update case)
    context.updated_actor = _make_actor(
        name=context.actor_name,
        updated_at=datetime(2026, 2, 7, 14, 22, 0),
    )


@given("an actor add CLI runner where the actor does not exist")
def step_given_actor_not_exists(context: Any) -> None:
    context.runner = CliRunner()
    context.actor_name = "local/new-actor"
    context.actor_config_path = _write_actor_config(context.actor_name)
    _register_cleanup(context, context.actor_config_path)

    context.new_actor = _make_actor(name=context.actor_name)


@when("I run actor add without the --update flag")
def step_when_add_without_update(context: Any) -> None:
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        registry = MagicMock()
        if hasattr(context, "existing_actor"):
            # Actor exists — get_actor returns it, triggering the error path
            registry.get_actor.return_value = context.existing_actor
        else:
            # Actor does not exist — get_actor raises NotFoundError
            registry.get_actor.side_effect = NotFoundError(
                resource_type="actor", resource_id=context.actor_name
            )
            registry.upsert_actor.return_value = context.new_actor
        mock_svc.return_value = (MagicMock(), registry)
        context.result = context.runner.invoke(
            actor_app,
            ["add", "--config", str(context.actor_config_path)],
        )


@when("I run actor add with the --update flag")
def step_when_add_with_update(context: Any) -> None:
    with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
        registry = MagicMock()
        # get_actor returns the existing actor (simulating actor already exists)
        registry.get_actor.return_value = context.existing_actor
        # upsert_actor returns the updated actor
        registry.upsert_actor.return_value = context.updated_actor
        mock_svc.return_value = (MagicMock(), registry)
        context.result = context.runner.invoke(
            actor_app,
            ["add", "--config", str(context.actor_config_path), "--update"],
        )


@then("the actor-add-enforcement exit code should be {code:d}")
def step_then_exit_code(context: Any, code: int) -> None:
    assert context.result.exit_code == code, (
        f"Expected exit code {code}, got {context.result.exit_code}.\n"
        f"Output:\n{context.result.output}"
    )


@then('the actor-add-enforcement output should contain an error panel with "{text}"')
def step_then_output_contains_error_panel(context: Any, text: str) -> None:
    output = context.result.output
    assert text in output, f"Expected '{text}' in output:\n{output}"
    assert "Error" in output, f"Expected 'Error' panel title in output:\n{output}"


@then('the actor-add-enforcement output should contain "{text}"')
def step_then_output_contains(context: Any, text: str) -> None:
    output = context.result.output
    assert text in output, f"Expected '{text}' in output:\n{output}"


@then("the actor-add-enforcement output should contain the registration timestamp")
def step_then_output_contains_timestamp(context: Any) -> None:
    output = context.result.output
    # The timestamp should be formatted as "YYYY-MM-DD HH:MM"
    assert "2026-02-07 14:22" in output, (
        f"Expected registration timestamp '2026-02-07 14:22' in output:\n{output}"
    )
