"""Step definitions for CLI lifecycle command coverage feature.

All step names are prefixed with ``lifecycle coverage`` to avoid
collisions with existing steps (Behave loads all steps globally).
"""

from __future__ import annotations

import contextlib
import os
import tempfile
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_lifecycle_service import (
    ActionNotAvailableError,
    InvalidPhaseTransitionError,
    PlanNotReadyError,
)
from cleveragents.cli.commands.action import app as action_app
from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.core.exceptions import (
    CleverAgentsError,
    NotFoundError,
    PlanError,
    ValidationError,
)
from cleveragents.domain.models.core.action import (
    Action,
    ActionState,
)
from cleveragents.domain.models.core.plan import (
    AutomationProfileProvenance,
    AutomationProfileRef,
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
    ProjectLink,
)

_PLAN_ULID = "01KHDE6WWS2171PWW3GJEBXZ8S"

_VALID_YAML = """\
name: local/lc-action
description: Lifecycle coverage action
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: All lifecycle tests pass
"""

_INVALID_SCHEMA_YAML = """\
name: 123-bad
"""

_INVALID_VALUE_YAML = """\
description: missing name
strategy_actor: openai/gpt-4
execution_actor: openai/gpt-4
definition_of_done: passes
"""


def _register_cleanup(context: Context, func) -> None:
    """Register cleanup callback with Behave scenario context."""
    if hasattr(context, "add_cleanup"):
        context.add_cleanup(func)
        return
    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(func)


def _safe_stop_patcher(patcher: object) -> None:
    """Best-effort patcher stop used in scenario cleanup."""
    stop = getattr(patcher, "stop", None)
    if not callable(stop):
        return
    with contextlib.suppress(RuntimeError):
        stop()


def _make_lc_action(
    name: str = "local/lc-action",
    state: ActionState = ActionState.AVAILABLE,
) -> Action:
    """Create a mock Action for lifecycle coverage tests."""
    return Action(
        namespaced_name=NamespacedName.parse(name),
        description="Lifecycle coverage action",
        long_description="A lifecycle coverage action",
        definition_of_done="All lifecycle tests pass",
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
        reusable=True,
        read_only=False,
        state=state,
        created_at=datetime.now(),
        updated_at=datetime.now(),
        created_by=None,
    )


def _make_lc_plan(
    name: str = "local/lc-plan",
    phase: PlanPhase = PlanPhase.STRATEGIZE,
    state: ProcessingState = ProcessingState.QUEUED,
    project_links: list[ProjectLink] | None = None,
    error_message: str | None = None,
    automation_profile: AutomationProfileRef | None = None,
    strategy_actor: str | None = "openai/gpt-4",
    execution_actor: str | None = "openai/gpt-4",
    plan_id: str = _PLAN_ULID,
) -> Plan:
    """Create a mock Plan for lifecycle coverage tests."""
    now = datetime.now()
    return Plan(
        identity=PlanIdentity(plan_id=plan_id),
        namespaced_name=NamespacedName.parse(name),
        description="Lifecycle coverage plan",
        definition_of_done="Tests pass",
        action_name="local/lc-action",
        phase=phase,
        processing_state=state,
        project_links=project_links or [],
        arguments={"target_coverage": 80},
        arguments_order=["target_coverage"],
        automation_profile=automation_profile,
        strategy_actor=strategy_actor,
        execution_actor=execution_actor,
        reusable=True,
        read_only=False,
        created_by=None,
        timestamps=PlanTimestamps(created_at=now, updated_at=now),
        error_message=error_message,
    )


def _write_temp_yaml(context: Context, content: str) -> str:
    """Write YAML content to a temporary file."""
    fd, path = tempfile.mkstemp(suffix=".yaml")
    with os.fdopen(fd, "w") as fh:
        fh.write(content)
    _register_cleanup(
        context,
        lambda p=path: os.unlink(p) if os.path.exists(p) else None,
    )
    return path


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("a lifecycle coverage CLI runner")
def step_lc_cli_runner(context: Context) -> None:
    """Set up the CLI runner for lifecycle coverage tests."""
    context.lc_runner = CliRunner()


