"""Step definitions for plan commands coverage tests."""

from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import typer
from behave import given, then, when
from typer.testing import CliRunner

from cleveragents.application.services.plan_service import PlanService
from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.core.exceptions import (
    CleverAgentsError,
    PlanError,
    ValidationError,
)


@dataclass
class MockPlan:
    """Mock plan object for testing."""

    name: str
    prompt: str | None
    status: str
    created_at: datetime
    current: bool = False


@dataclass
class MockChange:
    """Mock change object for testing."""

    file_path: str
    operation: str
    content: str | None = None


def _mock_plan_container(project):
    """Create a mock container with plan and project services."""

    mock_container = MagicMock()
    mock_plan_service = MagicMock(spec=PlanService)
    mock_project_service = MagicMock()
    mock_project_service.get_current_project.return_value = project
    mock_container.plan_service.return_value = mock_plan_service
    mock_container.project_service.return_value = mock_project_service
    return mock_container, mock_plan_service, mock_project_service


@given("I have a plan test project initialized")
def step_plan_initialized_project(context):
    """Create an initialized project for plan tests."""
    project_dir = Path(context.temp_dir) / ".cleveragents"
    project_dir.mkdir(exist_ok=True)
    project_file = project_dir / "project.json"
    project_file.write_text('{"name": "test_project", "version": "1.0.0"}')


@given("I have an initialized project with current plan")
def step_initialized_project_with_plan(context):
    """Create an initialized project with a current plan."""
    step_plan_initialized_project(context)
    context.current_plan = MockPlan(
        name="current-plan",
        prompt="Build a feature",
        status="draft",
        created_at=datetime.now(),
        current=True,
    )


@given("I have an initialized project with built plan")
def step_initialized_project_with_built_plan(context):
    """Create an initialized project with a built plan."""
    step_initialized_project_with_plan(context)
    context.current_plan.status = "built"
    context.pending_changes = [
        MockChange("file1.py", "create"),
        MockChange("file2.py", "modify"),
        MockChange("file3.py", "delete"),
    ]


@given("I have an initialized project with empty plan")
def step_initialized_project_with_empty_plan(context):
    """Create an initialized project with an empty plan."""
    step_plan_initialized_project(context)
    context.current_plan = MockPlan(
        name="empty-plan",
        prompt=None,
        status="draft",
        created_at=datetime.now(),
        current=True,
    )


@given("I have an initialized project with no pending changes")
def step_initialized_project_no_pending_changes(context):
    """Create an initialized project with no pending changes."""
    step_initialized_project_with_plan(context)
    context.pending_changes = []


@given("I have an initialized project with no plans")
def step_initialized_project_no_plans(context):
    """Create an initialized project with no plans."""
    step_plan_initialized_project(context)
    context.current_plan = None


@given("I have an initialized project with multiple plans")
def step_initialized_project_multiple_plans(context):
    """Create an initialized project with multiple plans."""
    step_plan_initialized_project(context)
    context.plans = [
        MockPlan("plan-1", "First plan", "draft", datetime.now(), current=True),
        MockPlan("plan-2", "Second plan", "built", datetime.now()),
        MockPlan("other-plan", "Third plan", "applied", datetime.now()),
    ]
    context.current_plan = context.plans[0]


@given("I have an initialized project with many pending changes")
def step_initialized_project_many_changes(context):
    """Create an initialized project with many pending changes."""
    step_initialized_project_with_plan(context)
    context.pending_changes = [
        MockChange(f"file{i}.py", ["create", "modify", "delete"][i % 3])
        for i in range(15)
    ]


@when('I execute plan tell with prompt "{prompt}" and name "{name}"')
def step_run_plan_tell_with_name(context, prompt, name):
    """Run plan tell command with prompt and name."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan = MockPlan(
            name=name, prompt=prompt, status="draft", created_at=datetime.now()
        )
        mock_plan_service.create_plan.return_value = mock_plan
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["tell", prompt, "--name", name])
        context.result = result
        context.created_plan = mock_plan


@when('I execute plan tell with prompt "{prompt}"')
def step_run_plan_tell(context, prompt):
    """Run plan tell command with a prompt."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan = MockPlan(
            name=f"plan_{prompt[:10].replace(' ', '_')}",
            prompt=prompt,
            status="draft",
            created_at=datetime.now(),
        )
        mock_plan_service.create_plan.return_value = mock_plan
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["tell", prompt])
        context.result = result
        context.created_plan = mock_plan


@when("I execute plan tell with invalid prompt causing validation error")
def step_run_plan_tell_validation_error(context):
    """Run plan tell with validation error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.create_plan.side_effect = ValidationError("Invalid prompt")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["tell", ""])
        context.result = result


@when("I execute plan tell causing plan error")
def step_run_plan_tell_plan_error(context):
    """Run plan tell with plan error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.create_plan.side_effect = PlanError("Plan creation failed")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["tell", "test"])
        context.result = result


