"""Step definitions for plan CLI streaming and helper function coverage tests.

These steps cover _print_lifecycle_plan, _get_current_project, and the
programmatic wrapper functions in cleveragents.cli.commands.plan.
"""

from __future__ import annotations

import re
from io import StringIO
from unittest.mock import MagicMock, patch

import typer
from behave import then, when
from rich.console import Console

from cleveragents.core.exceptions import CleverAgentsError


def _make_mock_container(project=None):
    """Create a mock container with plan and project services."""
    mock_container = MagicMock()
    mock_plan_service = MagicMock()
    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


def _strip_markup(text: str) -> str:
    """Strip Rich markup tags from text."""
    return re.sub(r"\[[^\]]*\]", "", text)


def _make_lifecycle_plan(
    description: str = "Test plan description",
    error_message: str | None = None,
    project_links: list | None = None,
):
    """Create a real LifecyclePlan (v3 Plan) instance for testing."""
    from cleveragents.domain.models.core.plan import (
        NamespacedName,
        Plan,
        PlanIdentity,
        PlanPhase,
        PlanTimestamps,
        ProcessingState,
        ProjectLink,
    )

    links = (
        project_links
        if project_links is not None
        else [
            ProjectLink(project_name="test-project"),
        ]
    )

    return Plan(
        identity=PlanIdentity(plan_id="01HXYZ01HXYZ01HXYZ01HXYZ01"),
        namespaced_name=NamespacedName(namespace="local", name="test-plan"),
        action_name="local/test-action",
        description=description,
        phase=PlanPhase.STRATEGIZE,
        state=ProcessingState.QUEUED,
        project_links=links,
        error_message=error_message,
        timestamps=PlanTimestamps(),
    )


# ---------------------------------------------------------------------------
# _print_lifecycle_plan scenarios
# ---------------------------------------------------------------------------


@when("I call print_lifecycle_plan with a non-lifecycle plan object")
def step_call_print_lifecycle_plan_non_lifecycle(context):
    """Call _print_lifecycle_plan with a plain object (not a LifecyclePlan)."""
    from cleveragents.cli.commands.plan import _print_lifecycle_plan

    output = StringIO()
    test_console = Console(file=output, force_terminal=False, width=120)

    fake_plan = MagicMock()
    fake_plan.__str__ = lambda self: "FakePlanRepr"

    with patch("cleveragents.cli.commands.plan.console", test_console):
        _print_lifecycle_plan(fake_plan, title="Fallback Test")

    context.captured_output = output.getvalue()


@then("the fallback display path should be used for non-lifecycle plan")
def step_assert_fallback_display(context):
    """Assert the fallback path was used, printing a simple panel."""
    output = _strip_markup(context.captured_output)
    assert "Plan:" in output or "FakePlanRepr" in output, (
        f"Expected fallback display but got: {output}"
    )


@when("I call print_lifecycle_plan with a lifecycle plan that has an error_message")
def step_call_print_lifecycle_plan_with_error(context):
    """Call _print_lifecycle_plan with a plan that has error_message set."""
    from cleveragents.cli.commands.plan import _print_lifecycle_plan

    output = StringIO()
    test_console = Console(file=output, force_terminal=False, width=120)

    plan = _make_lifecycle_plan(
        description="Plan with error",
        error_message="Something went terribly wrong",
    )

    with patch("cleveragents.cli.commands.plan.console", test_console):
        _print_lifecycle_plan(plan, title="Error Plan")

    context.captured_output = output.getvalue()


@then("the lifecycle plan error_message should appear in the output")
def step_assert_error_message_displayed(context):
    """Assert the error_message text is rendered."""
    output = _strip_markup(context.captured_output)
    assert "Something went terribly wrong" in output, (
        f"Expected error message in output but got: {output}"
    )


@when("I call print_lifecycle_plan with a description longer than 200 characters")
def step_call_print_lifecycle_plan_long_desc(context):
    """Call _print_lifecycle_plan with a >200 char description."""
    from cleveragents.cli.commands.plan import _print_lifecycle_plan

    output = StringIO()
    test_console = Console(file=output, force_terminal=False, width=120)

    long_description = "A" * 250
    plan = _make_lifecycle_plan(description=long_description)

    with patch("cleveragents.cli.commands.plan.console", test_console):
        _print_lifecycle_plan(plan, title="Long Desc Plan")

    context.captured_output = output.getvalue()


