"""Step definitions for TDD Issue #932 — plan apply missing --yes flag.

These steps verify that the ``apply`` CLI command accepts the
``--yes`` / ``-y`` flag as required by the specification, and that the
confirmation prompt behaves correctly:

- With ``--yes`` / ``-y``: no confirmation prompt is shown.
- Without ``--yes``: a confirmation prompt appears; declining aborts with
  ``"Apply cancelled."``, accepting proceeds with the apply.
"""

from __future__ import annotations

from datetime import UTC, datetime
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.plan import app as plan_app
from cleveragents.domain.models.core.plan import Plan as LifecyclePlan
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    ProcessingState,
)

_PATCH_LIFECYCLE = "cleveragents.cli.commands.plan._get_lifecycle_service"

# Confirmation prompt text fragment used to detect whether the prompt appeared.
_CONFIRM_FRAGMENT = "Apply changes for plan"


def _make_mock_plan(
    plan_id: str = "01JAAAAAAAAAAAAAAAAAAAAAAA",
    phase: PlanPhase = PlanPhase.EXECUTE,
    state: ProcessingState = ProcessingState.COMPLETE,
) -> LifecyclePlan:
    """Build a lightweight mock Plan for confirmation-prompt tests."""
    from cleveragents.domain.models.core.plan import (
        NamespacedName,
        PlanIdentity,
        PlanTimestamps,
        ProjectLink,
    )

    now = datetime.now(tz=UTC)
    return LifecyclePlan(
        identity=PlanIdentity(plan_id=plan_id),
        namespaced_name=NamespacedName(
            server=None, namespace="local", name="test-plan"
        ),
        action_name="local/test-action",
        description="Test plan for yes-flag scenarios",
        definition_of_done="All tests pass",
        phase=phase,
        processing_state=state,
        project_links=[ProjectLink(project_name="proj-1")],
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
        created_by="test-user",
        reusable=True,
        read_only=False,
        timestamps=PlanTimestamps(created_at=now, updated_at=now),
    )


def _build_mock_service() -> MagicMock:
    """Build a mock lifecycle service for --yes flag tests.

    Returns a ``MagicMock`` whose ``get_plan`` returns a valid plan in
    Execute/complete state and whose ``apply_plan`` returns a plan in
    Apply/queued state.
    """
    mock_service = MagicMock()
    mock_service.get_plan.return_value = _make_mock_plan()
    mock_service.apply_plan.return_value = _make_mock_plan(
        phase=PlanPhase.APPLY, state=ProcessingState.QUEUED
    )
    return mock_service


# ---------------------------------------------------------------------------
# GIVEN
# ---------------------------------------------------------------------------


@given("a plan CLI runner for the yes-flag test")
def step_plan_cli_runner(context: Context) -> None:
    """Create a Typer CliRunner for the plan sub-app."""
    context.apply_yes_runner = CliRunner()


# ---------------------------------------------------------------------------
# WHEN — flag recognition (with mocked service for proper prompt testing)
# ---------------------------------------------------------------------------


@when("I invoke apply with --yes flag and a mocked service")
def step_invoke_apply_yes_mocked(context: Context) -> None:
    """Invoke ``plan apply --yes <PLAN_ID>`` with a mocked service.

    The lifecycle service is mocked so the command completes the full
    happy path.  This allows us to verify that ``--yes`` genuinely
    suppresses the confirmation prompt (not just that the command
    crashes before reaching it).
    """
    mock_service = _build_mock_service()
    context.apply_yes_mock_service = mock_service

    with patch(_PATCH_LIFECYCLE, return_value=mock_service):
        context.apply_yes_result = context.apply_yes_runner.invoke(
            plan_app,
            ["apply", "--yes", "01JAAAAAAAAAAAAAAAAAAAAAAA"],
        )


@when("I invoke apply with -y flag and a mocked service")
def step_invoke_apply_y_mocked(context: Context) -> None:
    """Invoke ``plan apply -y <PLAN_ID>`` with a mocked service."""
    mock_service = _build_mock_service()
    context.apply_yes_mock_service = mock_service

    with patch(_PATCH_LIFECYCLE, return_value=mock_service):
        context.apply_yes_result = context.apply_yes_runner.invoke(
            plan_app,
            ["apply", "-y", "01JAAAAAAAAAAAAAAAAAAAAAAA"],
        )


# ---------------------------------------------------------------------------
# WHEN — confirmation prompt behaviour
# ---------------------------------------------------------------------------


@when("I invoke apply without --yes and decline")
def step_invoke_apply_decline(context: Context) -> None:
    """Invoke ``apply`` without ``--yes``, supply ``n`` to the prompt."""
    mock_service = _build_mock_service()
    context.apply_yes_mock_service = mock_service

    with patch(_PATCH_LIFECYCLE, return_value=mock_service):
        context.apply_yes_result = context.apply_yes_runner.invoke(
            plan_app,
            ["apply", "01JAAAAAAAAAAAAAAAAAAAAAAA"],
            input="n\n",
        )


@when("I invoke apply without --yes and accept")
def step_invoke_apply_accept(context: Context) -> None:
    """Invoke ``apply`` without ``--yes``, supply ``y`` to the prompt."""
    mock_service = _build_mock_service()
    context.apply_yes_mock_service = mock_service

    with patch(_PATCH_LIFECYCLE, return_value=mock_service):
        context.apply_yes_result = context.apply_yes_runner.invoke(
            plan_app,
            ["apply", "01JAAAAAAAAAAAAAAAAAAAAAAA"],
            input="y\n",
        )


# ---------------------------------------------------------------------------
# WHEN — unexpected error scenario
# ---------------------------------------------------------------------------