@when("I execute plan tell causing general error")
def step_run_plan_tell_general_error(context):
    """Run plan tell with general error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.create_plan.side_effect = CleverAgentsError("General error")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["tell", "test"])
        context.result = result


@when('I execute plan tell streaming with prompt "{prompt}"')
def step_run_plan_tell_streaming(context, prompt):
    """Run plan tell command in streaming mode."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_container.return_value.plan_service.return_value = mock_plan_service

        mock_project_service = MagicMock()
        mock_project = MagicMock()
        mock_project_service.get_current_project.return_value = mock_project
        mock_container.return_value.project_service.return_value = mock_project_service

        streaming_mock = AsyncMock()
        with patch(
            "cleveragents.cli.commands.plan._tell_streaming",
            new=streaming_mock,
        ):
            result = runner.invoke(plan_app, ["tell", prompt, "--stream"])

    context.result = result
    context.stream_call_count = streaming_mock.await_count
    context.stream_call_args = streaming_mock.await_args_list
    context.stream_prompt = prompt
    context.stream_plan_service = mock_plan_service


@when("I execute plan build")
def step_run_plan_build(context):
    """Run plan build command."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        changes = [
            MockChange("api.py", "create"),
            MockChange("models.py", "create"),
        ]
        mock_plan_service.build_plan.return_value = changes
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["build"])
        context.result = result
        context.generated_changes = changes


@when("I execute plan build with verbose flag")
def step_run_plan_build_verbose(context):
    """Run plan build command with verbose flag."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        changes = [MockChange("api.py", "create")]
        mock_plan_service.build_plan.return_value = changes
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["build", "--verbose"])
        context.result = result


@when("I execute plan build generating no changes")
def step_run_plan_build_no_changes(context):
    """Run plan build generating no changes."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.build_plan.return_value = []
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["build"])
        context.result = result


@when("I execute plan build causing plan error")
def step_run_plan_build_plan_error(context):
    """Run plan build with plan error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.build_plan.side_effect = PlanError("Build failed")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["build"])
        context.result = result


@when("I execute plan build causing general error")
def step_run_plan_build_general_error(context):
    """Run plan build with general error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.build_plan.side_effect = CleverAgentsError("Build error")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["build"])
        context.result = result


@when("I execute plan apply with confirmation")
def step_run_plan_apply_confirm(context):
    """Run plan apply with user confirmation."""
    import os
    from io import StringIO

    from rich.console import Console

    runner = CliRunner()

    # Create a string buffer to capture console output
    string_buffer = StringIO()
    test_console = Console(
        file=string_buffer, force_terminal=True, width=120, force_jupyter=False
    )

    # Force confirmation prompt even in testing mode
    # This ensures the truncated changes list is shown regardless of CLEVERAGENTS_TESTING_USE_MOCK_AI
    env = os.environ.copy()
    env["CLEVERAGENTS_TEST_FORCE_CONFIRMATION"] = "true"

    with (
        patch("cleveragents.application.container.get_container") as mock_container,
        patch("cleveragents.cli.commands.plan.console", test_console),
    ):
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.get_pending_changes.return_value = context.pending_changes
        mock_plan_service.apply_changes.return_value = len(context.pending_changes)
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["apply"], input="y\n", env=env)

        # Store both outputs for testing
        context.result = result
        context.console_output = string_buffer.getvalue()


@when("I execute plan apply with auto-confirm")
def step_run_plan_apply_auto_confirm(context):
    """Run plan apply with auto-confirm flag."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.get_pending_changes.return_value = context.pending_changes
        mock_plan_service.apply_changes.return_value = len(context.pending_changes)
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["apply", "--yes"])
        context.result = result


@when("I execute plan apply")
def step_run_plan_apply_no_changes(context):
    """Run plan apply with no changes."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.get_pending_changes.return_value = []
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["apply"])
        context.result = result


@when("I execute plan apply and cancel confirmation")
def step_run_plan_apply_cancel(context):
    """Run plan apply and cancel confirmation."""
    import os

    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.get_pending_changes.return_value = context.pending_changes
        mock_container.return_value.plan_service.return_value = mock_plan_service

        # Force the confirmation prompt to appear even in test mode
        old_value = os.environ.get("CLEVERAGENTS_TEST_FORCE_CONFIRMATION")
        os.environ["CLEVERAGENTS_TEST_FORCE_CONFIRMATION"] = "true"

        try:
            result = runner.invoke(plan_app, ["apply"], input="n\n")
            context.result = result
        finally:
            # Restore the old value or remove the variable
            if old_value is None:
                os.environ.pop("CLEVERAGENTS_TEST_FORCE_CONFIRMATION", None)
            else:
                os.environ["CLEVERAGENTS_TEST_FORCE_CONFIRMATION"] = old_value


@when("I execute plan apply causing plan error")
def step_run_plan_apply_plan_error(context):
    """Run plan apply with plan error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.get_pending_changes.side_effect = PlanError("Apply failed")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["apply"])
        context.result = result


