"""Step definitions for plan_commands_new_coverage.feature.

Covers the remaining uncovered lines in plan.py:
- Lines 1817, 1821, 1822: _get_apply_service() function body
- Lines 1845-1855: plan_diff command body (correction branch + normal path)
- Lines 1878-1888: plan_artifacts command body
"""

from __future__ import annotations

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 CleverAgentsError, PlanError

runner = CliRunner()

_PATCH_GET_APPLY = "cleveragents.cli.commands.plan._get_apply_service"
_PATCH_LIFECYCLE = "cleveragents.cli.commands.plan._get_lifecycle_service"
_PATCH_PAS_CLASS = (
    "cleveragents.application.services.plan_apply_service.PlanApplyService"
)


def _build_mock_service(
    diff_return=None,
    diff_side_effect=None,
    artifacts_return=None,
    artifacts_side_effect=None,
):
    """Build a mock PlanApplyService with configurable diff/artifacts behaviour."""
    svc = MagicMock()
    if diff_side_effect:
        svc.diff.side_effect = diff_side_effect
    else:
        svc.diff.return_value = diff_return or ""
    if artifacts_side_effect:
        svc.artifacts.side_effect = artifacts_side_effect
    else:
        svc.artifacts.return_value = artifacts_return or ""
    return svc


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

use_step_matcher("parse")


@given("new_cov no service mocks are needed")
def step_new_cov_no_mocks(context: Context) -> None:
    """The correction branch returns early before calling any service."""
    context.new_cov_mock_service = None


@given('new_cov the apply service diff returns "{output}"')
def step_new_cov_diff_returns_dq(context: Context, output: str) -> None:
    context.new_cov_mock_service = _build_mock_service(diff_return=output)


@given("new_cov the apply service diff returns '{output}'")
def step_new_cov_diff_returns_sq(context: Context, output: str) -> None:
    context.new_cov_mock_service = _build_mock_service(diff_return=output)


@given('new_cov the apply service diff raises PlanError "{msg}"')
def step_new_cov_diff_plan_error(context: Context, msg: str) -> None:
    context.new_cov_mock_service = _build_mock_service(
        diff_side_effect=PlanError(msg),
    )


@given('new_cov the apply service diff raises CleverAgentsError "{msg}"')
def step_new_cov_diff_ca_error(context: Context, msg: str) -> None:
    context.new_cov_mock_service = _build_mock_service(
        diff_side_effect=CleverAgentsError(msg),
    )


@given('new_cov the apply service artifacts returns "{output}"')
def step_new_cov_artifacts_returns_dq(context: Context, output: str) -> None:
    context.new_cov_mock_service = _build_mock_service(artifacts_return=output)


@given("new_cov the apply service artifacts returns '{output}'")
def step_new_cov_artifacts_returns_sq(context: Context, output: str) -> None:
    context.new_cov_mock_service = _build_mock_service(artifacts_return=output)


@given('new_cov the apply service artifacts raises PlanError "{msg}"')
def step_new_cov_artifacts_plan_error(context: Context, msg: str) -> None:
    context.new_cov_mock_service = _build_mock_service(
        artifacts_side_effect=PlanError(msg),
    )


@given('new_cov the apply service artifacts raises CleverAgentsError "{msg}"')
def step_new_cov_artifacts_ca_error(context: Context, msg: str) -> None:
    context.new_cov_mock_service = _build_mock_service(
        artifacts_side_effect=CleverAgentsError(msg),
    )


@given("new_cov a mocked lifecycle service for apply service creation")
def step_new_cov_mock_lifecycle(context: Context) -> None:
    context.new_cov_mock_lifecycle = MagicMock()


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

use_step_matcher("re")


@when(
    r'new_cov I run plan diff for "(?P<plan_id>[^"]+)" with correction "(?P<corr>[^"]+)"'
)
def step_new_cov_diff_with_correction(
    context: Context, plan_id: str, corr: str
) -> None:
    """Invoke ``plan diff <plan_id> --correction <corr>`` via the CLI runner.

    The correction branch returns before calling _get_apply_service, so
    we do NOT need to mock it.
    """
    context.new_cov_result = runner.invoke(
        plan_app, ["diff", plan_id, "--correction", corr]
    )