@given("a lifecycle coverage mocked lifecycle service")
def step_lc_mocked_service(context: Context) -> None:
    """Set up a mock PlanLifecycleService for both action and plan apps."""
    context.lc_mock = MagicMock()
    context.lc_action_patcher = patch(
        "cleveragents.cli.commands.action._get_lifecycle_service",
        return_value=context.lc_mock,
    )
    context.lc_plan_patcher = patch(
        "cleveragents.cli.commands.plan._get_lifecycle_service",
        return_value=context.lc_mock,
    )
    context.lc_executor_patcher = patch(
        "cleveragents.cli.commands.plan._get_plan_executor",
        return_value=MagicMock(),
    )
    context.lc_action_patcher.start()
    context.lc_plan_patcher.start()
    context.lc_executor_patcher.start()
    _register_cleanup(context, lambda: _safe_stop_patcher(context.lc_action_patcher))
    _register_cleanup(context, lambda: _safe_stop_patcher(context.lc_plan_patcher))
    _register_cleanup(context, lambda: _safe_stop_patcher(context.lc_executor_patcher))


# ---------------------------------------------------------------------------
# Action create
# ---------------------------------------------------------------------------


@given("a lifecycle coverage valid action config file")
def step_lc_valid_config(context: Context) -> None:
    context.lc_config_path = _write_temp_yaml(context, _VALID_YAML)


@given("a lifecycle coverage invalid schema config file")
def step_lc_invalid_schema_config(context: Context) -> None:
    context.lc_bad_schema_path = _write_temp_yaml(context, _INVALID_SCHEMA_YAML)


@given("a lifecycle coverage invalid value config file")
def step_lc_invalid_value_config(context: Context) -> None:
    context.lc_bad_value_path = _write_temp_yaml(context, _INVALID_VALUE_YAML)


@given("a lifecycle coverage empty config file")
def step_lc_empty_config(context: Context) -> None:
    context.lc_empty_config_path = _write_temp_yaml(context, "")


@when("I run lifecycle coverage action create with config")
def step_lc_action_create(context: Context) -> None:
    created = _make_lc_action()
    context.lc_mock.create_action.return_value = created
    context.lc_result = context.lc_runner.invoke(
        action_app, ["create", "--config", context.lc_config_path]
    )


@when("I run lifecycle coverage action create with missing config")
def step_lc_action_create_missing(context: Context) -> None:
    context.lc_result = context.lc_runner.invoke(
        action_app, ["create", "--config", "/tmp/nonexistent_lc_config.yaml"]
    )


@when("I run lifecycle coverage action create with invalid schema config")
def step_lc_action_create_invalid_schema(context: Context) -> None:
    context.lc_result = context.lc_runner.invoke(
        action_app, ["create", "--config", context.lc_bad_schema_path]
    )


@when("I run lifecycle coverage action create with value error config")
def step_lc_action_create_value_error(context: Context) -> None:
    context.lc_result = context.lc_runner.invoke(
        action_app, ["create", "--config", context.lc_bad_value_path]
    )


@when("I run lifecycle coverage action create with empty config")
def step_lc_action_create_empty(context: Context) -> None:
    context.lc_result = context.lc_runner.invoke(
        action_app, ["create", "--config", context.lc_empty_config_path]
    )


@then("lifecycle coverage action create should succeed")
def step_lc_action_create_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"
    context.lc_mock.create_action.assert_called_once()


@then('lifecycle coverage created action name should be "{name}"')
def step_lc_created_action_name(context: Context, name: str) -> None:
    call_kwargs = context.lc_mock.create_action.call_args[1]
    assert call_kwargs["name"] == name


@then("lifecycle coverage action CLI should abort")
def step_lc_action_abort(context: Context) -> None:
    assert context.lc_result.exit_code != 0


# ---------------------------------------------------------------------------
# Action list
# ---------------------------------------------------------------------------


@given("lifecycle coverage mocked actions exist")
def step_lc_mocked_actions(context: Context) -> None:
    context.lc_actions = [
        _make_lc_action(name="local/alpha-action"),
        _make_lc_action(name="local/beta-action"),
        _make_lc_action(name="myorg/gamma-action"),
    ]
    context.lc_mock.list_actions.return_value = context.lc_actions


@when("I run lifecycle coverage action list")
def step_lc_action_list(context: Context) -> None:
    context.lc_result = context.lc_runner.invoke(action_app, ["list"])


@when('I run lifecycle coverage action list with format "{fmt}"')
def step_lc_action_list_fmt(context: Context, fmt: str) -> None:
    context.lc_result = context.lc_runner.invoke(action_app, ["list", "--format", fmt])


