"""Step definitions for plan_correction_diff.feature.

Covers:
- ``correction_diff()`` method in PlanApplyService for all 4 output formats
- Error paths: plan not found, correction not found

All step text uses the ``correction-diff`` prefix to avoid collisions.
"""

from __future__ import annotations

import json
from unittest.mock import MagicMock, patch

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

from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.core.exceptions import PlanError, ValidationError

runner = CliRunner()

_PATCH_GET_APPLY = "cleveragents.cli.commands.plan._get_apply_service"


def _build_mock_svc(
    correction_diff_return: str | None = None,
    correction_diff_side_effect: Exception | None = None,
) -> MagicMock:
    """Build a mock PlanApplyService with configurable correction_diff behaviour."""
    svc = MagicMock()
    if correction_diff_side_effect is not None:
        svc.correction_diff.side_effect = correction_diff_side_effect
    else:
        svc.correction_diff.return_value = correction_diff_return or ""
    return svc


# ---------------------------------------------------------------------------
# GIVEN steps
# ---------------------------------------------------------------------------


@given("correction-diff a mocked apply service returning rich correction diff")
def step_corr_diff_rich(context: Context) -> None:
    context.corr_diff_mock_svc = _build_mock_svc(
        correction_diff_return=(
            "[bold]Correction Diff[/bold]\n"
            "  [bold]Correction:[/bold] CORR-001\n"
            "  [bold]Original Decision:[/bold] orig-001\n"
            "  [bold]Mode:[/bold] [cyan]revert[/cyan]\n"
            "  [bold]State:[/bold] [yellow]completed[/yellow]\n"
            "  [bold]Files Changed:[/bold] 1\n"
        )
    )


@given("correction-diff a mocked apply service returning plain correction diff")
def step_corr_diff_plain(context: Context) -> None:
    context.corr_diff_mock_svc = _build_mock_svc(
        correction_diff_return=(
            "Correction Diff\n"
            "  Correction: CORR-002\n"
            "  Original Decision: orig-002\n"
            "  Mode: revert\n"
            "  State: completed\n"
            "  Files Changed: 1\n"
        )
    )


@given(
    'correction-diff a mocked apply service returning json correction diff for "{corr}"'
)
def step_corr_diff_json(context: Context, corr: str) -> None:
    data = {
        "correction_diff": {
            "correction": corr,
            "original_decision": "orig-003",
            "mode": "revert",
            "state": "completed",
            "files_changed": 1,
        },
        "comparison": {"reverted_decisions": [], "added_decisions": []},
        "patch_preview": [
            {"note": "Patch preview available after execution completes."}
        ],
    }
    context.corr_diff_mock_svc = _build_mock_svc(
        correction_diff_return=json.dumps(data, indent=2)
    )


@given(
    'correction-diff a mocked apply service returning yaml correction diff for "{corr}"'
)
def step_corr_diff_yaml(context: Context, corr: str) -> None:
    context.corr_diff_mock_svc = _build_mock_svc(
        correction_diff_return=(
            f"correction_diff:\n"
            f"  correction: {corr}\n"
            f"  original_decision: orig-004\n"
            f"  mode: revert\n"
            f"  state: completed\n"
        )
    )


@given('correction-diff the apply service correction_diff raises PlanError "{msg}"')
def step_corr_diff_plan_error(context: Context, msg: str) -> None:
    context.corr_diff_mock_svc = _build_mock_svc(
        correction_diff_side_effect=PlanError(msg)
    )


@given(
    'correction-diff the apply service correction_diff raises ValidationError "{msg}"'
)
def step_corr_diff_validation_error(context: Context, msg: str) -> None:
    context.corr_diff_mock_svc = _build_mock_svc(
        correction_diff_side_effect=ValidationError(msg)
    )


# ---------------------------------------------------------------------------
# WHEN steps
# ---------------------------------------------------------------------------

use_step_matcher("re")


@when(
    r'correction-diff I run plan diff for "(?P<plan_id>[^"]*)"'
    r' with correction "(?P<corr>[^"]*)"'
    r' format "(?P<fmt>[^"]+)"'
)
def step_corr_diff_run(context: Context, plan_id: str, corr: str, fmt: str) -> None:
    """Invoke plan diff with correction and format flags.

    Always passes ``--correction`` so that the correction_diff() code path is
    exercised.  An empty ``corr`` value tests the empty-input guard in the
    service (ValidationError for empty correction_attempt_id).
    """
    args: list[str] = ["diff", plan_id, "--correction", corr, "--format", fmt]
    with patch(_PATCH_GET_APPLY, return_value=context.corr_diff_mock_svc):
        context.corr_diff_result = runner.invoke(
            plan_app,
            args,
        )


use_step_matcher("parse")


# ---------------------------------------------------------------------------
# THEN steps
# ---------------------------------------------------------------------------


@then("correction-diff the exit code should be 0")
def step_corr_diff_exit_0(context: Context) -> None:
    assert context.corr_diff_result.exit_code == 0, (
        f"Expected exit code 0, got {context.corr_diff_result.exit_code}.\n"
        f"Output: {context.corr_diff_result.output}"
    )


@then("correction-diff the exit code should be nonzero")
def step_corr_diff_exit_nonzero(context: Context) -> None:
    assert context.corr_diff_result.exit_code != 0, (
        f"Expected non-zero exit code, got {context.corr_diff_result.exit_code}.\n"
        f"Output: {context.corr_diff_result.output}"
    )


@then('correction-diff the output should contain "{text}"')
def step_corr_diff_output_contains(context: Context, text: str) -> None:
    output = context.corr_diff_result.output
    assert text in output, f"Expected '{text}' in output. Got:\n{output}"


@then('correction-diff the service was called with format "{fmt}"')
def step_corr_diff_service_called_with_fmt(context: Context, fmt: str) -> None:
    """Assert correction_diff() was invoked with the expected fmt argument."""
    svc = context.corr_diff_mock_svc
    svc.correction_diff.assert_called_once()
    _args, kwargs = svc.correction_diff.call_args
    actual_fmt = kwargs.get("fmt", _args[2] if len(_args) > 2 else None)
    assert actual_fmt == fmt, (
        f"Expected correction_diff called with fmt={fmt!r}, got fmt={actual_fmt!r}"
    )
