"""Step definitions for TDD Issue #967 — plan execute phase processing.

These steps exercise the **CLI orchestration layer** (the ``execute_plan``
command handler in ``plan.py``) via Typer's ``CliRunner`` to verify that
the ``plan execute`` command correctly handles plans in Strategize/QUEUED
state.

Bug #967: The ``plan execute`` CLI command originally only called
``service.execute_plan(plan_id)``, which is a state transition only
(Strategize/COMPLETE → Execute/QUEUED).  It did not construct a
``PlanExecutor`` or call ``run_strategize()`` / ``run_execute()``.  When a
plan was in Strategize/QUEUED state (immediately after ``plan use``), the
command failed because ``execute_plan()`` requires Strategize/COMPLETE.

The fix added phase-aware orchestration to the CLI handler: when a plan is
in Strategize/QUEUED, it calls ``PlanExecutor.run_strategize()`` before
transitioning.  The auto-discovery filter was also updated to include
Strategize/QUEUED plans.

Scenarios 1, 2, and 4 exercise the CLI handler (the code path that was
fixed).  Since the fix is already in the codebase, the ``@tdd_expected_fail``
tag has been removed and these tests serve as permanent regression guards.

The "positive control" scenario (Scenario 3) demonstrates that the proper
service-level orchestration path (``run_strategize`` → ``execute_plan``)
works correctly.

This test was written to capture bug #967 per ticket #977.
"""

from __future__ import annotations

from datetime import 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.application.services.plan_executor import PlanExecutor
from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
    PlanNotReadyError,
)
from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import PlanError
from cleveragents.domain.models.core.plan import (
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
    ProjectLink,
)


def _make_cli_plan(
    *,
    plan_id: str = "01ARZ3NDEKTSV4RRFFQ69G5967",
    phase: PlanPhase = PlanPhase.STRATEGIZE,
    state: ProcessingState = ProcessingState.QUEUED,
) -> Plan:
    """Build a real ``Plan`` domain object for CLI tests.

    Uses the Plan domain model directly (not a MagicMock) so that the CLI
    handler's ``isinstance`` checks and attribute accesses work correctly.
    """
    return Plan(
        identity=PlanIdentity(plan_id=plan_id),
        namespaced_name=NamespacedName(namespace="local", name="tdd-967-plan"),
        action_name="local/tdd-967-action",
        description="TDD plan for bug #967",
        phase=phase,
        processing_state=state,
        project_links=[ProjectLink(project_name="proj-1")],
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
        timestamps=PlanTimestamps(created_at=datetime.now(), updated_at=datetime.now()),
    )


def _create_service_and_queued_plan() -> tuple[PlanLifecycleService, str]:
    """Create a real in-memory PlanLifecycleService and a plan in Strategize/QUEUED.

    Returns a tuple of (service, plan_id).  The plan is created via the normal
    ``create_action`` → ``use_action`` flow, which puts the plan in
    Strategize/QUEUED — the state that triggers bug #967.
    """
    settings: Settings = Settings()
    service: PlanLifecycleService = PlanLifecycleService(settings=settings)
    action = service.create_action(
        name="local/tdd-967-action",
        description="TDD action for bug #967",
        definition_of_done="Complete the test plan",
        strategy_actor="local/stub-strategize",
        execution_actor="local/stub-execute",
    )
    plan: Plan = service.use_action(action_name=str(action.namespaced_name))
    return service, plan.identity.plan_id


# ---------------------------------------------------------------------------
# CLI-level test setup (used by Scenarios 1, 2, 4)
# ---------------------------------------------------------------------------

_PLAN_ID: str = "01ARZ3NDEKTSV4RRFFQ69G5967"


@given("a CLI runner and mocked services for bug 967")
def step_cli_runner_and_mocked_services(context: Context) -> None:
    """Set up a CliRunner and mock service/executor for CLI-level testing."""
    context.runner_967 = CliRunner()
    context.mock_service_967 = MagicMock()
    context.mock_executor_967 = MagicMock()


@given("a plan in Strategize/QUEUED state for bug 967")
def step_plan_in_strategize_queued(context: Context) -> None:
    """Configure the mocked service to return a plan in Strategize/QUEUED.

    The mock ``get_plan`` returns the QUEUED plan for the initial checks,
    then returns an Execute/QUEUED plan after strategize runs (simulating
    auto_progress), and finally an Execute/COMPLETE plan after run_execute.
    """
    queued_plan: Plan = _make_cli_plan(
        plan_id=_PLAN_ID,
        phase=PlanPhase.STRATEGIZE,
        state=ProcessingState.QUEUED,
    )
    execute_queued_plan: Plan = _make_cli_plan(
        plan_id=_PLAN_ID,
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.QUEUED,
    )
    execute_complete_plan: Plan = _make_cli_plan(
        plan_id=_PLAN_ID,
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.COMPLETE,
    )

    # get_plan call sequence in the CLI execute_plan handler:
    #   1. current_plan (phase detection + read-only check — merged)
    #   2. re-fetch after run_strategize (auto_progress moved to Execute)
    #   3. re-fetch for inline execute check
    #   4. re-fetch after run_execute
    context.mock_service_967.get_plan.side_effect = [
        queued_plan,
        execute_queued_plan,
        execute_queued_plan,
        execute_complete_plan,
    ]

    context.queued_plan_967 = queued_plan
    context.execute_complete_plan_967 = execute_complete_plan