@when('I run lifecycle coverage action list with regex "{pattern}"')
def step_lc_action_list_regex(context: Context, pattern: str) -> None:
    context.lc_result = context.lc_runner.invoke(action_app, ["list", pattern])


@when('I run lifecycle coverage action list with invalid regex "{pattern}"')
def step_lc_action_list_invalid_regex(context: Context, pattern: str) -> None:
    context.lc_result = context.lc_runner.invoke(action_app, ["list", pattern])


@then("lifecycle coverage action list should show table with 3 actions")
def step_lc_action_list_table(context: Context) -> None:
    assert context.lc_result.exit_code == 0
    assert "(3 total)" in context.lc_result.output


@then("lifecycle coverage action list should succeed")
def step_lc_action_list_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0


@then("lifecycle coverage action list filtered should succeed")
def step_lc_action_list_filtered_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0


# ---------------------------------------------------------------------------
# Action show
# ---------------------------------------------------------------------------


@given('lifecycle coverage a specific action "{name}" exists')
def step_lc_specific_action(context: Context, name: str) -> None:
    action = _make_lc_action(name=name)
    context.lc_mock.get_action_by_name.return_value = action
    context.lc_existing_action = action


@given("lifecycle coverage action not found for show")
def step_lc_action_not_found_show(context: Context) -> None:
    context.lc_mock.get_action_by_name.side_effect = NotFoundError(
        resource_type="action", resource_id="local/no-such-action"
    )


@given("lifecycle coverage action not found for archive")
def step_lc_action_not_found_archive(context: Context) -> None:
    context.lc_mock.get_action_by_name.side_effect = NotFoundError(
        resource_type="action", resource_id="local/no-such-action"
    )


@when('I run lifecycle coverage action show "{name}" with format "{fmt}"')
def step_lc_action_show_fmt(context: Context, name: str, fmt: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        action_app, ["show", name, "--format", fmt]
    )


@when('I run lifecycle coverage action show "{name}" in default format')
def step_lc_action_show(context: Context, name: str) -> None:
    context.lc_result = context.lc_runner.invoke(action_app, ["show", name])


@then("lifecycle coverage action show should display details")
def step_lc_action_show_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0
    assert "Action" in context.lc_result.output


@then("lifecycle coverage action show should succeed")
def step_lc_action_show_succeed(context: Context) -> None:
    assert context.lc_result.exit_code == 0


@then("lifecycle coverage action not found should abort")
def step_lc_action_not_found_abort(context: Context) -> None:
    assert context.lc_result.exit_code != 0
    assert "not found" in context.lc_result.output.lower()


# ---------------------------------------------------------------------------
# Action archive
# ---------------------------------------------------------------------------


@given("lifecycle coverage an archivable action exists")
def step_lc_archivable_action(context: Context) -> None:
    action = _make_lc_action()
    context.lc_mock.get_action_by_name.return_value = action
    archived = _make_lc_action(state=ActionState.ARCHIVED)
    context.lc_mock.archive_action.return_value = archived


@when('I run lifecycle coverage action archive "{name}" using format "{fmt}"')
def step_lc_action_archive_fmt(context: Context, name: str, fmt: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        action_app, ["archive", name, "--format", fmt]
    )


@when('I run lifecycle coverage action archive "{name}" in default format')
def step_lc_action_archive(context: Context, name: str) -> None:
    context.lc_result = context.lc_runner.invoke(action_app, ["archive", name])


@then("lifecycle coverage action archive should succeed")
def step_lc_action_archive_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0
    context.lc_mock.archive_action.assert_called_once()


@then("lifecycle coverage action archive json should succeed")
def step_lc_action_archive_json_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0


# ---------------------------------------------------------------------------
# Plan use
# ---------------------------------------------------------------------------


@given("lifecycle coverage an action for plan use exists")
def step_lc_action_for_plan_use(context: Context) -> None:
    action = _make_lc_action()
    context.lc_mock.get_action_by_name.return_value = action
    context.lc_plan = _make_lc_plan(
        project_links=[ProjectLink(project_name="proj-a")],
        automation_profile=AutomationProfileRef(
            profile_name="trusted",
            provenance=AutomationProfileProvenance.PLAN,
        ),
    )
    context.lc_mock.use_action.return_value = context.lc_plan