@when("I execute plan apply causing general error")
def step_run_plan_apply_general_error(context):
    """Run plan apply with general error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.get_pending_changes.side_effect = CleverAgentsError(
            "Apply error"
        )
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["apply"])
        context.result = result


@when('I run plan new with name "{name}"')
def step_run_plan_new(context, name):
    """Run plan new command."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan = MockPlan(
            name=name, prompt=None, status="draft", created_at=datetime.now()
        )
        mock_plan_service.create_empty_plan.return_value = mock_plan
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["new", name])
        context.result = result
        context.created_plan = mock_plan


@when('I execute plan new with name "{name}"')
def step_run_plan_new_with_name(context, name):
    """Run plan new command with a name."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan = MockPlan(
            name=name, prompt=None, status="draft", created_at=datetime.now()
        )
        mock_plan_service.new_plan.return_value = mock_plan
        mock_container.return_value.plan_service.return_value = mock_plan_service

        # Also mock project service for _get_current_project
        mock_project_service = MagicMock()
        mock_project = MagicMock()
        mock_project_service.get_current_project.return_value = mock_project
        mock_container.return_value.project_service.return_value = mock_project_service

        result = runner.invoke(plan_app, ["new", name])
        context.result = result
        context.created_plan = mock_plan


@when("I execute plan new without an initialized project")
def step_run_plan_new_without_project(context):
    """Run plan new when no project exists."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_container.return_value.plan_service.return_value = mock_plan_service

        mock_project_service = MagicMock()
        mock_project_service.get_current_project.return_value = None
        mock_container.return_value.project_service.return_value = mock_project_service

        result = runner.invoke(plan_app, ["new", "missing-project"])
        context.result = result
        context.new_plan_service_mock = mock_plan_service


@when("I execute plan new with invalid name")
def step_run_plan_new_invalid(context):
    """Run plan new with invalid name."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.new_plan.side_effect = ValidationError("Invalid name")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        # Also mock project service
        mock_project_service = MagicMock()
        mock_project = MagicMock()
        mock_project_service.get_current_project.return_value = mock_project
        mock_container.return_value.project_service.return_value = mock_project_service

        result = runner.invoke(plan_app, ["new", ""])
        context.result = result


@when("I execute plan new causing general error")
def step_run_plan_new_error(context):
    """Run plan new with general error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.new_plan.side_effect = CleverAgentsError("New plan error")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        # Also mock project service
        mock_project_service = MagicMock()
        mock_project = MagicMock()
        mock_project_service.get_current_project.return_value = mock_project
        mock_container.return_value.project_service.return_value = mock_project_service

        result = runner.invoke(plan_app, ["new", "test"])
        context.result = result


@when("I execute plan current")
def step_run_plan_current(context):
    """Run plan current command."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.get_current_plan.return_value = context.current_plan
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["current"])
        context.result = result


@when("I execute plan current causing error")
def step_run_plan_current_error(context):
    """Run plan current with error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.get_current_plan.side_effect = CleverAgentsError(
            "Current error"
        )
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["current"])
        context.result = result


@when("I execute plan list")
def step_run_plan_list(context):
    """Run plan list command."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        plans = getattr(context, "plans", [])
        mock_plan_service.list_plans.return_value = plans
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["list"])
        context.result = result


@when("I execute plan list causing error")
def step_run_plan_list_error(context):
    """Run plan list with error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.list_plans.side_effect = CleverAgentsError("List error")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["list"])
        context.result = result


@when('I run plan cd to "{name}"')
def step_run_plan_cd(context, name):
    """Run plan cd command."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        # Find the plan by name
        plan = next((p for p in context.plans if p.name == name), None)
        if plan:
            mock_plan_service.switch_to_plan.return_value = plan
        else:
            mock_plan_service.switch_to_plan.side_effect = ValidationError(
                "Plan not found"
            )
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["cd", name])
        context.result = result


@when('I execute plan cd to "{name}"')
def step_execute_plan_cd_to_name(context, name):
    """Execute plan cd to a specific plan."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        # Find the plan by name
        plan = next((p for p in getattr(context, "plans", []) if p.name == name), None)
        if plan:
            mock_plan_service.switch_to_plan.return_value = plan
        else:
            # Create a mock plan for the scenario
            plan = MockPlan(
                name=name, prompt="test", status="draft", created_at=datetime.now()
            )
            mock_plan_service.switch_to_plan.return_value = plan
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["cd", name])
        context.result = result


