"""Step definitions for TDD bug #1038.

``agents validation add`` missing ``--required`` flag.

This test captures bug #1038.  The specification
(``docs/specification.md`` line 22339) states that the validation mode
can be set "via ``--required``/``--informational`` on ``agents validation
add``", and numerous workflow examples throughout the spec use
``--required``.  The fix adds ``--required`` and ``--informational`` as
mutually exclusive boolean options on the ``add`` command that override
the ``mode`` field in the YAML config when specified.

These scenarios were originally tagged ``@tdd_expected_fail`` while the
bug was unfixed.  Now that the fix is in place, the tag has been removed
and the tests run normally as permanent regression guards.
"""

from __future__ import annotations

import contextlib
import os
import tempfile
from typing import Any
from unittest.mock import MagicMock, patch

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

from cleveragents.cli.commands.validation import app as validation_app

_runner = CliRunner()

_PATCH_SVC = "cleveragents.cli.commands.validation._get_tool_registry_service"

_VALID_MODES = {"required", "informational"}


def _safe_unlink(path: str) -> None:
    """Remove a file if it exists, silently ignoring missing files."""
    with contextlib.suppress(FileNotFoundError):
        os.unlink(path)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("a tdd 1038 CLI test runner with mocked services")
def step_tdd_1038_background(context: Context) -> None:
    """Set up CliRunner and a mocked ToolRegistryService.

    The mock uses ``side_effect = lambda v: v`` so that ``register_tool``
    returns the actual Validation object the CLI passes in, rather than a
    hard-coded mock.  This ensures the CLI output reflects the real
    Validation produced by the CLI's processing pipeline, not mock
    configuration.
    """
    context.tdd1038_runner = _runner
    context.tdd1038_result = None
    context.tdd1038_config_path = None

    mock_service = MagicMock()

    # Return whatever Validation object the CLI passes in, so the CLI
    # output reflects actual CLI processing rather than mock configuration.
    mock_service.register_tool.side_effect = lambda v: v
    context.tdd1038_mock_service = mock_service


@given("a tdd 1038 temporary validation YAML config")
def step_tdd_1038_temp_config(context: Context) -> None:
    """Create a temporary YAML config file with mode ``required``.

    The YAML uses a flat ``mode`` key to match what
    ``Validation.from_config()`` actually reads (``config.get("mode",
    "required")``).  Note: the specification's Validation Configuration
    schema (specification.md line 34109) nests mode under a ``validation``
    key, but the current ``from_config()`` implementation reads a flat
    top-level ``mode`` key.  This test uses the flat format so the config
    is actually effective and the false-positive guard is meaningful.
    """
    config_content = (
        "name: local/unit-tests\n"
        "description: Unit tests\n"
        "source: custom\n"
        "mode: required\n"
        "code: |\n"
        "  def run(inputs):\n"
        "      return {'passed': True}\n"
    )
    with tempfile.NamedTemporaryFile(
        mode="w",
        suffix=".yaml",
        delete=False,
    ) as tmp:
        tmp.write(config_content)
        config_path = tmp.name
    context.tdd1038_config_path = config_path
    context.add_cleanup(lambda: _safe_unlink(config_path))


@given('a tdd 1038 temporary validation YAML config with mode "{mode}"')
def step_tdd_1038_temp_config_with_mode(context: Context, mode: str) -> None:
    """Create a temporary YAML config file with a specific mode.

    The YAML uses a flat ``mode`` key to match what
    ``Validation.from_config()`` actually reads (``config.get("mode",
    "required")``).  See the docstring on ``step_tdd_1038_temp_config``
    for the rationale on flat vs nested format.
    """
    assert mode in _VALID_MODES, f"Invalid mode '{mode}': must be one of {_VALID_MODES}"
    config_content = (
        "name: local/unit-tests\n"
        "description: Unit tests\n"
        "source: custom\n"
        f"mode: {mode}\n"
        "code: |\n"
        "  def run(inputs):\n"
        "      return {'passed': True}\n"
    )
    # Clean up any previous config from the Background step.
    if context.tdd1038_config_path is not None:
        _safe_unlink(context.tdd1038_config_path)

    with tempfile.NamedTemporaryFile(
        mode="w",
        suffix=".yaml",
        delete=False,
    ) as tmp:
        tmp.write(config_content)
        config_path = tmp.name
    context.tdd1038_config_path = config_path
    context.add_cleanup(lambda: _safe_unlink(config_path))


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