@given("lifecycle coverage an action not available for plan use")
def step_lc_action_not_available(context: Context) -> None:
    context.lc_mock.get_action_by_name.side_effect = None
    context.lc_mock.get_action_by_name.return_value = _make_lc_action()
    context.lc_mock.use_action.side_effect = ActionNotAvailableError(
        "local/archived-action", ActionState.ARCHIVED
    )


@given("lifecycle coverage an action with validation error for plan use")
def step_lc_action_validation_error(context: Context) -> None:
    context.lc_mock.get_action_by_name.side_effect = None
    context.lc_mock.get_action_by_name.return_value = _make_lc_action()
    context.lc_mock.use_action.side_effect = ValidationError("Missing required args")


@given("lifecycle coverage an action with general error for plan use")
def step_lc_action_general_error(context: Context) -> None:
    context.lc_mock.get_action_by_name.side_effect = None
    context.lc_mock.get_action_by_name.return_value = _make_lc_action()
    context.lc_mock.use_action.side_effect = CleverAgentsError("Something broke")


@given("lifecycle coverage unknown action for plan use")
def step_lc_unknown_action(context: Context) -> None:
    context.lc_mock.get_action_by_name.side_effect = NotFoundError(
        resource_type="action", resource_id="local/does-not-exist"
    )


@when('I run lifecycle coverage plan use "{action}" with projects "{p1}" "{p2}" "{p3}"')
def step_lc_plan_use_multi(
    context: Context, action: str, p1: str, p2: str, p3: str
) -> None:
    plan = _make_lc_plan(
        project_links=[
            ProjectLink(project_name=p1),
            ProjectLink(project_name=p2),
            ProjectLink(project_name=p3),
        ]
    )
    context.lc_mock.use_action.return_value = plan
    context.lc_plan = plan
    context.lc_result = context.lc_runner.invoke(plan_app, ["use", action, p1, p2, p3])


@when(
    'I run lifecycle coverage plan use "{action}" on project "{project}" with format "{fmt}"'
)
def step_lc_plan_use_fmt(context: Context, action: str, project: str, fmt: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", action, project, "--format", fmt]
    )


@when('I run lifecycle coverage plan use "{action}" targeting project "{project}"')
def step_lc_plan_use(context: Context, action: str, project: str) -> None:
    context.lc_result = context.lc_runner.invoke(plan_app, ["use", action, project])


@when('I run lifecycle coverage plan use with automation profile "{profile}"')
def step_lc_plan_use_profile(context: Context, profile: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", "local/lc-action", "proj-a", "--automation-profile", profile]
    )


@when('I run lifecycle coverage plan use with invariants "{inv1}" and "{inv2}"')
def step_lc_plan_use_invariants(context: Context, inv1: str, inv2: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app,
        [
            "use",
            "local/lc-action",
            "proj-a",
            "--invariant",
            inv1,
            "--invariant",
            inv2,
        ],
    )


@when('I run lifecycle coverage plan use with strategy actor "{actor}"')
def step_lc_plan_use_strategy_actor(context: Context, actor: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", "local/lc-action", "proj-a", "--strategy-actor", actor]
    )


@when('I run lifecycle coverage plan use with execution actor "{actor}"')
def step_lc_plan_use_exec_actor(context: Context, actor: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", "local/lc-action", "proj-a", "--execution-actor", actor]
    )


@when('I run lifecycle coverage plan use with estimation actor "{actor}"')
def step_lc_plan_use_estimation_actor(context: Context, actor: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", "local/lc-action", "proj-a", "--estimation-actor", actor]
    )


@when('I run lifecycle coverage plan use with invariant actor "{actor}"')
def step_lc_plan_use_invariant_actor(context: Context, actor: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", "local/lc-action", "proj-a", "--invariant-actor", actor]
    )


@when('I run lifecycle coverage plan use with arg "{arg_str}"')
def step_lc_plan_use_arg(context: Context, arg_str: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", "local/lc-action", "proj-a", "--arg", arg_str]
    )


@when('I run lifecycle coverage plan use with automation level "{level}"')
def step_lc_plan_use_auto_level(context: Context, level: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", "local/lc-action", "proj-a", "--automation-level", level]
    )


@when('I run lifecycle coverage plan use with invalid automation level "{level}"')
def step_lc_plan_use_invalid_auto(context: Context, level: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", "local/lc-action", "proj-a", "--automation-level", level]
    )


@when('I run lifecycle coverage plan use with invalid arg "{arg_str}"')
def step_lc_plan_use_invalid_arg(context: Context, arg_str: str) -> None:
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["use", "local/lc-action", "proj-a", "--arg", arg_str]
    )