@then("the lifecycle plan description should be truncated with ellipsis")
def step_assert_description_truncated(context):
    """Assert the description is truncated and ends with '...'."""
    output = _strip_markup(context.captured_output)
    # The full 250-char string should NOT appear; the truncated version (200 chars + ...) should
    assert "A" * 250 not in output, "Full description should be truncated"
    assert "..." in output, f"Expected ellipsis for truncation but got: {output}"


@when("I call print_lifecycle_plan with a lifecycle plan with no project links")
def step_call_print_lifecycle_plan_no_projects(context):
    """Call _print_lifecycle_plan with empty project_links."""
    from cleveragents.cli.commands.plan import _print_lifecycle_plan

    output = StringIO()
    test_console = Console(file=output, force_terminal=False, width=120)

    plan = _make_lifecycle_plan(project_links=[])

    with patch("cleveragents.cli.commands.plan.console", test_console):
        _print_lifecycle_plan(plan, title="No Projects Plan")

    context.captured_output = output.getvalue()


@then("the lifecycle plan output should show projects as none")
def step_assert_projects_none(context):
    """Assert the output shows (none) for projects."""
    output = _strip_markup(context.captured_output)
    assert "(none)" in output, f"Expected '(none)' for empty projects but got: {output}"


# ---------------------------------------------------------------------------
# _get_current_project scenarios
# ---------------------------------------------------------------------------


@when("I call the get_current_project helper with a valid project")
def step_call_get_current_project_valid(context):
    """Call _get_current_project when a project is available."""
    from cleveragents.cli.commands.plan import _get_current_project

    mock_project = MagicMock()
    mock_project.name = "valid-project"

    container, _, _project_service = _make_mock_container(project=mock_project)

    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        context.returned_project = _get_current_project()
        context.expected_project = mock_project


@then("the get_current_project helper should return the mock project")
def step_assert_get_current_project_returns(context):
    """Assert _get_current_project returned the mock project."""
    assert context.returned_project is context.expected_project


@when("I call the get_current_project helper with no project available")
def step_call_get_current_project_no_project(context):
    """Call _get_current_project when no project exists."""
    from cleveragents.cli.commands.plan import _get_current_project

    container, _, _ = _make_mock_container(project=None)

    context.call_exception = None
    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        # Also patch the console to avoid side effects
        output = StringIO()
        test_console = Console(file=output, force_terminal=False, width=120)
        with patch("cleveragents.cli.commands.plan.console", test_console):
            try:
                _get_current_project()
            except Exception as exc:
                context.call_exception = exc


@then("the get_current_project helper should raise typer Abort")
def step_assert_get_current_project_aborts(context):
    """Assert _get_current_project raised typer.Abort."""
    assert isinstance(context.call_exception, typer.Abort), (
        f"Expected typer.Abort but got: {type(context.call_exception)}"
    )


# ---------------------------------------------------------------------------
# tell_command programmatic wrapper
# ---------------------------------------------------------------------------


@when(
    "I invoke the plan tell_command programmatic wrapper with valid project and prompt"
)
def step_invoke_tell_command_wrapper(context):
    """Call tell_command with a valid project."""
    from cleveragents.cli.commands.plan import tell_command

    mock_project = MagicMock()
    mock_project.name = "wrapper-project"
    container, plan_service, _ = _make_mock_container(project=mock_project)

    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        tell_command(prompt="Build a REST API", name="api-plan")

    context.plan_service_mock = plan_service
    context.mock_project = mock_project


@then("the plan tell_command programmatic wrapper should call create_plan")
def step_assert_tell_command_calls_create(context):
    """Assert tell_command called create_plan with correct args."""
    context.plan_service_mock.create_plan.assert_called_once_with(
        project=context.mock_project, prompt="Build a REST API", name="api-plan"
    )


@when("I invoke the plan tell_command programmatic wrapper with no project")
def step_invoke_tell_command_wrapper_no_project(context):
    """Call tell_command when no project is available."""
    from cleveragents.cli.commands.plan import tell_command

    container, _, _ = _make_mock_container(project=None)

    context.call_exception = None
    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        try:
            tell_command(prompt="Something")
        except Exception as exc:
            context.call_exception = exc