# ---------------------------------------------------------------------------
# Scenario 1: CLI execute command handles plan in Strategize/QUEUED state
# Scenario 2: CLI execute command orchestrates full lifecycle for QUEUED plan
# ---------------------------------------------------------------------------


@when("I invoke the plan execute CLI command for the QUEUED plan for bug 967")
def step_invoke_cli_execute(context: Context) -> None:
    """Invoke the ``plan execute`` CLI command via CliRunner.

    This exercises the CLI orchestration layer — the actual code path that
    was buggy in #967.  The handler should detect Strategize/QUEUED and
    call ``run_strategize`` before transitioning to Execute.
    """
    with (
        patch(
            "cleveragents.cli.commands.plan._get_lifecycle_service",
            return_value=context.mock_service_967,
        ),
        patch(
            "cleveragents.cli.commands.plan._create_sandbox_for_plan",
            return_value=(None, None),
        ),
        patch(
            "cleveragents.cli.commands.plan._get_plan_executor",
            return_value=context.mock_executor_967,
        ),
    ):
        context.cli_result_967 = context.runner_967.invoke(
            plan_app, ["execute", _PLAN_ID]
        )


@then("the CLI should succeed and the plan should reach Execute phase for bug 967")
def step_cli_succeed_execute_phase(context: Context) -> None:
    """Assert the CLI command completed without error.

    Bug #967 regression guard: if the CLI orchestration is broken, this
    assertion fails because the command would abort with an error.
    """
    result = context.cli_result_967
    assert result.exit_code == 0, (
        f"Bug #967: CLI 'plan execute' failed with exit code {result.exit_code}. "
        f"The CLI should handle Strategize/QUEUED plans by running "
        f"run_strategize() before transitioning. Output:\n{result.output}"
    )
    assert "Execute" in result.output or "execute" in result.output, (
        f"Bug #967: CLI output does not mention Execute phase. Output:\n{result.output}"
    )


@then("the executor should have run strategize for the plan for bug 967")
def step_executor_ran_strategize(context: Context) -> None:
    """Assert the CLI handler invoked run_strategize on the executor.

    Bug #967 regression guard: the CLI handler must call
    ``executor.run_strategize(plan_id)`` for plans in Strategize/QUEUED.
    """
    context.mock_executor_967.run_strategize.assert_called_once_with(_PLAN_ID)


@then("the plan should have completed execute phase processing via CLI for bug 967")
def step_plan_completed_execute_via_cli(context: Context) -> None:
    """Assert the CLI handler also ran the execute phase.

    Bug #967 regression guard: after strategize, the CLI should run
    ``executor.run_execute(plan_id)`` to complete the Execute phase.
    """
    context.mock_executor_967.run_execute.assert_called_once_with(_PLAN_ID)


# ---------------------------------------------------------------------------
# Scenario 3: Positive control — proper orchestration works
# ---------------------------------------------------------------------------


@given("a real plan executor with a plan in Strategize/QUEUED for bug 967")
def step_real_executor_queued_plan(context: Context) -> None:
    """Set up a real PlanExecutor and PlanLifecycleService with a QUEUED plan."""
    service, plan_id = _create_service_and_queued_plan()
    context.service_967 = service
    context.plan_id_967 = plan_id
    context.executor_967 = PlanExecutor(lifecycle_service=service)


@when("I run the proper orchestration of strategize then execute for bug 967")
def step_proper_orchestration(context: Context) -> None:
    """Run the correct orchestration: run_strategize → execute_plan → verify.

    This positive control demonstrates that when the caller properly
    orchestrates the lifecycle (as the fixed CLI does), a QUEUED plan
    can be taken through to Execute phase.
    """
    service: PlanLifecycleService = context.service_967
    executor: PlanExecutor = context.executor_967
    plan_id: str = context.plan_id_967
    context.orchestration_error_967 = None
    context.orchestrated_plan_967 = None

    try:
        # Step 1: Run strategize to completion (QUEUED → PROCESSING → COMPLETE)
        executor.run_strategize(plan_id)

        # Step 2: Re-fetch the plan — auto_progress in complete_strategize
        # may have already advanced the plan to Execute phase.
        plan: Plan = service.get_plan(plan_id)

        if plan.phase == PlanPhase.EXECUTE:
            # auto_progress already moved it
            context.orchestrated_plan_967 = plan
        elif (
            plan.phase == PlanPhase.STRATEGIZE
            and plan.state == ProcessingState.COMPLETE
        ):
            # Transition to Execute phase
            context.orchestrated_plan_967 = service.execute_plan(plan_id)
        else:
            context.orchestration_error_967 = PlanError(
                f"Unexpected plan state after strategize: "
                f"{plan.phase.value}/{plan.state.value}"
            )
    except (PlanError, PlanNotReadyError) as exc:
        context.orchestration_error_967 = exc