@when("I execute plan cd to non-existent plan")
def step_run_plan_cd_not_found(context):
    """Run plan cd to non-existent plan."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.switch_to_plan.side_effect = ValidationError("Plan not found")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["cd", "non-existent"])
        context.result = result


@when("I execute plan cd causing error")
def step_run_plan_cd_error(context):
    """Run plan cd with error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.switch_to_plan.side_effect = CleverAgentsError("CD error")
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["cd", "test"])
        context.result = result


@when('I execute plan continue with prompt "{prompt}"')
def step_execute_plan_continue_with_prompt(context, prompt):
    """Execute plan continue with prompt."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.continue_plan.return_value = None
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["continue", prompt])
        context.result = result


@when('I run plan continue with prompt "{prompt}"')
def step_run_plan_continue_with_prompt(context, prompt):
    """Run plan continue with prompt."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.continue_plan.return_value = None
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["continue", prompt])
        context.result = result


@when("I execute plan continue without prompt")
def step_run_plan_continue_no_prompt(context):
    """Run plan continue without prompt."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        plan = getattr(context, "current_plan", None)
        mock_plan_service.get_current_plan.return_value = plan
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["continue"])
        context.result = result


@when("I execute plan continue causing error")
def step_run_plan_continue_error(context):
    """Run plan continue with error."""
    runner = CliRunner()
    with patch("cleveragents.application.container.get_container") as mock_container:
        mock_plan_service = MagicMock(spec=PlanService)
        mock_plan_service.continue_plan.side_effect = CleverAgentsError(
            "Continue error"
        )
        mock_container.return_value.plan_service.return_value = mock_plan_service

        result = runner.invoke(plan_app, ["continue", "test"])
        context.result = result


@when("I call plan tell_command without a project")
def step_call_plan_tell_command_without_project(context):
    """Call tell_command when no project exists."""
    from cleveragents.cli.commands.plan import tell_command

    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, project_service = _mock_plan_container(project=None)
        mock_get_container.return_value = container

        context.plan_service_mock = plan_service
        context.project_service_mock = project_service
        context.call_result = None
        context.call_exception = None
        try:
            tell_command("Automate improvements")
        except Exception as exc:
            context.call_exception = exc


@when("I call plan build_command without a project")
def step_call_plan_build_command_without_project(context):
    """Call build_command when no project exists."""
    from cleveragents.cli.commands.plan import build_command

    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, project_service = _mock_plan_container(project=None)
        mock_get_container.return_value = container

        context.plan_service_mock = plan_service
        context.project_service_mock = project_service
        context.call_result = None
        context.call_exception = None
        try:
            build_command()
        except Exception as exc:
            context.call_exception = exc


@when("I call plan apply_command successfully")
def step_call_plan_apply_command_success(context):
    """Call apply_command when pending changes exist."""
    from cleveragents.cli.commands.plan import apply_command

    project = object()
    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, _project_service = _mock_plan_container(
            project=project
        )
        plan_service.apply_changes.return_value = 3
        mock_get_container.return_value = container

        context.project_mock = project
        context.plan_service_mock = plan_service
        context.call_exception = None
        context.call_result = apply_command()


@when("I call plan apply_command without a project")
def step_call_plan_apply_command_without_project(context):
    """Call apply_command when no project exists."""
    from cleveragents.cli.commands.plan import apply_command

    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, project_service = _mock_plan_container(project=None)
        mock_get_container.return_value = container

        context.plan_service_mock = plan_service
        context.project_service_mock = project_service
        context.call_result = None
        context.call_exception = None
        try:
            apply_command()
        except Exception as exc:
            context.call_exception = exc


@when("I call plan new_command without a project")
def step_call_plan_new_command_without_project(context):
    """Call new_command when no project exists."""
    from cleveragents.cli.commands.plan import new_command

    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, project_service = _mock_plan_container(project=None)
        mock_get_container.return_value = container

        context.plan_service_mock = plan_service
        context.project_service_mock = project_service
        context.call_result = None
        context.call_exception = None
        try:
            new_command("feature-x")
        except Exception as exc:
            context.call_exception = exc


@when("I call plan current_command successfully")
def step_call_plan_current_command_success(context):
    """Call current_command when a project has an active plan."""
    from cleveragents.cli.commands.plan import current_command

    project = object()
    current_plan = MockPlan(
        name="demo-plan",
        prompt="Ship features",
        status="draft",
        created_at=datetime.now(),
        current=True,
    )
    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, _project_service = _mock_plan_container(
            project=project
        )
        plan_service.get_current_plan.return_value = current_plan
        mock_get_container.return_value = container

        context.project_mock = project
        context.plan_service_mock = plan_service
        context.mock_current_plan = current_plan
        context.call_exception = None
        context.call_result = current_command()


@when("I call plan current_command without a project")
def step_call_plan_current_command_without_project(context):
    """Call current_command when no project exists."""
    from cleveragents.cli.commands.plan import current_command

    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, project_service = _mock_plan_container(project=None)
        mock_get_container.return_value = container

        context.plan_service_mock = plan_service
        context.project_service_mock = project_service
        context.call_result = None
        context.call_exception = None
        try:
            current_command()
        except Exception as exc:
            context.call_exception = exc


@when("I call plan list_command with no stored plans")
def step_call_plan_list_command_with_no_plans(context):
    """Call list_command when the plan service returns no plans."""
    from cleveragents.cli.commands.plan import list_command

    project = object()
    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, _project_service = _mock_plan_container(
            project=project
        )
        plan_service.list_plans.return_value = None
        mock_get_container.return_value = container

        context.project_mock = project
        context.plan_service_mock = plan_service
        context.call_exception = None
        context.call_result = list_command()


@when("I call plan list_command without a project")
def step_call_plan_list_command_without_project(context):
    """Call list_command when no project exists."""
    from cleveragents.cli.commands.plan import list_command

    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, project_service = _mock_plan_container(project=None)
        mock_get_container.return_value = container

        context.plan_service_mock = plan_service
        context.project_service_mock = project_service
        context.call_result = None
        context.call_exception = None
        try:
            list_command()
        except Exception as exc:
            context.call_exception = exc


@when("I call plan cd_command without a project")
def step_call_plan_cd_command_without_project(context):
    """Call cd_command when no project exists."""
    from cleveragents.cli.commands.plan import cd_command

    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, project_service = _mock_plan_container(project=None)
        mock_get_container.return_value = container

        context.plan_service_mock = plan_service
        context.project_service_mock = project_service
        context.call_result = None
        context.call_exception = None
        try:
            cd_command("feature-stream")
        except Exception as exc:
            context.call_exception = exc


@when('I call plan cd_command successfully to "{name}"')
def step_call_plan_cd_command_success(context, name):
    """Call cd_command when a project exists."""
    from cleveragents.cli.commands.plan import cd_command

    project = object()
    switched_plan = MockPlan(
        name=name,
        prompt="Switch plan",
        status="draft",
        created_at=datetime.now(),
        current=True,
    )
    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, _project_service = _mock_plan_container(
            project=project
        )
        plan_service.switch_to_plan.return_value = switched_plan
        mock_get_container.return_value = container

        context.project_mock = project
        context.plan_service_mock = plan_service
        context.call_exception = None
        context.call_result = cd_command(name)


@when('I call plan continue_command with prompt "{prompt}"')
def step_call_plan_continue_command_with_prompt(context, prompt):
    """Call continue_command with explicit prompt."""
    from cleveragents.cli.commands.plan import continue_command

    project = object()
    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, _project_service = _mock_plan_container(
            project=project
        )
        plan_service.continue_plan.return_value = None
        mock_get_container.return_value = container

        context.project_mock = project
        context.plan_service_mock = plan_service
        context.call_exception = None
        context.call_result = continue_command(prompt)


@when("I call plan continue_command without prompt and an active plan")
def step_call_plan_continue_command_with_active_plan(context):
    """Call continue_command without prompt when a current plan exists."""
    from cleveragents.cli.commands.plan import continue_command

    project = object()
    active_plan = MockPlan(
        name="active-plan",
        prompt="Keep going",
        status="draft",
        created_at=datetime.now(),
        current=True,
    )
    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, _project_service = _mock_plan_container(
            project=project
        )
        plan_service.get_current_plan.return_value = active_plan
        mock_get_container.return_value = container

        context.project_mock = project
        context.plan_service_mock = plan_service
        context.active_plan = active_plan
        context.call_exception = None
        context.call_result = continue_command()


@when("I call plan continue_command without prompt and no current plan")
def step_call_plan_continue_command_without_plan(context):
    """Call continue_command without prompt when no plan exists."""
    from cleveragents.cli.commands.plan import continue_command

    project = object()
    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, _project_service = _mock_plan_container(
            project=project
        )
        plan_service.get_current_plan.return_value = None
        mock_get_container.return_value = container

        context.project_mock = project
        context.plan_service_mock = plan_service
        context.call_result = None
        context.call_exception = None
        try:
            continue_command()
        except Exception as exc:
            context.call_exception = exc


@when("I call plan continue_command without an initialized project")
def step_call_plan_continue_command_without_project(context):
    """Call continue_command when no project is initialized."""
    from cleveragents.cli.commands.plan import continue_command

    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, project_service = _mock_plan_container(project=None)
        mock_get_container.return_value = container

        context.plan_service_mock = plan_service
        context.project_service_mock = project_service
        context.call_result = None
        context.call_exception = None
        try:
            continue_command("Stream coverage")
        except Exception as exc:
            context.call_exception = exc


@when("I call the plan helper to fetch current project without initialization")
def step_call_plan_helper_without_project(context):
    """Call _get_current_project when no project exists."""
    from cleveragents.cli.commands import plan as plan_module

    with patch(
        "cleveragents.application.container.get_container"
    ) as mock_get_container:
        container, plan_service, project_service = _mock_plan_container(project=None)
        mock_get_container.return_value = container

        context.plan_service_mock = plan_service
        context.project_service_mock = project_service
        context.call_result = None
        context.call_exception = None
        try:
            plan_module._get_current_project()
        except Exception as exc:
            context.call_exception = exc


# Then steps for assertions


@then("the programmatic plan apply_command should report {count:d} applied changes")
def step_assert_programmatic_apply_changes(context, count):
    """Assert apply_command returned the expected change count."""
    assert context.call_exception is None
    assert context.call_result == count
    context.plan_service_mock.apply_changes.assert_called_once()
    kwargs = context.plan_service_mock.apply_changes.call_args.kwargs
    assert kwargs["project"] is context.project_mock


@then("the programmatic current_command should return the mock plan")
def step_assert_programmatic_current_command(context):
    """Assert current_command returned the mock plan."""
    assert context.call_exception is None
    assert context.call_result is context.mock_current_plan


@then("the programmatic list command should return an empty list")
def step_assert_programmatic_list_command_empty(context):
    """Assert list_command returned an empty list."""
    assert context.call_exception is None
    assert context.call_result == []
    context.plan_service_mock.list_plans.assert_called_once()


@then('the programmatic cd command should switch to plan "{name}"')
def step_assert_programmatic_cd_command(context, name):
    """Assert cd_command switched to the requested plan."""
    assert context.call_exception is None
    context.plan_service_mock.switch_to_plan.assert_called_once()
    kwargs = context.plan_service_mock.switch_to_plan.call_args.kwargs
    assert kwargs["project"] is context.project_mock
    assert kwargs["name"] == name


@then('the programmatic continue command should forward prompt "{prompt}"')
def step_assert_programmatic_continue_forward(context, prompt):
    """Assert continue_command forwarded the prompt to the plan service."""
    assert context.call_exception is None
    context.plan_service_mock.continue_plan.assert_called_once_with(
        project=context.project_mock,
        prompt=prompt,
    )


@then("the programmatic continue command should keep the current plan active")
def step_assert_programmatic_continue_keeps_plan(context):
    """Assert continue_command validated the current plan without new instructions."""
    assert context.call_exception is None
    context.plan_service_mock.get_current_plan.assert_called_once_with(
        project=context.project_mock
    )
    context.plan_service_mock.continue_plan.assert_not_called()


@then("a typer Abort should be raised for missing project")
def step_assert_typer_abort(context):
    """Assert _get_current_project raised a typer.Abort exception."""
    assert isinstance(context.call_exception, typer.Abort)


@then("the plan tell should execute successfully")
def step_plan_tell_success(context):
    """Assert plan tell executed successfully."""
    assert context.result.exit_code == 0, (
        f"Exit code was {context.result.exit_code}, output: {context.result.output}"
    )
    assert "Plan created" in context.result.output or "✓" in context.result.output


@then("the plan should be created with name and prompt")
def step_plan_created_with_details(context):
    """Assert plan was created with expected details."""
    assert context.created_plan is not None
    assert context.created_plan.prompt is not None


@then('the plan should be created with custom name "{name}"')
def step_plan_created_with_name(context, name):
    """Assert plan was created with custom name."""
    assert context.created_plan.name == name


@then("the plan tell should abort with validation error")
def step_plan_tell_validation_abort(context):
    """Assert plan tell aborted with validation error."""
    assert context.result.exit_code != 0
    assert (
        "Validation Error" in context.result.output
        or "validation" in context.result.output.lower()
    )


@then("the plan tell should abort with plan error")
def step_plan_tell_plan_abort(context):
    """Assert plan tell aborted with plan error."""
    assert context.result.exit_code != 0
    assert (
        "Plan Error" in context.result.output or "plan" in context.result.output.lower()
    )


@then("the plan tell should abort with general error")
def step_plan_tell_general_abort(context):
    """Assert plan tell aborted with general error."""
    assert context.result.exit_code != 0
    assert "Error" in context.result.output


@then("the plan tell streaming path should execute successfully")
def step_plan_tell_streaming_success(context):
    """Assert plan tell streaming path executed via asyncio."""
    assert context.result.exit_code == 0, (
        f"Exit code was {context.result.exit_code}, output: {context.result.output}"
    )
    assert getattr(context, "stream_call_count", 0) == 1, (
        "Streaming coroutine was not awaited"
    )
    assert context.stream_call_args, "No streaming call arguments recorded"
    first_call = context.stream_call_args[0]
    assert first_call.args[1] == context.stream_prompt
    assert first_call.args[3] is context.stream_plan_service


@then("the plan build should execute successfully")
def step_plan_build_success(context):
    """Assert plan build executed successfully."""
    assert context.result.exit_code == 0, f"Exit code was {context.result.exit_code}"


@then("plan changes should be generated")
def step_changes_generated(context):
    """Assert changes were generated."""
    assert (
        "change" in context.result.output.lower()
        or "generated" in context.result.output.lower()
    )


@then("the plan build should execute successfully with verbose output")
def step_plan_build_verbose_success(context):
    """Assert plan build executed with verbose output."""
    assert context.result.exit_code == 0


@then("the build should complete with no changes message")
def step_build_no_changes_message(context):
    """Assert build completed with no changes."""
    assert context.result.exit_code == 0
    assert (
        "No changes" in context.result.output
        or "no changes" in context.result.output.lower()
    )


@then("the plan build should abort with plan error")
def step_plan_build_plan_abort(context):
    """Assert plan build aborted with plan error."""
    assert context.result.exit_code != 0
    assert (
        "Build Error" in context.result.output
        or "error" in context.result.output.lower()
    )


@then("the plan build should abort with general error")
def step_plan_build_general_abort(context):
    """Assert plan build aborted with general error."""
    assert context.result.exit_code != 0
    assert "Error" in context.result.output


@then("the plan apply should execute successfully")
def step_plan_apply_success(context):
    """Assert plan apply executed successfully."""
    assert context.result.exit_code == 0


@then("plan changes should be applied")
def step_changes_applied(context):
    """Assert changes were applied."""
    output = context.result.output
    console_output = getattr(context, "console_output", "")
    combined_output = output + console_output
    assert "applied" in combined_output.lower() or "✓" in combined_output


@then("the plan apply should execute successfully without prompting")
def step_plan_apply_auto_success(context):
    """Assert plan apply executed without prompting."""
    assert context.result.exit_code == 0
    assert "Apply these changes?" not in context.result.output


@then("the apply should exit with no changes message")
def step_apply_no_changes_exit(context):
    """Assert apply exited with no changes."""
    assert context.result.exit_code == 0
    assert (
        "No changes" in context.result.output
        or "no changes" in context.result.output.lower()
    )


@then("the apply should exit with cancelled message")
def step_apply_cancelled_exit(context):
    """Assert apply was cancelled."""
    assert context.result.exit_code == 0
    assert (
        "Cancelled" in context.result.output
        or "cancelled" in context.result.output.lower()
    )


@then("the plan apply should abort with plan error")
def step_plan_apply_plan_abort(context):
    """Assert plan apply aborted with plan error."""
    assert context.result.exit_code != 0
    assert (
        "Apply Error" in context.result.output
        or "error" in context.result.output.lower()
    )


@then("the plan apply should abort with general error")
def step_plan_apply_general_abort(context):
    """Assert plan apply aborted with general error."""
    assert context.result.exit_code != 0
    assert "Error" in context.result.output


@then("the plan new should execute successfully")
def step_plan_new_success(context):
    """Assert plan new executed successfully."""
    assert context.result.exit_code == 0


@then('empty plan "{name}" should be created')
def step_empty_plan_created(context, name):
    """Assert empty plan was created."""
    assert context.created_plan.name == name
    assert context.created_plan.prompt is None


@then("the plan new should abort due to missing project")
def step_plan_new_missing_project_abort(context):
    """Assert plan new aborted when no project exists."""
    assert context.result.exit_code != 0
    assert "No project found" in context.result.output
    if hasattr(context, "new_plan_service_mock"):
        context.new_plan_service_mock.new_plan.assert_not_called()


@then("the plan new should abort with validation error")
def step_plan_new_validation_abort(context):
    """Assert plan new aborted with validation error."""
    assert context.result.exit_code != 0
    assert (
        "Validation Error" in context.result.output
        or "validation" in context.result.output.lower()
    )


@then("the plan new should abort with general error")
def step_plan_new_general_abort(context):
    """Assert plan new aborted with general error."""
    assert context.result.exit_code != 0
    assert "Error" in context.result.output


@then("the plan current should execute successfully")
def step_plan_current_success(context):
    """Assert plan current executed successfully."""
    assert context.result.exit_code == 0


@then("current plan details should be displayed")
def step_current_plan_details_displayed(context):
    """Assert current plan details are displayed."""
    assert "Current Plan" in context.result.output or (
        hasattr(context, "current_plan")
        and context.current_plan
        and context.current_plan.name in context.result.output
    )


@then("plan command current plan details should be displayed")
def step_current_plan_displayed(context):
    """Assert current plan details are displayed."""
    assert (
        "Current Plan" in context.result.output
        or context.current_plan.name in context.result.output
    )


@then("the current should exit with no plan message")
def step_current_no_plan_exit(context):
    """Assert current exited with no plan message."""
    assert context.result.exit_code == 0
    assert (
        "No current plan" in context.result.output
        or "no current plan" in context.result.output.lower()
    )


@then("the plan current should abort with error")
def step_plan_current_abort(context):
    """Assert plan current aborted with error."""
    assert context.result.exit_code != 0
    assert "Error" in context.result.output


@then("the plan list should execute successfully")
def step_plan_list_success(context):
    """Assert plan list executed successfully."""
    assert context.result.exit_code == 0


@then("all plans should be displayed in table")
def step_all_plans_displayed_in_table(context):
    """Assert plans are displayed in table."""
    if hasattr(context, "plans") and context.plans:
        for plan in context.plans:
            assert plan.name in context.result.output


@then("plan command all plans should be displayed in table")
def step_plans_displayed_table(context):
    """Assert plans are displayed in table."""
    if hasattr(context, "plans") and context.plans:
        for plan in context.plans:
            assert plan.name in context.result.output


@then("the list should show no plans message")
def step_list_no_plans_message(context):
    """Assert list shows no plans message."""
    assert (
        "No plans" in context.result.output
        or "no plans" in context.result.output.lower()
    )


@then("the plan list should abort with error")
def step_plan_list_abort(context):
    """Assert plan list aborted with error."""
    assert context.result.exit_code != 0
    assert "Error" in context.result.output


@then("the plan cd should execute successfully")
def step_plan_cd_success(context):
    """Assert plan cd executed successfully."""
    assert context.result.exit_code == 0


@then('current plan should be "{name}"')
def step_current_plan_is(context, name):
    """Assert current plan is the expected one."""
    assert "Switched to plan" in context.result.output or name in context.result.output


@then("the plan cd should abort with validation error")
def step_plan_cd_validation_abort(context):
    """Assert plan cd aborted with validation error."""
    assert context.result.exit_code != 0
    assert (
        "not found" in context.result.output.lower()
        or "validation" in context.result.output.lower()
    )


@then("the plan cd should abort with error")
def step_plan_cd_abort(context):
    """Assert plan cd aborted with error."""
    assert context.result.exit_code != 0
    assert "Error" in context.result.output


@then("the plan continue should execute successfully")
def step_plan_continue_success(context):
    """Assert plan continue executed successfully."""
    assert context.result.exit_code == 0


@then("instructions should be added to plan")
def step_instructions_added_to_plan(context):
    """Assert instructions were added to plan."""
    assert (
        "Added instructions" in context.result.output
        or "added" in context.result.output.lower()
    )


@then("plan command instructions should be added to plan")
def step_instructions_added(context):
    """Assert instructions were added to plan."""
    assert (
        "Added instructions" in context.result.output
        or "added" in context.result.output.lower()
    )


@then("plan command continue message should be displayed")
def step_continue_message_displayed(context):
    """Assert continue message is displayed."""
    assert (
        "Continuing" in context.result.output
        or "continue" in context.result.output.lower()
    )


@then("continue message should be displayed")
def step_continue_message_should_be_displayed(context):
    """Assert continue message is displayed."""
    assert (
        "Continuing" in context.result.output
        or "continue" in context.result.output.lower()
    )


@then("the continue should abort with no plan error")
def step_continue_no_plan_abort(context):
    """Assert continue aborted with no plan error."""
    assert context.result.exit_code != 0
    assert (
        "No current plan" in context.result.output
        or "no current plan" in context.result.output.lower()
    )


@then("the plan continue should abort with error")
def step_plan_continue_abort(context):
    """Assert plan continue aborted with error."""
    assert context.result.exit_code != 0
    assert "Error" in context.result.output


@then("the apply should show truncated changes list")
def step_apply_truncated_changes(context):
    """Assert apply shows truncated changes list."""
    # Check both possible outputs (runner output and console output)
    output = context.result.output
    console_output = getattr(context, "console_output", "")
    combined_output = output + console_output

    # More flexible check for truncation - looking for the pattern of truncated output
    has_truncation = (
        ("... and" in combined_output and "more" in combined_output)
        or ("more changes" in combined_output and "..." in combined_output)
        or (combined_output.count("file") >= 10 and "more" in combined_output)
    )

    if not has_truncation:
        # If the assertion will fail, provide helpful debug info
        print("DEBUG: Output does not show truncation.")
        print(f"DEBUG: Runner output:\n{output}")
        print(f"DEBUG: Console output:\n{console_output}")
        print(f"DEBUG: File count in combined output: {combined_output.count('file')}")
        print(f"DEBUG: Contains '...': {'...' in combined_output}")
        print(f"DEBUG: Contains 'more': {'more' in combined_output}")

    assert has_truncation, "Expected truncated list indicator in output"


@then("plan changes should be applied after confirmation")
def step_changes_applied_after_confirm(context):
    """Assert changes were applied after confirmation."""
    output = context.result.output
    console_output = getattr(context, "console_output", "")
    combined_output = (output + console_output).lower()
    assert "applied" in combined_output