@when(r'new_cov I run plan diff for "(?P<plan_id>[^"]+)" with format "(?P<fmt>[^"]+)"')
def step_new_cov_diff_with_fmt(context: Context, plan_id: str, fmt: str) -> None:
    with patch(_PATCH_GET_APPLY, return_value=context.new_cov_mock_service):
        context.new_cov_result = runner.invoke(
            plan_app, ["diff", plan_id, "--format", fmt]
        )


@when(r'new_cov I run plan diff for "(?P<plan_id>[^"]+)"')
def step_new_cov_diff(context: Context, plan_id: str) -> None:
    with patch(_PATCH_GET_APPLY, return_value=context.new_cov_mock_service):
        context.new_cov_result = runner.invoke(plan_app, ["diff", plan_id])


@when(
    r'new_cov I run plan artifacts for "(?P<plan_id>[^"]+)" with format "(?P<fmt>[^"]+)"'
)
def step_new_cov_artifacts_with_fmt(context: Context, plan_id: str, fmt: str) -> None:
    with patch(_PATCH_GET_APPLY, return_value=context.new_cov_mock_service):
        context.new_cov_result = runner.invoke(
            plan_app, ["artifacts", plan_id, "--format", fmt]
        )


@when(r'new_cov I run plan artifacts for "(?P<plan_id>[^"]+)"')
def step_new_cov_artifacts(context: Context, plan_id: str) -> None:
    with patch(_PATCH_GET_APPLY, return_value=context.new_cov_mock_service):
        context.new_cov_result = runner.invoke(plan_app, ["artifacts", plan_id])


# Switch back to parse for the remaining non-parameterized WHEN step
use_step_matcher("parse")


@when("new_cov I call _get_apply_service directly")
def step_new_cov_call_get_apply(context: Context) -> None:
    """Call the real ``_get_apply_service``, mocking its dependencies.

    This exercises the actual function body (import, lifecycle lookup,
    ``PlanApplyService`` construction) to cover lines 1817, 1821, 1822.

    Both ``_get_lifecycle_service`` **and** ``PlanApplyService`` are
    patched.  The class is patched at two locations — the canonical
    source module and the plan module (with ``create=True``) — so the
    lazy ``from … import PlanApplyService`` inside the function always
    resolves to our mock, even under ``behave-parallel``'s
    ``fork()``-based workers.
    """
    from cleveragents.cli.commands.plan import _get_apply_service

    mock_lifecycle = MagicMock()
    mock_pas_cls = MagicMock()
    mock_pas_instance = MagicMock()
    mock_pas_cls.return_value = mock_pas_instance

    with (
        patch(_PATCH_LIFECYCLE, return_value=mock_lifecycle),
        patch(
            "cleveragents.cli.commands.plan.PlanApplyService",
            mock_pas_cls,
            create=True,
        ),
        patch(_PATCH_PAS_CLASS, mock_pas_cls),
    ):
        result = _get_apply_service()

    context.new_cov_apply_result = result
    context.new_cov_mock_lifecycle_used = mock_lifecycle
    context.new_cov_mock_pas_cls = mock_pas_cls
    context.new_cov_mock_pas_instance = mock_pas_instance


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


@then("new_cov the exit code should be {code:d}")
def step_new_cov_exit_code(context: Context, code: int) -> None:
    assert context.new_cov_result.exit_code == code, (
        f"Expected exit code {code}, got {context.new_cov_result.exit_code}. "
        f"Output: {context.new_cov_result.output}"
    )


@then("new_cov the exit code should be nonzero")
def step_new_cov_exit_nonzero(context: Context) -> None:
    assert context.new_cov_result.exit_code != 0, (
        f"Expected non-zero exit code, got {context.new_cov_result.exit_code}. "
        f"Output: {context.new_cov_result.output}"
    )


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


@then("new_cov the returned object should be a PlanApplyService instance")
def step_new_cov_result_is_pas(context: Context) -> None:
    result = context.new_cov_apply_result
    expected = context.new_cov_mock_pas_instance
    assert result is expected, (
        f"Expected _get_apply_service to return the mock PlanApplyService "
        f"instance, got {type(result).__name__}"
    )


@then("new_cov PlanApplyService was constructed with the lifecycle service")
def step_new_cov_pas_called_with_lifecycle(context: Context) -> None:
    context.new_cov_mock_pas_cls.assert_called_once_with(
        lifecycle_service=context.new_cov_mock_lifecycle_used,
    )
