"""Step definitions for resolve_config_files unit tests, edge cases, and
error propagation in actor run positional-argument tests.

This module contains the ``resolve_config_files`` unit-level BDD
scenarios as well as shared helpers used by
``actor_run_signature_cli_steps.py``.
"""

from __future__ import annotations

import contextlib
import tempfile
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import click
import typer
import yaml
from behave import then, when

with contextlib.suppress(ImportError, ModuleNotFoundError):
    from cleveragents.cli.commands._resolve_actor import (
        _cleanup_temp_files,
        _temp_files,
        resolve_config_files,
    )
from cleveragents.core.exceptions import InfrastructureError, NotFoundError

# ---------------------------------------------------------------------------
# Shared helpers (also imported by actor_run_signature_cli_steps)
# ---------------------------------------------------------------------------


def _make_app(
    *,
    result: str,
    config_global_context: dict[str, Any] | None = None,
) -> MagicMock:
    app_exec = MagicMock()
    app_exec.config = SimpleNamespace(global_context=dict(config_global_context or {}))
    app_exec.run_single_shot = AsyncMock(return_value=result)
    return app_exec


def _get_combined_output(result: Any) -> str:
    """Return combined stdout + stderr from a CliRunner result."""
    output = result.output or ""
    stderr = getattr(result, "stderr", None) or ""
    return output + stderr


# ---------------------------------------------------------------------------
# resolve_config_files unit tests (P2-3 / P2-4)
# ---------------------------------------------------------------------------


@when("I call resolve_config_files with a config list")
def step_resolve_with_config_list(context: Any) -> None:
    cfg = [Path(tempfile.gettempdir()) / "dummy.yaml"]
    context.resolve_result = resolve_config_files("ignored", cfg)
    context.expected_config = cfg


@then("the original config list is returned unchanged")
def step_config_returned_unchanged(context: Any) -> None:
    assert context.resolve_result is context.expected_config


@when("I call resolve_config_files with a name and no config")
def step_resolve_with_name_no_config(context: Any) -> None:
    mock_actor = MagicMock()
    mock_actor.yaml_text = "name: test-actor\ntype: custom\n"
    mock_actor.config_blob = None

    mock_registry = MagicMock()
    mock_registry.get.return_value = mock_actor

    mock_container = MagicMock()
    mock_container.actor_registry.return_value = mock_registry

    with patch(
        "cleveragents.cli.commands._resolve_actor.get_container",
        return_value=mock_container,
    ):
        context.resolve_result = resolve_config_files("local/test-actor", [])

    context.mock_registry = mock_registry


@then("a temporary YAML file is created from the registry actor")
def step_temp_file_from_registry(context: Any) -> None:
    assert len(context.resolve_result) == 1
    tmp_path = context.resolve_result[0]
    context.add_cleanup(lambda p=tmp_path: p.unlink(missing_ok=True))
    assert tmp_path.exists()
    content = tmp_path.read_text(encoding="utf-8")
    assert "test-actor" in content
    context.mock_registry.get.assert_called_once_with("local/test-actor")


@when("I call resolve_config_files with an actor that has no yaml_text")
def step_resolve_with_no_yaml_text(context: Any) -> None:
    mock_actor = MagicMock()
    mock_actor.yaml_text = ""
    mock_actor.config_blob = {"name": "blob-actor", "type": "custom"}

    mock_registry = MagicMock()
    mock_registry.get.return_value = mock_actor

    mock_container = MagicMock()
    mock_container.actor_registry.return_value = mock_registry

    with patch(
        "cleveragents.cli.commands._resolve_actor.get_container",
        return_value=mock_container,
    ):
        context.resolve_result = resolve_config_files("local/blob-actor", [])


@then("a temporary YAML file is created from config_blob")
def step_temp_file_from_config_blob(context: Any) -> None:
    assert len(context.resolve_result) == 1
    tmp_path = context.resolve_result[0]
    context.add_cleanup(lambda p=tmp_path: p.unlink(missing_ok=True))
    assert tmp_path.exists()
    content = tmp_path.read_text(encoding="utf-8")
    assert "blob-actor" in content
    parsed = yaml.safe_load(content)
    assert isinstance(parsed, dict)
    assert parsed["name"] == "blob-actor"


@when("I call resolve_config_files with an actor that has no config data")
def step_resolve_with_no_config_data(context: Any) -> None:
    mock_actor = MagicMock()
    mock_actor.yaml_text = ""
    mock_actor.config_blob = None

    mock_registry = MagicMock()
    mock_registry.get.return_value = mock_actor

    mock_container = MagicMock()
    mock_container.actor_registry.return_value = mock_registry

    captured_stderr: list[str] = []
    original_echo = typer.echo

    def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
        if err:
            captured_stderr.append(message)
        original_echo(message, err=err, **kwargs)

    with (
        patch(
            "cleveragents.cli.commands._resolve_actor.get_container",
            return_value=mock_container,
        ),
        patch(
            "cleveragents.cli.commands._resolve_actor.typer.echo",
            side_effect=_capture_echo,
        ),
    ):
        try:
            resolve_config_files("local/empty-actor", [])
            context.resolve_exit_code = 0
        except (SystemExit, click.exceptions.Exit) as exc:
            context.resolve_exit_code = getattr(
                exc, "exit_code", getattr(exc, "code", 1)
            )
    context.resolve_stderr = " ".join(captured_stderr)