@then("the plan should be in Execute/QUEUED state via orchestration for bug 967")
def step_plan_in_execute_via_orchestration(context: Context) -> None:
    """Assert the properly orchestrated plan reached Execute phase with QUEUED state.

    This is a positive control: when the orchestration is done correctly,
    the plan should be in Execute phase.  This scenario is NOT tagged
    ``@tdd_expected_fail`` and must always pass.
    """
    if context.orchestration_error_967 is not None:
        raise AssertionError(
            f"Proper orchestration failed unexpectedly: "
            f"{context.orchestration_error_967}"
        )
    plan: Plan | None = context.orchestrated_plan_967
    assert plan is not None, "Orchestration returned no plan"
    assert plan.phase == PlanPhase.EXECUTE, (
        f"Expected Execute phase, got {plan.phase.value}"
    )
    assert plan.state == ProcessingState.QUEUED, (
        f"Expected QUEUED state, got {plan.state.value}"
    )


# ---------------------------------------------------------------------------
# Scenario 4: CLI auto-discovery finds plans in Strategize/QUEUED state
# ---------------------------------------------------------------------------


@given(
    "a single plan in Strategize/QUEUED state eligible for auto-discovery for bug 967"
)
def step_single_queued_plan_auto_discovery(context: Context) -> None:
    """Configure the mocked service for auto-discovery with a QUEUED plan.

    The CLI auto-discovery (no plan_id) calls ``service.list_plans()`` and
    filters for eligible plans.  Bug #967 had a filter that only accepted
    Strategize/COMPLETE, missing QUEUED plans.  The fix includes QUEUED.
    """
    queued_plan: Plan = _make_cli_plan(
        plan_id=_PLAN_ID,
        phase=PlanPhase.STRATEGIZE,
        state=ProcessingState.QUEUED,
    )
    execute_queued_plan: Plan = _make_cli_plan(
        plan_id=_PLAN_ID,
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.QUEUED,
    )
    execute_complete_plan: Plan = _make_cli_plan(
        plan_id=_PLAN_ID,
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.COMPLETE,
    )

    # list_plans is called twice: once for STRATEGIZE, once for EXECUTE
    context.mock_service_967.list_plans.side_effect = [
        [queued_plan],  # Strategize phase plans
        [],  # Execute phase plans (none)
    ]

    # get_plan call sequence after auto-discovery selects the plan:
    #   1. current_plan (phase detection + read-only check — merged)
    #   2. re-fetch after run_strategize
    #   3. re-fetch for inline execute check
    #   4. re-fetch after run_execute
    context.mock_service_967.get_plan.side_effect = [
        queued_plan,
        execute_queued_plan,
        execute_queued_plan,
        execute_complete_plan,
    ]


@when("I invoke the plan execute CLI command without a plan id for bug 967")
def step_invoke_cli_execute_no_plan_id(context: Context) -> None:
    """Invoke ``plan execute`` without a plan ID to trigger auto-discovery.

    Bug #967 regression guard: the CLI auto-discovery filter must include
    Strategize/QUEUED plans, not just Strategize/COMPLETE.
    """
    with (
        patch(
            "cleveragents.cli.commands.plan._get_lifecycle_service",
            return_value=context.mock_service_967,
        ),
        patch(
            "cleveragents.cli.commands.plan._create_sandbox_for_plan",
            return_value=(None, None),
        ),
        patch(
            "cleveragents.cli.commands.plan._get_plan_executor",
            return_value=context.mock_executor_967,
        ),
    ):
        context.cli_result_967 = context.runner_967.invoke(plan_app, ["execute"])


@then("the CLI should succeed and auto-discover the QUEUED plan for bug 967")
def step_cli_auto_discovery_succeeds(context: Context) -> None:
    """Assert the CLI auto-discovered and processed the Strategize/QUEUED plan.

    Bug #967 regression guard: if the auto-discovery filter excludes QUEUED
    plans, the CLI would abort with "No plans ready for execution."
    """
    result = context.cli_result_967
    assert result.exit_code == 0, (
        "Bug #967: CLI 'plan execute' (no plan_id) failed with exit code "
        f"{result.exit_code}. The auto-discovery filter should include "
        f"Strategize/QUEUED plans. Output:\n{result.output}"
    )
    # Verify the executor was actually invoked (plan was found and processed)
    context.mock_executor_967.run_strategize.assert_called_once_with(_PLAN_ID)