@when("I tdd 1038 invoke validation add with --required flag")
def step_tdd_1038_add_required(context: Context) -> None:
    """Invoke ``agents validation add --config <file> --required``.

    Per the spec, the ``--required`` flag sets the validation mode to
    ``required``, overriding whatever mode the YAML config defines.
    """
    with patch(_PATCH_SVC, return_value=context.tdd1038_mock_service):
        context.tdd1038_result = context.tdd1038_runner.invoke(
            validation_app,
            [
                "add",
                "--config",
                context.tdd1038_config_path,
                "--required",
                "--format",
                "plain",
            ],
        )


@when("I tdd 1038 invoke validation add with --informational flag")
def step_tdd_1038_add_informational(context: Context) -> None:
    """Invoke ``agents validation add --config <file> --informational``.

    Per the spec (specification.md line 22339), ``--informational`` sets
    the mode to ``informational``.
    """
    with patch(_PATCH_SVC, return_value=context.tdd1038_mock_service):
        context.tdd1038_result = context.tdd1038_runner.invoke(
            validation_app,
            [
                "add",
                "--config",
                context.tdd1038_config_path,
                "--informational",
                "--format",
                "plain",
            ],
        )


@when("I tdd 1038 invoke validation add with both flags")
def step_tdd_1038_add_both_flags(context: Context) -> None:
    """Invoke ``agents validation add`` with both ``--required`` and
    ``--informational``.

    These flags are mutually exclusive; the CLI should reject the
    invocation and abort.
    """
    with patch(_PATCH_SVC, return_value=context.tdd1038_mock_service):
        context.tdd1038_result = context.tdd1038_runner.invoke(
            validation_app,
            [
                "add",
                "--config",
                context.tdd1038_config_path,
                "--required",
                "--informational",
                "--format",
                "plain",
            ],
        )


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


@then("the tdd 1038 CLI result should succeed")
def step_tdd_1038_result_succeed(context: Context) -> None:
    """Assert the CLI invocation completed successfully (exit code 0)."""
    result = context.tdd1038_result
    assert result is not None, "No CLI result captured"
    assert result.exit_code == 0, (
        f"Expected exit code 0 but got {result.exit_code}. Output:\n{result.output}"
    )


@then("the tdd 1038 CLI result should be aborted")
def step_tdd_1038_result_aborted(context: Context) -> None:
    """Assert the CLI invocation was aborted (non-zero exit code).

    When both ``--required`` and ``--informational`` are passed, the CLI
    should print an error message and abort.
    """
    result = context.tdd1038_result
    assert result is not None, "No CLI result captured"
    assert result.exit_code != 0, (
        f"Expected non-zero exit code but got {result.exit_code}. "
        f"Output:\n{result.output}"
    )
    assert "mutually exclusive" in result.output, (
        f"Expected 'mutually exclusive' in output, got:\n{result.output}"
    )


@then('the tdd 1038 registered validation mode should be "{expected_mode}"')
def step_tdd_1038_mode_check(context: Context, expected_mode: str) -> None:
    """Assert the output contains the expected mode and the service received it.

    This step verifies that ``register_tool`` was called with a Validation
    object whose ``mode`` attribute matches the expected value.  This
    prevents a false positive where the mock always returns a hard-coded
    mode regardless of whether the CLI actually forwarded the flag to the
    service layer.
    """
    result = context.tdd1038_result
    assert result is not None, "No CLI result captured"
    assert f"mode: {expected_mode}" in result.output, (
        f"Expected 'mode: {expected_mode}' in output, got:\n{result.output}"
    )

    # Verify the service layer received the correct mode -- guards against
    # false positives from hard-coded mock return values.
    mock_service = context.tdd1038_mock_service
    assert mock_service.register_tool.called, (
        "register_tool was never called on the mock service"
    )
    call_args: Any = mock_service.register_tool.call_args
    validation_arg = call_args[0][0]
    actual_mode = getattr(validation_arg, "mode", None)
    assert actual_mode == expected_mode, (
        f"Expected register_tool to receive mode '{expected_mode}', "
        f"but the Validation object had mode '{actual_mode}'"
    )