@then("it should exit with a no-configuration-data error")
def step_exit_no_config_data(context: Any) -> None:
    assert context.resolve_exit_code == 2
    assert "no configuration data" in context.resolve_stderr


@when("I call resolve_config_files with an unknown actor name directly")
def step_resolve_unknown_actor_directly(context: Any) -> None:
    mock_registry = MagicMock()
    mock_registry.get.side_effect = NotFoundError(
        resource_type="actor", resource_id="nonexistent/actor"
    )

    mock_container = MagicMock()
    mock_container.actor_registry.return_value = mock_registry

    captured_stderr: list[str] = []
    original_echo = typer.echo

    def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
        if err:
            captured_stderr.append(message)
        original_echo(message, err=err, **kwargs)

    with (
        patch(
            "cleveragents.cli.commands._resolve_actor.get_container",
            return_value=mock_container,
        ),
        patch(
            "cleveragents.cli.commands._resolve_actor.typer.echo",
            side_effect=_capture_echo,
        ),
    ):
        try:
            resolve_config_files("nonexistent/actor", [])
            context.resolve_exit_code = 0
        except (SystemExit, click.exceptions.Exit) as exc:
            context.resolve_exit_code = getattr(
                exc, "exit_code", getattr(exc, "code", 1)
            )
    context.resolve_stderr = " ".join(captured_stderr)


@then("it should exit with a not-found error")
def step_exit_not_found(context: Any) -> None:
    assert context.resolve_exit_code == 2
    assert "not found in registry" in context.resolve_stderr


# ---------------------------------------------------------------------------
# Edge cases and error propagation (P2-3, P3-2, P3-4)
# ---------------------------------------------------------------------------


@when("I call resolve_config_files with an actor that has empty config_blob")
def step_resolve_with_empty_config_blob(context: Any) -> None:
    mock_actor = MagicMock()
    mock_actor.yaml_text = ""
    mock_actor.config_blob = {}

    mock_registry = MagicMock()
    mock_registry.get.return_value = mock_actor

    mock_container = MagicMock()
    mock_container.actor_registry.return_value = mock_registry

    captured_stderr: list[str] = []
    original_echo = typer.echo

    def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
        if err:
            captured_stderr.append(message)
        original_echo(message, err=err, **kwargs)

    with (
        patch(
            "cleveragents.cli.commands._resolve_actor.get_container",
            return_value=mock_container,
        ),
        patch(
            "cleveragents.cli.commands._resolve_actor.typer.echo",
            side_effect=_capture_echo,
        ),
    ):
        try:
            resolve_config_files("local/empty-blob-actor", [])
            context.resolve_exit_code = 0
        except (SystemExit, click.exceptions.Exit) as exc:
            context.resolve_exit_code = getattr(
                exc, "exit_code", getattr(exc, "code", 1)
            )
    context.resolve_stderr = " ".join(captured_stderr)


@when("I call resolve_config_files and the container raises an infrastructure error")
def step_resolve_with_infrastructure_error(context: Any) -> None:
    with patch(
        "cleveragents.cli.commands._resolve_actor.get_container",
        side_effect=InfrastructureError("database unavailable"),
    ):
        try:
            resolve_config_files("local/some-actor", [])
            context.infra_error_propagated = False
        except InfrastructureError:
            context.infra_error_propagated = True


@then("the infrastructure error should propagate uncaught")
def step_infra_error_propagated(context: Any) -> None:
    assert context.infra_error_propagated is True


@when("I call resolve_config_files with an empty string name")
def step_resolve_with_empty_name(context: Any) -> None:
    captured_stderr: list[str] = []
    original_echo = typer.echo

    def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
        if err:
            captured_stderr.append(message)
        original_echo(message, err=err, **kwargs)

    with patch(
        "cleveragents.cli.commands._resolve_actor.typer.echo",
        side_effect=_capture_echo,
    ):
        try:
            resolve_config_files("", [])
            context.empty_name_exit_code = 0
        except (SystemExit, typer.Exit) as exc:
            context.empty_name_exit_code = getattr(
                exc, "exit_code", getattr(exc, "code", 1)
            )
    context.resolve_stderr = " ".join(captured_stderr)


@then("the empty name should be rejected with a validation error")
def step_empty_name_rejected(context: Any) -> None:
    assert context.empty_name_exit_code == 2
    assert "must not be empty" in context.resolve_stderr


# ---------------------------------------------------------------------------
# YAML serialisation failure (P2-1)
# ---------------------------------------------------------------------------