@then("lifecycle coverage plan use should succeed")
def step_lc_plan_use_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"


@then("lifecycle coverage plan should be in strategize phase")
def step_lc_plan_strategize(context: Context) -> None:
    assert context.lc_result.exit_code == 0


@then("lifecycle coverage plan should link 3 projects")
def step_lc_plan_3_projects(context: Context) -> None:
    assert context.lc_result.exit_code == 0
    assert len(context.lc_plan.project_links) == 3


@then('lifecycle coverage plan automation profile should be "{profile}"')
def step_lc_plan_auto_profile(context: Context, profile: str) -> None:
    # The profile is applied post-creation in the CLI
    assert context.lc_result.exit_code == 0


@then("lifecycle coverage plan should have 2 invariants")
def step_lc_plan_invariants(context: Context) -> None:
    assert context.lc_result.exit_code == 0
    # Verify invariants were passed to use_action
    call_kwargs = context.lc_mock.use_action.call_args[1]
    invariants = call_kwargs.get("invariants", [])
    assert len(invariants) == 2


@then('lifecycle coverage plan strategy actor should be "{actor}"')
def step_lc_plan_strategy_actor(context: Context, actor: str) -> None:
    assert context.lc_result.exit_code == 0


@then('lifecycle coverage plan execution actor should be "{actor}"')
def step_lc_plan_exec_actor(context: Context, actor: str) -> None:
    assert context.lc_result.exit_code == 0


@then("lifecycle coverage plan use should abort")
def step_lc_plan_use_abort(context: Context) -> None:
    assert context.lc_result.exit_code != 0


# ---------------------------------------------------------------------------
# Plan list
# ---------------------------------------------------------------------------


@given("lifecycle coverage plans exist for listing")
def step_lc_plans_exist(context: Context) -> None:
    context.lc_plans = [
        _make_lc_plan(name="local/plan-one", plan_id="01KHDE6WWS2171PWW3GJEBXZ8A"),
        _make_lc_plan(
            name="local/plan-two",
            plan_id="01KHDE6WWS2171PWW3GJEBXZ8B",
            phase=PlanPhase.EXECUTE,
            state=ProcessingState.PROCESSING,
        ),
    ]
    context.lc_mock.list_plans.return_value = context.lc_plans


@given("lifecycle coverage no plans exist for listing")
def step_lc_no_plans(context: Context) -> None:
    context.lc_mock.list_plans.return_value = []


@when("I run lifecycle coverage plan list")
def step_lc_plan_list(context: Context) -> None:
    context.lc_result = context.lc_runner.invoke(plan_app, ["list"])


@when('I run lifecycle coverage plan list with format "{fmt}"')
def step_lc_plan_list_fmt(context: Context, fmt: str) -> None:
    context.lc_result = context.lc_runner.invoke(plan_app, ["list", "--format", fmt])


@when('I run lifecycle coverage plan list with invalid phase "{phase}"')
def step_lc_plan_list_invalid_phase(context: Context, phase: str) -> None:
    context.lc_result = context.lc_runner.invoke(plan_app, ["list", "--phase", phase])


@when('I run lifecycle coverage plan list with invalid state "{state}"')
def step_lc_plan_list_invalid_state(context: Context, state: str) -> None:
    context.lc_result = context.lc_runner.invoke(plan_app, ["list", "--state", state])


@then("lifecycle coverage plan list should show table")
def step_lc_plan_list_table(context: Context) -> None:
    assert context.lc_result.exit_code == 0
    assert "Plans" in context.lc_result.output


@then("lifecycle coverage plan list should succeed")
def step_lc_plan_list_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0


@then("lifecycle coverage plan list should show no plans message")
def step_lc_plan_list_empty(context: Context) -> None:
    assert context.lc_result.exit_code == 0
    assert "No plans found" in context.lc_result.output


@then("lifecycle coverage plan list should abort")
def step_lc_plan_list_abort(context: Context) -> None:
    assert context.lc_result.exit_code != 0


# ---------------------------------------------------------------------------
# Plan status
# ---------------------------------------------------------------------------


