"""Step definitions for plan_cli_legacy_r2.feature.

Targets remaining partial branches in
``cleveragents.cli.commands.plan`` (plan.py) - round 2, split 3 of 3.

Covers:
- Legacy wrappers: no-project branch, continue_command prompt/no-prompt
- ``_resolve_active_plan_id``: no active plans, service error
- ``build_command``: None changes -> empty list
- ``list_command``: None plans -> empty list

All step text uses the ``r2plan-`` prefix to avoid collisions.
"""

from __future__ import annotations

import os
import tempfile
import warnings
from datetime import datetime
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock, patch

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

from cleveragents.cli.commands.plan import (
    _resolve_active_plan_id,
)
from cleveragents.core.exceptions import CleverAgentsError
from cleveragents.domain.models.core.plan import (
    AutomationProfileRef,
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanInvariant,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
    ProjectLink,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_ULID_BASE = "01ARZ3NDEKTSV4RRFFQ69G5F"
_runner = CliRunner()


def _ulid(suffix: str = "A1") -> str:
    """Return a valid 26-char ULID for tests.

    ULIDs use Crockford's Base32 (0-9, A-H, J-K, M-N, P-T, V-Z; no I/L/O/U).
    """
    # Map potentially invalid chars to valid Crockford Base32
    cleaned = (
        suffix.replace("I", "J").replace("L", "K").replace("O", "P").replace("U", "V")
    )
    base = _ULID_BASE + cleaned
    return base[:26]


def _make_plan(
    *,
    plan_id: str | None = None,
    name: str = "local/r2-plan",
    description: str = "Test plan for r2 coverage",
    phase: PlanPhase = PlanPhase.STRATEGIZE,
    processing_state: ProcessingState = ProcessingState.QUEUED,
    project_links: list[ProjectLink] | None = None,
    automation_profile: AutomationProfileRef | None = None,
    invariants: list[PlanInvariant] | None = None,
    validation_summary: dict[str, Any] | None = None,
    error_message: str | None = None,
    last_completed_step: int = -1,
    last_checkpoint_id: str | None = None,
    definition_of_done: str | None = None,
    arguments: dict[str, Any] | None = None,
    arguments_order: list[str] | None = None,
    estimation_actor: str | None = None,
    invariant_actor: str | None = None,
    timestamps: PlanTimestamps | None = None,
    action_name: str = "local/test-action",
) -> Plan:
    if timestamps is None:
        timestamps = PlanTimestamps(
            created_at=datetime.now(),
            updated_at=datetime.now(),
        )
    return Plan(
        identity=PlanIdentity(plan_id=plan_id or _ulid("A1")),
        namespaced_name=NamespacedName.parse(name),
        action_name=action_name,
        description=description,
        definition_of_done=definition_of_done,
        phase=phase,
        processing_state=processing_state,
        strategy_actor=None,
        execution_actor=None,
        project_links=project_links or [],
        automation_profile=automation_profile,
        invariants=invariants or [],
        validation_summary=validation_summary,
        error_message=error_message,
        last_completed_step=last_completed_step,
        last_checkpoint_id=last_checkpoint_id,
        arguments=arguments or {},
        arguments_order=arguments_order or [],
        estimation_actor=estimation_actor,
        invariant_actor=invariant_actor,
        timestamps=timestamps,
        created_by=None,
        reusable=True,
        read_only=False,
    )


def _mock_container(project_exists: bool = True) -> MagicMock:
    """Build a mock container for legacy wrapper tests."""
    container = MagicMock()
    project_service = MagicMock()
    plan_service = MagicMock()
    if project_exists:
        project_service.get_current_project.return_value = SimpleNamespace(name="proj")
    else:
        project_service.get_current_project.return_value = None
    container.project_service.return_value = project_service
    container.plan_service.return_value = plan_service
    return container


# ---------------------------------------------------------------------------
# When steps - legacy programmatic wrappers (no project)
# ---------------------------------------------------------------------------


@when("r2plan-I call tell_command with no project")
def step_tell_no_project(context: Any) -> None:
    container = _mock_container(project_exists=False)
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        try:
            from cleveragents.cli.commands.plan import tell_command

            tell_command("do stuff")
            context.r2_error = None
        except CleverAgentsError as exc:
            context.r2_error = exc


@when("r2plan-I call build_command with no project")
def step_build_no_project(context: Any) -> None:
    container = _mock_container(project_exists=False)
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        try:
            from cleveragents.cli.commands.plan import build_command

            build_command()
            context.r2_error = None
        except CleverAgentsError as exc:
            context.r2_error = exc


@when("r2plan-I call apply_command with no project")
def step_apply_no_project(context: Any) -> None:
    container = _mock_container(project_exists=False)
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        try:
            from cleveragents.cli.commands.plan import apply_command

            apply_command()
            context.r2_error = None
        except CleverAgentsError as exc:
            context.r2_error = exc


@when("r2plan-I call new_command with no project")
def step_new_no_project(context: Any) -> None:
    container = _mock_container(project_exists=False)
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        try:
            from cleveragents.cli.commands.plan import new_command

            new_command("test")
            context.r2_error = None
        except CleverAgentsError as exc:
            context.r2_error = exc


@when("r2plan-I call current_command with no project")
def step_current_no_project(context: Any) -> None:
    container = _mock_container(project_exists=False)
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        try:
            from cleveragents.cli.commands.plan import current_command

            current_command()
            context.r2_error = None
        except CleverAgentsError as exc:
            context.r2_error = exc


@when("r2plan-I call list_command with no project")
def step_list_no_project(context: Any) -> None:
    container = _mock_container(project_exists=False)
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        try:
            from cleveragents.cli.commands.plan import list_command

            list_command()
            context.r2_error = None
        except CleverAgentsError as exc:
            context.r2_error = exc


@when("r2plan-I call cd_command with no project")
def step_cd_no_project(context: Any) -> None:
    container = _mock_container(project_exists=False)
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        try:
            from cleveragents.cli.commands.plan import cd_command

            cd_command("some-plan")
            context.r2_error = None
        except CleverAgentsError as exc:
            context.r2_error = exc


@when("r2plan-I call continue_command with no project")
def step_continue_no_project(context: Any) -> None:
    container = _mock_container(project_exists=False)
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        try:
            from cleveragents.cli.commands.plan import continue_command

            continue_command()
            context.r2_error = None
        except CleverAgentsError as exc:
            context.r2_error = exc


# ---------------------------------------------------------------------------
# When steps - continue_command with prompt / no-prompt
# ---------------------------------------------------------------------------


@when('r2plan-I call continue_command with prompt "{prompt}"')
def step_continue_with_prompt(context: Any, prompt: str) -> None:
    container = _mock_container(project_exists=True)
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        from cleveragents.cli.commands.plan import continue_command

        continue_command(prompt=prompt)
        context.r2_plan_service = container.plan_service()


@when("r2plan-I call continue_command with no prompt and no current plan")
def step_continue_no_prompt_no_plan(context: Any) -> None:
    container = _mock_container(project_exists=True)
    plan_service = container.plan_service()
    plan_service.get_current_plan.return_value = None
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        try:
            from cleveragents.cli.commands.plan import continue_command

            continue_command(prompt=None)
            context.r2_error = None
        except CleverAgentsError as exc:
            context.r2_error = exc


# ---------------------------------------------------------------------------
# When steps - build_command returns None / list_command returns None
# ---------------------------------------------------------------------------


@when("r2plan-I call build_command with build returning None")
def step_build_returns_none(context: Any) -> None:
    container = _mock_container(project_exists=True)
    plan_service = container.plan_service()
    plan_service.build_plan.return_value = None
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        from cleveragents.cli.commands.plan import build_command

        context.r2_build_result = build_command()


@when("r2plan-I call list_command with list returning None")
def step_list_returns_none(context: Any) -> None:
    container = _mock_container(project_exists=True)
    plan_service = container.plan_service()
    plan_service.list_plans.return_value = None
    with (
        patch(
            "cleveragents.application.container.get_container", return_value=container
        ),
        warnings.catch_warnings(),
    ):
        warnings.simplefilter("ignore", DeprecationWarning)
        from cleveragents.cli.commands.plan import list_command

        context.r2_list_result = list_command()


# ---------------------------------------------------------------------------
# When steps - _resolve_active_plan_id
# ---------------------------------------------------------------------------


@when("r2plan-I call _resolve_active_plan_id with no active plans")
def step_resolve_no_active(context: Any) -> None:
    mock_svc = MagicMock()
    # All plans are terminal
    p = _make_plan(phase=PlanPhase.APPLY, processing_state=ProcessingState.APPLIED)
    mock_svc.list_plans.return_value = [p]
    with patch(
        "cleveragents.cli.commands.plan._get_lifecycle_service",
        return_value=mock_svc,
    ):
        try:
            _resolve_active_plan_id()
            context.r2_error = None
        except typer.Abort:
            context.r2_error = typer.Abort()


@when("r2plan-I call _resolve_active_plan_id with service error")
def step_resolve_service_error(context: Any) -> None:
    with patch(
        "cleveragents.cli.commands.plan._get_lifecycle_service",
        side_effect=CleverAgentsError("service unavailable"),
    ):
        try:
            _resolve_active_plan_id()
            context.r2_error = None
        except typer.Abort:
            context.r2_error = typer.Abort()


@when(
    "r2plan-I call _resolve_active_plan_id with explicit DB env override and no active plans"
)
def step_resolve_skips_home_fallback_when_db_override(context: Any) -> None:
    mock_svc = MagicMock()
    terminal_plan = _make_plan(
        phase=PlanPhase.APPLY,
        processing_state=ProcessingState.APPLIED,
    )
    mock_svc.list_plans.return_value = [terminal_plan]

    with tempfile.TemporaryDirectory(prefix="r2plan-db-override-") as tmp:
        home_dir = os.path.join(tmp, "home")
        os.makedirs(home_dir, exist_ok=True)

        mock_uow = MagicMock()
        with (
            patch(
                "cleveragents.cli.commands.plan._get_lifecycle_service",
                return_value=mock_svc,
            ),
            patch(
                "cleveragents.infrastructure.database.unit_of_work.UnitOfWork",
                return_value=mock_uow,
            ),
            patch.dict(
                os.environ,
                {
                    "CLEVERAGENTS_HOME": home_dir,
                    "CLEVERAGENTS_DATABASE_URL": "sqlite:////tmp/explicit-test.db",
                },
                clear=False,
            ),
        ):
            try:
                _resolve_active_plan_id()
                context.r2_error = None
            except typer.Abort:
                context.r2_error = typer.Abort()

        context.r2_home_fallback_attempted = mock_uow.called


@when("r2plan-I call _resolve_active_plan_id with unexpected fallback error")
def step_resolve_unexpected_fallback_error(context: Any) -> None:
    mock_svc = MagicMock()
    terminal_plan = _make_plan(
        phase=PlanPhase.APPLY,
        processing_state=ProcessingState.APPLIED,
    )
    mock_svc.list_plans.return_value = [terminal_plan]

    with tempfile.TemporaryDirectory(prefix="r2plan-fallback-error-") as tmp:
        home_dir = os.path.join(tmp, "home")
        workspace_dir = os.path.join(tmp, "workspace")
        os.makedirs(home_dir, exist_ok=True)
        os.makedirs(workspace_dir, exist_ok=True)

        original_cwd = os.getcwd()
        os.chdir(workspace_dir)
        try:
            with (
                patch(
                    "cleveragents.cli.commands.plan._get_lifecycle_service",
                    return_value=mock_svc,
                ),
                patch(
                    "cleveragents.infrastructure.database.unit_of_work.UnitOfWork",
                    side_effect=RuntimeError("unexpected fallback crash"),
                ),
                patch.dict(
                    os.environ,
                    {
                        "CLEVERAGENTS_HOME": home_dir,
                        "CLEVERAGENTS_DATABASE_URL": "",
                        "CLEVERAGENTS_TEST_DATABASE_URL": "",
                    },
                    clear=False,
                ),
            ):
                try:
                    _resolve_active_plan_id()
                    context.r2_error = None
                except typer.Abort:
                    context.r2_error = typer.Abort()
                except RuntimeError as exc:
                    context.r2_error = exc
        finally:
            os.chdir(original_cwd)


# ---------------------------------------------------------------------------
# Then steps - legacy wrapper assertions
# ---------------------------------------------------------------------------


@then('r2plan-a CleverAgentsError should be raised with message "{fragment}"')
def step_agents_error(context: Any, fragment: str) -> None:
    assert context.r2_error is not None, "Expected CleverAgentsError but none raised"
    assert isinstance(context.r2_error, CleverAgentsError), (
        f"Expected CleverAgentsError, got {type(context.r2_error).__name__}"
    )
    assert fragment in context.r2_error.message, (
        f"Expected '{fragment}' in '{context.r2_error.message}'"
    )


@then("r2plan-the continue_plan service method should be called")
def step_continue_called(context: Any) -> None:
    context.r2_plan_service.continue_plan.assert_called_once()


@then("r2plan-the build result should be an empty list")
def step_build_empty(context: Any) -> None:
    assert context.r2_build_result == [], f"Expected [], got {context.r2_build_result}"


@then("r2plan-the list result should be an empty list")
def step_list_empty(context: Any) -> None:
    assert context.r2_list_result == [], f"Expected [], got {context.r2_list_result}"


@then("r2plan-a typer Abort should be raised")
def step_typer_abort(context: Any) -> None:
    assert context.r2_error is not None, "Expected typer.Abort but none raised"
    assert isinstance(context.r2_error, typer.Abort), (
        f"Expected typer.Abort, got {type(context.r2_error).__name__}"
    )


@then("r2plan-the home DB fallback should not be attempted")
def step_home_fallback_not_attempted(context: Any) -> None:
    attempted = getattr(context, "r2_home_fallback_attempted", None)
    assert attempted is False, "Expected fallback UnitOfWork not to be called"


@then("r2plan-a RuntimeError should be raised")
def step_runtime_error(context: Any) -> None:
    assert context.r2_error is not None, "Expected RuntimeError but none raised"
    assert isinstance(context.r2_error, RuntimeError), (
        f"Expected RuntimeError, got {type(context.r2_error).__name__}"
    )