@when("I invoke apply --yes with a service that raises an unexpected error")
def step_invoke_apply_unexpected_error(context: Context) -> None:
    """Invoke ``apply --yes`` with a service whose ``apply_plan``
    raises a ``RuntimeError`` to exercise the catch-all ``except Exception``
    handler.
    """
    mock_service = _build_mock_service()
    mock_service.apply_plan.side_effect = RuntimeError("Simulated unexpected failure")
    context.apply_yes_mock_service = mock_service

    with patch(_PATCH_LIFECYCLE, return_value=mock_service):
        context.apply_yes_result = context.apply_yes_runner.invoke(
            plan_app,
            ["apply", "--yes", "01JAAAAAAAAAAAAAAAAAAAAAAA"],
        )


# ---------------------------------------------------------------------------
# THEN — flag recognition
# ---------------------------------------------------------------------------


@then("the apply --yes invocation should not report an unknown option")
def step_assert_yes_recognised(context: Context) -> None:
    """Assert the output does NOT contain the Click/Typer unknown-option error.

    When ``--yes`` is not defined on the command, Click produces::

        Error: No such option: --yes

    and returns exit code 2.  If the flag is properly defined, the
    command proceeds past option parsing (and may fail later due to
    the dummy plan ID, but that is irrelevant — we only test flag
    recognition).
    """
    output = context.apply_yes_result.output
    exit_code = context.apply_yes_result.exit_code
    # Check both the text message and the Click/Typer usage-error exit code.
    # Exit code 2 specifically means "unrecognised option" in Click.
    assert exit_code != 2 and "No such option" not in output, (
        f"The --yes flag was not recognised by apply.\n"
        f"Exit code: {exit_code}\n"
        f"Output:\n{output}"
    )


@then("the apply -y invocation should not report an unknown option")
def step_assert_y_recognised(context: Context) -> None:
    """Assert the output does NOT contain the Click/Typer unknown-option error.

    Same check as the ``--yes`` variant but for the ``-y`` short flag.
    """
    output = context.apply_yes_result.output
    exit_code = context.apply_yes_result.exit_code
    assert exit_code != 2 and "No such option" not in output, (
        f"The -y flag was not recognised by apply.\n"
        f"Exit code: {exit_code}\n"
        f"Output:\n{output}"
    )


# ---------------------------------------------------------------------------
# THEN — prompt suppression (unified for both --yes and -y)
# ---------------------------------------------------------------------------


@then("the apply {flag} output should not contain the confirmation prompt")
def step_assert_flag_no_prompt(context: Context, flag: str) -> None:
    """When ``--yes`` or ``-y`` is supplied, the confirmation prompt must not appear."""
    output = context.apply_yes_result.output
    assert _CONFIRM_FRAGMENT not in output, (
        f"The {flag} flag should suppress the confirmation prompt, "
        f"but the output contained '{_CONFIRM_FRAGMENT}'.\n"
        f"Output:\n{output}"
    )


# ---------------------------------------------------------------------------
# THEN — exit code
# ---------------------------------------------------------------------------


@then("the apply exit code should be {expected_code:d}")
def step_assert_exit_code(context: Context, expected_code: int) -> None:
    """Assert the CLI invocation exited with the expected code."""
    actual = context.apply_yes_result.exit_code
    output = context.apply_yes_result.output
    assert actual == expected_code, (
        f"Expected exit code {expected_code}, got {actual}.\nOutput:\n{output}"
    )


@then("the apply exit code should be non-zero")
def step_assert_exit_code_nonzero(context: Context) -> None:
    """Assert the CLI invocation exited with a non-zero code."""
    actual = context.apply_yes_result.exit_code
    output = context.apply_yes_result.output
    assert actual != 0, f"Expected non-zero exit code, got {actual}.\nOutput:\n{output}"


# ---------------------------------------------------------------------------
# THEN — mock service call verification
# ---------------------------------------------------------------------------


@then("the apply should have called apply")
def step_assert_apply_called(context: Context) -> None:
    """Assert that ``apply_plan`` was called on the mock service."""
    mock_service: MagicMock = context.apply_yes_mock_service
    mock_service.apply_plan.assert_called_once()


@then("the apply should not have called apply")
def step_assert_apply_not_called(context: Context) -> None:
    """Assert that ``apply_plan`` was NOT called on the mock service."""
    mock_service: MagicMock = context.apply_yes_mock_service
    mock_service.apply_plan.assert_not_called()


# ---------------------------------------------------------------------------
# THEN — confirmation prompt behaviour
# ---------------------------------------------------------------------------


@then('the apply output should contain "{text}"')
def step_assert_output_contains(context: Context, text: str) -> None:
    """Assert the CLI output contains the given text."""
    output = context.apply_yes_result.output
    assert text in output, f"Expected output to contain '{text}'.\nOutput:\n{output}"


@then('the apply output should not contain "{text}"')
def step_assert_output_not_contains(context: Context, text: str) -> None:
    """Assert the CLI output does NOT contain the given text."""
    output = context.apply_yes_result.output
    assert text not in output, (
        f"Expected output NOT to contain '{text}'.\nOutput:\n{output}"
    )


@then("the apply output should contain the confirmation prompt")
def step_assert_output_has_prompt(context: Context) -> None:
    """Assert the confirmation prompt text appeared in the output."""
    output = context.apply_yes_result.output
    assert _CONFIRM_FRAGMENT in output, (
        f"Expected output to contain the confirmation prompt "
        f"'{_CONFIRM_FRAGMENT}', but it was not found.\n"
        f"Output:\n{output}"
    )