@given("lifecycle coverage a specific plan exists for status")
def step_lc_specific_plan_status(context: Context) -> None:
    plan = _make_lc_plan(
        project_links=[ProjectLink(project_name="proj-a", alias="api")],
        automation_profile=AutomationProfileRef(
            profile_name="trusted",
            provenance=AutomationProfileProvenance.PLAN,
        ),
    )
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a plan in strategize phase exists")
def step_lc_plan_strategize_phase(context: Context) -> None:
    plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.PROCESSING)
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a plan in execute phase exists")
def step_lc_plan_execute_phase(context: Context) -> None:
    plan = _make_lc_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.PROCESSING)
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a plan in apply phase exists")
def step_lc_plan_apply_phase(context: Context) -> None:
    plan = _make_lc_plan(phase=PlanPhase.APPLY, state=ProcessingState.PROCESSING)
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a plan with applied outcome exists")
def step_lc_plan_applied(context: Context) -> None:
    plan = _make_lc_plan(phase=PlanPhase.APPLY, state=ProcessingState.APPLIED)
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a plan with constrained outcome exists")
def step_lc_plan_constrained(context: Context) -> None:
    plan = _make_lc_plan(
        phase=PlanPhase.APPLY,
        state=ProcessingState.CONSTRAINED,
        error_message="Cannot proceed within constraints",
    )
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a plan with errored outcome exists")
def step_lc_plan_errored(context: Context) -> None:
    plan = _make_lc_plan(
        phase=PlanPhase.APPLY,
        state=ProcessingState.ERRORED,
        error_message="Apply failed",
    )
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a plan with cancelled outcome exists")
def step_lc_plan_cancelled(context: Context) -> None:
    plan = _make_lc_plan(
        phase=PlanPhase.STRATEGIZE,
        state=ProcessingState.CANCELLED,
        error_message="User cancelled",
    )
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage plan status service error")
def step_lc_plan_status_error(context: Context) -> None:
    context.lc_mock.get_plan.side_effect = CleverAgentsError("Status fetch failed")
    context.lc_plan = _make_lc_plan()


@when("I run lifecycle coverage plan status with plan ID")
def step_lc_plan_status_by_id(context: Context) -> None:
    plan_id = getattr(context, "lc_plan", None)
    pid = plan_id.identity.plan_id if plan_id else _PLAN_ULID
    context.lc_result = context.lc_runner.invoke(plan_app, ["status", pid])


@when("I run lifecycle coverage plan status without ID")
def step_lc_plan_status_no_id(context: Context) -> None:
    context.lc_result = context.lc_runner.invoke(plan_app, ["status"])


@when('I run lifecycle coverage plan status with plan ID and format "{fmt}"')
def step_lc_plan_status_fmt(context: Context, fmt: str) -> None:
    plan_id = context.lc_plan.identity.plan_id
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["status", plan_id, "--format", fmt]
    )


@then("lifecycle coverage plan status should show details")
def step_lc_plan_status_details(context: Context) -> None:
    assert context.lc_result.exit_code == 0
    assert "Plan" in context.lc_result.output


@then("lifecycle coverage plan status should show table")
def step_lc_plan_status_table(context: Context) -> None:
    assert context.lc_result.exit_code == 0


@then("lifecycle coverage plan status should show no plans")
def step_lc_plan_status_no_plans(context: Context) -> None:
    assert context.lc_result.exit_code == 0
    assert "No v3 lifecycle plans found" in context.lc_result.output


@then("lifecycle coverage plan status should succeed")
def step_lc_plan_status_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0


@then("lifecycle coverage plan status should abort")
def step_lc_plan_status_abort(context: Context) -> None:
    assert context.lc_result.exit_code != 0


@then('lifecycle coverage plan status should show phase "{phase}"')
def step_lc_plan_status_phase(context: Context, phase: str) -> None:
    assert context.lc_result.exit_code == 0
    output_lower = context.lc_result.output.lower()
    assert phase.lower() in output_lower


@then('lifecycle coverage plan status should show state "{state}"')
def step_lc_plan_status_state(context: Context, state: str) -> None:
    assert context.lc_result.exit_code == 0
    output_lower = context.lc_result.output.lower()
    assert state.lower() in output_lower


@then("lifecycle coverage plan should be terminal")
def step_lc_plan_terminal(context: Context) -> None:
    assert context.lc_plan.is_terminal


# ---------------------------------------------------------------------------
# Plan execute
# ---------------------------------------------------------------------------