@then("the plan tell_command programmatic wrapper should raise CleverAgentsError")
def step_assert_tell_command_raises(context):
    """Assert tell_command raised CleverAgentsError."""
    assert isinstance(context.call_exception, CleverAgentsError), (
        f"Expected CleverAgentsError but got: {type(context.call_exception)}"
    )
    assert "No project found" in str(context.call_exception)


# ---------------------------------------------------------------------------
# build_command programmatic wrapper
# ---------------------------------------------------------------------------


@when("I invoke the plan build_command programmatic wrapper with valid project")
def step_invoke_build_command_wrapper(context):
    """Call build_command with a valid project."""
    from cleveragents.cli.commands.plan import build_command

    mock_project = MagicMock()
    mock_change = MagicMock()
    mock_change.file_path = "file.py"
    mock_change.operation = "create"

    container, plan_service, _ = _make_mock_container(project=mock_project)
    plan_service.build_plan.return_value = [mock_change]

    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        context.build_result = build_command()

    context.plan_service_mock = plan_service
    context.mock_project = mock_project


@then("the plan build_command programmatic wrapper should return the changes list")
def step_assert_build_command_returns_changes(context):
    """Assert build_command returned the changes list."""
    assert len(context.build_result) == 1
    context.plan_service_mock.build_plan.assert_called_once()


@when("I invoke the plan build_command programmatic wrapper with no project")
def step_invoke_build_command_wrapper_no_project(context):
    """Call build_command when no project is available."""
    from cleveragents.cli.commands.plan import build_command

    container, _, _ = _make_mock_container(project=None)

    context.call_exception = None
    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        try:
            build_command()
        except Exception as exc:
            context.call_exception = exc


@then("the plan build_command programmatic wrapper should raise CleverAgentsError")
def step_assert_build_command_raises(context):
    """Assert build_command raised CleverAgentsError."""
    assert isinstance(context.call_exception, CleverAgentsError)
    assert "No project found" in str(context.call_exception)


# ---------------------------------------------------------------------------
# apply_command programmatic wrapper
# ---------------------------------------------------------------------------


@when("I invoke the plan apply_command programmatic wrapper with valid project")
def step_invoke_apply_command_wrapper(context):
    """Call apply_command with a valid project."""
    from cleveragents.cli.commands.plan import apply_command

    mock_project = MagicMock()
    container, plan_service, _ = _make_mock_container(project=mock_project)
    plan_service.apply_changes.return_value = 5

    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        context.apply_result = apply_command()

    context.plan_service_mock = plan_service
    context.mock_project = mock_project


@then("the plan apply_command programmatic wrapper should return applied count")
def step_assert_apply_command_returns_count(context):
    """Assert apply_command returned the applied count."""
    assert context.apply_result == 5
    context.plan_service_mock.apply_changes.assert_called_once_with(
        project=context.mock_project
    )


# ---------------------------------------------------------------------------
# new_command programmatic wrapper
# ---------------------------------------------------------------------------


@when("I invoke the plan new_command programmatic wrapper with valid project")
def step_invoke_new_command_wrapper(context):
    """Call new_command with a valid project."""
    from cleveragents.cli.commands.plan import new_command

    mock_project = MagicMock()
    container, plan_service, _ = _make_mock_container(project=mock_project)

    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        new_command(name="my-new-plan")

    context.plan_service_mock = plan_service
    context.mock_project = mock_project


@then("the plan new_command programmatic wrapper should call new_plan")
def step_assert_new_command_calls_new_plan(context):
    """Assert new_command called new_plan with correct args."""
    context.plan_service_mock.new_plan.assert_called_once_with(
        project=context.mock_project, name="my-new-plan"
    )


# ---------------------------------------------------------------------------
# current_command programmatic wrapper
# ---------------------------------------------------------------------------


@when("I invoke the plan current_command programmatic wrapper with valid project")
def step_invoke_current_command_wrapper(context):
    """Call current_command with a valid project."""
    from cleveragents.cli.commands.plan import current_command

    mock_project = MagicMock()
    mock_plan = MagicMock()
    mock_plan.name = "current-plan"
    container, plan_service, _ = _make_mock_container(project=mock_project)
    plan_service.get_current_plan.return_value = mock_plan

    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        context.current_result = current_command()

    context.plan_service_mock = plan_service
    context.mock_project = mock_project
    context.expected_plan = mock_plan


