"""Step definitions for tdd_plan_correct_plan_id.feature.

Captures bug #969: the ``plan correct`` CLI command declares its first
positional argument as ``decision_id``.  When the M3 acceptance test
passes a plan_id, the plan_id is used as ``target_decision_id``, which
fails because the plan ID is not a valid decision ID.  The correction
service raises an error because the targeted decision cannot be found.

This test uses the ``@tdd_expected_fail`` tag until the fix in #969 is
merged.  Once fixed, the tag will be removed and the test will run
normally as a regression guard.

All step text uses the ``tpcpid`` prefix to avoid collisions with
other step files.

Bug: https://git.cleverthis.com/cleveragents/cleveragents-core/issues/969
TDD: https://git.cleverthis.com/cleveragents/cleveragents-core/issues/979
"""

from __future__ import annotations

from unittest.mock import 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 features.mocks.tdd_plan_correct_plan_id_fixtures import (
    PATCH_CONTAINER,
    PATCH_RESOLVE_PLAN,
    PLAN_ID,
    ROOT_DECISION_ID,
    build_cli_args,
    make_correction_svc,
    make_default_decisions,
    make_mock_container,
)

runner = CliRunner()


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


@given("tpcpid a plan with a known plan_id and a root decision")
def step_tpcpid_plan_with_root(context: Context) -> None:
    """Set up mock plan with a root decision and one child."""
    decisions = make_default_decisions()
    influence_edges: dict[str, list[str]] = {}
    context.tpcpid_mock_container = make_mock_container(
        decisions,
        influence_edges,
    )
    context.tpcpid_plan_id = PLAN_ID
    context.tpcpid_root_decision_id = ROOT_DECISION_ID


@given("tpcpid a CorrectionService that succeeds when targeting the root decision")
def step_tpcpid_correction_svc(context: Context) -> None:
    """Set up mock CorrectionService for revert mode."""
    context.tpcpid_correction_svc = make_correction_svc(ROOT_DECISION_ID)


@given(
    "tpcpid a CorrectionService for append mode that succeeds when"
    " targeting the root decision"
)
def step_tpcpid_correction_svc_append(context: Context) -> None:
    """Set up mock CorrectionService for append mode."""
    context.tpcpid_correction_svc = make_correction_svc(
        ROOT_DECISION_ID,
        mode="append",
    )


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


@when("tpcpid I invoke plan correct with the plan_id as the positional argument")
def step_tpcpid_invoke_with_plan_id(context: Context) -> None:
    """Invoke ``plan correct <plan_id>`` with ``--mode revert``.

    This is the exact pattern used by the M3 acceptance test:
    ``plan correct ${plan_id} --mode revert --guidance "..." --yes --format plain``

    The plan_id is passed as the first positional argument (the
    ``decision_id`` parameter in the current code).  The bug is that
    the code treats it as a decision_id directly instead of detecting
    it is a plan_id and resolving the root decision.
    """
    args = build_cli_args(context.tpcpid_plan_id, mode="revert")

    context.tpcpid_mock_container.correction_service.return_value = (
        context.tpcpid_correction_svc
    )
    with (
        patch(
            PATCH_CONTAINER,
            return_value=context.tpcpid_mock_container,
        ),
        patch(
            PATCH_RESOLVE_PLAN,
            return_value=context.tpcpid_plan_id,
        ),
    ):
        context.tpcpid_result = runner.invoke(plan_app, args)


@when(
    "tpcpid I invoke plan correct with the plan_id as the positional"
    " argument in append mode"
)
def step_tpcpid_invoke_with_plan_id_append(context: Context) -> None:
    """Invoke ``plan correct <plan_id>`` with ``--mode append``.

    Same bug path as revert mode — ``target_decision_id`` resolution
    at ``plan.py`` ``correct_decision`` happens **before** the
    mode-specific branching, so both modes are affected by bug #969.
    """
    args = build_cli_args(context.tpcpid_plan_id, mode="append")

    context.tpcpid_mock_container.correction_service.return_value = (
        context.tpcpid_correction_svc
    )
    with (
        patch(
            PATCH_CONTAINER,
            return_value=context.tpcpid_mock_container,
        ),
        patch(
            PATCH_RESOLVE_PLAN,
            return_value=context.tpcpid_plan_id,
        ),
    ):
        context.tpcpid_result = runner.invoke(plan_app, args)


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


@then("tpcpid the command should exit successfully")
def step_tpcpid_exit_ok(context: Context) -> None:
    """Assert plan correct exits with code 0.

    NOTE: This assertion is expected to **pass** even while bug #969
    is present — MagicMock accepts any arguments and the CLI command
    runs to completion.  The ``@tdd_expected_fail`` inversion only
    triggers on the *second* assertion (target_decision_id check)
    which captures the actual bug.
    """
    result = context.tpcpid_result
    assert result.exit_code == 0, (
        f"Expected exit code 0, got {result.exit_code}. Output: {result.output}"
    )


@then(
    "tpcpid request_correction should have targeted the root decision not the plan_id"
)
def step_tpcpid_target_is_root(context: Context) -> None:
    """Assert request_correction was called with the root decision ID.

    This is the core assertion that captures bug #969:
    - Currently the code passes the plan_id as target_decision_id.
    - The fix should resolve the plan_id to its root decision and pass
      the root decision_id as target_decision_id.
    """
    svc = context.tpcpid_correction_svc
    svc.request_correction.assert_called_once()
    call_record = svc.request_correction.call_args
    kw = call_record.kwargs

    actual_target = kw.get("target_decision_id")
    assert actual_target == ROOT_DECISION_ID, (
        f"Bug #969: request_correction was called with "
        f"target_decision_id={actual_target!r} but expected "
        f"{ROOT_DECISION_ID!r}. The plan_id ({PLAN_ID!r}) was "
        f"passed as the first positional argument and the code should "
        f"have resolved it to the root decision."
    )