@given("lifecycle coverage a plan ready for execute exists")
def step_lc_plan_ready_execute(context: Context) -> None:
    plan = _make_lc_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED)
    context.lc_mock.execute_plan.return_value = plan
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a single plan ready for auto-execute exists")
def step_lc_single_execute(context: Context) -> None:
    plan = _make_lc_plan(
        phase=PlanPhase.STRATEGIZE,
        state=ProcessingState.COMPLETE,
    )
    context.lc_mock.list_plans.return_value = [plan]
    context.lc_mock.get_plan.return_value = plan
    executed = _make_lc_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED)
    context.lc_mock.execute_plan.return_value = executed
    context.lc_plan = plan


@given("lifecycle coverage no plans ready for execute")
def step_lc_no_execute(context: Context) -> None:
    context.lc_mock.list_plans.return_value = []


@given("lifecycle coverage multiple plans ready for execute")
def step_lc_multi_execute(context: Context) -> None:
    plans = [
        _make_lc_plan(
            name="local/plan-x",
            plan_id="01KHDE6WWS2171PWW3GJEBXZ8C",
            phase=PlanPhase.STRATEGIZE,
            state=ProcessingState.COMPLETE,
        ),
        _make_lc_plan(
            name="local/plan-y",
            plan_id="01KHDE6WWS2171PWW3GJEBXZ8D",
            phase=PlanPhase.STRATEGIZE,
            state=ProcessingState.COMPLETE,
        ),
    ]
    context.lc_mock.list_plans.return_value = plans


@given("lifecycle coverage a plan with invalid transition for execute")
def step_lc_invalid_transition(context: Context) -> None:
    plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE)
    context.lc_mock.execute_plan.side_effect = InvalidPhaseTransitionError(
        PlanPhase.APPLY, PlanPhase.EXECUTE
    )
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a plan not ready for execute")
def step_lc_plan_not_ready(context: Context) -> None:
    plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE)
    context.lc_mock.execute_plan.side_effect = PlanNotReadyError(
        _PLAN_ULID, PlanPhase.STRATEGIZE, ProcessingState.QUEUED
    )
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a plan execute with general error")
def step_lc_plan_exec_general_error(context: Context) -> None:
    plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE)
    context.lc_mock.execute_plan.side_effect = CleverAgentsError("Execute failed")
    context.lc_mock.get_plan.return_value = plan
    context.lc_plan = plan


@when("I run lifecycle coverage plan execute with plan ID")
def step_lc_plan_execute(context: Context) -> None:
    pid = getattr(context, "lc_plan", None)
    plan_id = pid.identity.plan_id if pid else _PLAN_ULID
    context.lc_result = context.lc_runner.invoke(plan_app, ["execute", plan_id])


@when("I run lifecycle coverage plan execute without ID")
def step_lc_plan_execute_no_id(context: Context) -> None:
    context.lc_result = context.lc_runner.invoke(plan_app, ["execute"])


@when('I run lifecycle coverage plan execute with plan ID and format "{fmt}"')
def step_lc_plan_execute_fmt(context: Context, fmt: str) -> None:
    pid = context.lc_plan.identity.plan_id
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["execute", pid, "--format", fmt]
    )


@then("lifecycle coverage plan execute should succeed")
def step_lc_plan_execute_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"


@then("lifecycle coverage plan execute should abort")
def step_lc_plan_execute_abort(context: Context) -> None:
    assert context.lc_result.exit_code != 0


# ---------------------------------------------------------------------------
# Plan apply
# ---------------------------------------------------------------------------


@given("lifecycle coverage a plan ready for apply exists")
def step_lc_plan_ready_apply(context: Context) -> None:
    plan = _make_lc_plan(phase=PlanPhase.APPLY, state=ProcessingState.QUEUED)
    context.lc_mock.apply_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a single plan ready for auto-apply exists")
def step_lc_single_apply(context: Context) -> None:
    plan = _make_lc_plan(
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.COMPLETE,
    )
    context.lc_mock.list_plans.return_value = [plan]
    applied = _make_lc_plan(phase=PlanPhase.APPLY, state=ProcessingState.QUEUED)
    context.lc_mock.apply_plan.return_value = applied
    context.lc_plan = plan


@given("lifecycle coverage no plans ready for apply")
def step_lc_no_apply(context: Context) -> None:
    context.lc_mock.list_plans.return_value = []