@then("the plan current_command programmatic wrapper should return the plan")
def step_assert_current_command_returns_plan(context):
    """Assert current_command returned the current plan."""
    assert context.current_result is context.expected_plan
    context.plan_service_mock.get_current_plan.assert_called_once_with(
        project=context.mock_project
    )


# ---------------------------------------------------------------------------
# list_command programmatic wrapper
# ---------------------------------------------------------------------------


@when("I invoke the plan list_command programmatic wrapper with valid project")
def step_invoke_list_command_wrapper(context):
    """Call list_command with a valid project."""
    from cleveragents.cli.commands.plan import list_command

    mock_project = MagicMock()
    mock_plan_a = MagicMock()
    mock_plan_a.name = "plan-a"
    mock_plan_b = MagicMock()
    mock_plan_b.name = "plan-b"
    container, plan_service, _ = _make_mock_container(project=mock_project)
    plan_service.list_plans.return_value = [mock_plan_a, mock_plan_b]

    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        context.list_result = list_command()

    context.plan_service_mock = plan_service


@then("the plan list_command programmatic wrapper should return the plans list")
def step_assert_list_command_returns_plans(context):
    """Assert list_command returned the plans list."""
    assert len(context.list_result) == 2
    context.plan_service_mock.list_plans.assert_called_once()


# ---------------------------------------------------------------------------
# cd_command programmatic wrapper
# ---------------------------------------------------------------------------


@when("I invoke the plan cd_command programmatic wrapper with valid project")
def step_invoke_cd_command_wrapper(context):
    """Call cd_command with a valid project."""
    from cleveragents.cli.commands.plan import cd_command

    mock_project = MagicMock()
    container, plan_service, _ = _make_mock_container(project=mock_project)

    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        cd_command(name="target-plan")

    context.plan_service_mock = plan_service
    context.mock_project = mock_project


@then("the plan cd_command programmatic wrapper should call switch_to_plan")
def step_assert_cd_command_calls_switch(context):
    """Assert cd_command called switch_to_plan with correct args."""
    context.plan_service_mock.switch_to_plan.assert_called_once_with(
        project=context.mock_project, name="target-plan"
    )


# ---------------------------------------------------------------------------
# continue_command programmatic wrapper
# ---------------------------------------------------------------------------


@when(
    "I invoke the plan continue_command programmatic wrapper with prompt and valid project"
)
def step_invoke_continue_command_wrapper_with_prompt(context):
    """Call continue_command with prompt and valid project."""
    from cleveragents.cli.commands.plan import continue_command

    mock_project = MagicMock()
    container, plan_service, _ = _make_mock_container(project=mock_project)

    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        continue_command(prompt="Add authentication")

    context.plan_service_mock = plan_service
    context.mock_project = mock_project


@then("the plan continue_command programmatic wrapper should call continue_plan")
def step_assert_continue_command_calls_continue(context):
    """Assert continue_command called continue_plan with the prompt."""
    context.plan_service_mock.continue_plan.assert_called_once_with(
        project=context.mock_project, prompt="Add authentication"
    )


@when(
    "I invoke the plan continue_command programmatic wrapper without prompt and no current plan"
)
def step_invoke_continue_command_wrapper_no_prompt_no_plan(context):
    """Call continue_command without prompt when no plan exists."""
    from cleveragents.cli.commands.plan import continue_command

    mock_project = MagicMock()
    container, plan_service, _ = _make_mock_container(project=mock_project)
    plan_service.get_current_plan.return_value = None

    context.call_exception = None
    with patch(
        "cleveragents.application.container.get_container", return_value=container
    ):
        try:
            continue_command(prompt=None)
        except Exception as exc:
            context.call_exception = exc


@then(
    "the plan continue_command programmatic wrapper should raise CleverAgentsError for no plan"
)
def step_assert_continue_command_raises_no_plan(context):
    """Assert continue_command raised CleverAgentsError for missing plan."""
    assert isinstance(context.call_exception, CleverAgentsError), (
        f"Expected CleverAgentsError but got: {type(context.call_exception)}"
    )
    assert "No current plan to continue" in str(context.call_exception)