@when(
    "I call resolve_config_files with an actor whose config_blob is not YAML-serialisable"
)
def step_resolve_with_unserializable_config_blob(context: Any) -> None:
    mock_actor = MagicMock()
    mock_actor.yaml_text = ""
    # yaml.safe_dump cannot serialise arbitrary objects
    mock_actor.config_blob = {"bad_value": object()}

    mock_registry = MagicMock()
    mock_registry.get.return_value = mock_actor

    mock_container = MagicMock()
    mock_container.actor_registry.return_value = mock_registry

    captured_stderr: list[str] = []
    original_echo = typer.echo

    def _capture_echo(message: str = "", err: bool = False, **kwargs: Any) -> None:
        if err:
            captured_stderr.append(message)
        original_echo(message, err=err, **kwargs)

    with (
        patch(
            "cleveragents.cli.commands._resolve_actor.get_container",
            return_value=mock_container,
        ),
        patch(
            "cleveragents.cli.commands._resolve_actor.typer.echo",
            side_effect=_capture_echo,
        ),
    ):
        try:
            resolve_config_files("local/bad-blob-actor", [])
            context.resolve_exit_code = 0
        except (SystemExit, click.exceptions.Exit) as exc:
            context.resolve_exit_code = getattr(
                exc, "exit_code", getattr(exc, "code", 1)
            )
    context.resolve_stderr = " ".join(captured_stderr)


@then("it should exit with a serialisation error")
def step_exit_serialisation_error(context: Any) -> None:
    assert context.resolve_exit_code == 2
    assert "could not be serialised" in context.resolve_stderr


# ---------------------------------------------------------------------------
# Review round 5 — P3-1: whitespace-only yaml_text
# ---------------------------------------------------------------------------


@when("I call resolve_config_files with an actor that has whitespace-only yaml_text")
def step_resolve_with_whitespace_yaml_text(context: Any) -> None:
    mock_actor = MagicMock()
    mock_actor.yaml_text = "   \n  \t  "
    mock_actor.config_blob = {"name": "ws-actor", "type": "custom"}

    mock_registry = MagicMock()
    mock_registry.get.return_value = mock_actor

    mock_container = MagicMock()
    mock_container.actor_registry.return_value = mock_registry

    with patch(
        "cleveragents.cli.commands._resolve_actor.get_container",
        return_value=mock_container,
    ):
        context.resolve_result = resolve_config_files("local/ws-actor", [])


@then("a temporary YAML file is created from the whitespace actor config_blob")
def step_temp_file_from_whitespace_config_blob(context: Any) -> None:
    assert len(context.resolve_result) == 1
    tmp_path = context.resolve_result[0]
    context.add_cleanup(lambda p=tmp_path: p.unlink(missing_ok=True))
    assert tmp_path.exists()
    content = tmp_path.read_text(encoding="utf-8")
    assert "ws-actor" in content
    parsed = yaml.safe_load(content)
    assert isinstance(parsed, dict)
    assert parsed["name"] == "ws-actor"


# ---------------------------------------------------------------------------
# Review round 6 — m8: _cleanup_temp_files removes tracked files
# ---------------------------------------------------------------------------


@when("I call resolve_config_files and then cleanup_temp_files")
def step_resolve_then_cleanup(context: Any) -> None:
    _cleanup_temp_files()

    mock_actor = MagicMock()
    mock_actor.yaml_text = "name: cleanup-test-actor\ntype: custom\n"
    mock_actor.config_blob = None

    mock_registry = MagicMock()
    mock_registry.get.return_value = mock_actor

    mock_container = MagicMock()
    mock_container.actor_registry.return_value = mock_registry

    with patch(
        "cleveragents.cli.commands._resolve_actor.get_container",
        return_value=mock_container,
    ):
        result_paths = resolve_config_files("local/cleanup-test-actor", [])

    context.cleanup_tmp_path = result_paths[0]
    # Verify file exists before cleanup
    assert context.cleanup_tmp_path.exists()
    # Verify path was tracked
    assert str(context.cleanup_tmp_path) in _temp_files

    _cleanup_temp_files()


@then("the temporary file should be removed by cleanup")
def step_temp_file_removed_by_cleanup(context: Any) -> None:
    assert not context.cleanup_tmp_path.exists()
    assert len(_temp_files) == 0


# ---------------------------------------------------------------------------
# Review round 6 — m9: actor_registry() raising an infrastructure error
# ---------------------------------------------------------------------------


@when("I call resolve_config_files and actor_registry raises an infrastructure error")
def step_resolve_with_actor_registry_error(context: Any) -> None:
    mock_container = MagicMock()
    mock_container.actor_registry.side_effect = InfrastructureError(
        "actor registry unavailable"
    )

    with patch(
        "cleveragents.cli.commands._resolve_actor.get_container",
        return_value=mock_container,
    ):
        try:
            resolve_config_files("local/some-actor", [])
            context.actor_registry_error_propagated = False
        except InfrastructureError:
            context.actor_registry_error_propagated = True


@then("the actor_registry infrastructure error should propagate uncaught")
def step_actor_registry_error_propagated(context: Any) -> None:
    assert context.actor_registry_error_propagated is True