@given("lifecycle coverage multiple plans ready for apply")
def step_lc_multi_apply(context: Context) -> None:
    plans = [
        _make_lc_plan(
            name="local/plan-m",
            plan_id="01KHDE6WWS2171PWW3GJEBXZ8E",
            phase=PlanPhase.EXECUTE,
            state=ProcessingState.COMPLETE,
        ),
        _make_lc_plan(
            name="local/plan-n",
            plan_id="01KHDE6WWS2171PWW3GJEBXZ8F",
            phase=PlanPhase.EXECUTE,
            state=ProcessingState.COMPLETE,
        ),
    ]
    context.lc_mock.list_plans.return_value = plans


@given("lifecycle coverage a plan apply with general error")
def step_lc_plan_apply_general_error(context: Context) -> None:
    context.lc_mock.apply_plan.side_effect = CleverAgentsError("Apply failed")
    context.lc_plan = _make_lc_plan()


@when("I run lifecycle coverage plan apply with plan ID")
def step_lc_plan_apply(context: Context) -> None:
    pid = getattr(context, "lc_plan", None)
    plan_id = pid.identity.plan_id if pid else _PLAN_ULID
    context.lc_result = context.lc_runner.invoke(plan_app, ["apply", "--yes", plan_id])


@when("I run lifecycle coverage plan apply without ID")
def step_lc_plan_apply_no_id(context: Context) -> None:
    context.lc_result = context.lc_runner.invoke(plan_app, ["apply", "--yes"])


@when('I run lifecycle coverage plan apply with plan ID and format "{fmt}"')
def step_lc_plan_apply_fmt(context: Context, fmt: str) -> None:
    pid = context.lc_plan.identity.plan_id
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["apply", "--yes", pid, "--format", fmt]
    )


@then("lifecycle coverage plan apply should succeed")
def step_lc_plan_apply_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"


@then("lifecycle coverage plan apply should abort")
def step_lc_plan_apply_abort(context: Context) -> None:
    assert context.lc_result.exit_code != 0


# ---------------------------------------------------------------------------
# Plan cancel
# ---------------------------------------------------------------------------


@given("lifecycle coverage a cancellable plan exists")
def step_lc_cancellable_plan(context: Context) -> None:
    plan = _make_lc_plan(
        phase=PlanPhase.STRATEGIZE,
        state=ProcessingState.CANCELLED,
    )
    context.lc_mock.cancel_plan.return_value = plan
    context.lc_plan = plan


@given("lifecycle coverage a terminal plan exists for cancel")
def step_lc_terminal_cancel(context: Context) -> None:
    context.lc_mock.cancel_plan.side_effect = PlanError(
        "Plan is already in terminal state"
    )
    context.lc_plan = _make_lc_plan()


@given("lifecycle coverage a plan cancel with general error")
def step_lc_cancel_general_error(context: Context) -> None:
    context.lc_mock.cancel_plan.side_effect = CleverAgentsError("Cancel failed")
    context.lc_plan = _make_lc_plan()


@when('I run lifecycle coverage plan cancel with reason "{reason}" and format "{fmt}"')
def step_lc_plan_cancel_fmt(context: Context, reason: str, fmt: str) -> None:
    pid = context.lc_plan.identity.plan_id
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["cancel", pid, "--reason", reason, "--format", fmt]
    )


@when('I run lifecycle coverage plan cancel providing reason "{reason}"')
def step_lc_plan_cancel_reason(context: Context, reason: str) -> None:
    pid = getattr(context, "lc_plan", None)
    plan_id = pid.identity.plan_id if pid else _PLAN_ULID
    context.lc_result = context.lc_runner.invoke(
        plan_app, ["cancel", plan_id, "--reason", reason]
    )


@when("I run lifecycle coverage plan cancel without reason")
def step_lc_plan_cancel_no_reason(context: Context) -> None:
    pid = context.lc_plan.identity.plan_id
    context.lc_result = context.lc_runner.invoke(plan_app, ["cancel", pid])


@then("lifecycle coverage plan cancel should succeed")
def step_lc_plan_cancel_ok(context: Context) -> None:
    assert context.lc_result.exit_code == 0, f"CLI failed: {context.lc_result.output}"


@then('lifecycle coverage cancel output should contain "{text}"')
def step_lc_cancel_output(context: Context, text: str) -> None:
    assert text in context.lc_result.output


@then("lifecycle coverage plan cancel should abort")
def step_lc_plan_cancel_abort(context: Context) -> None:
    assert context.lc_result.exit_code != 0
