"""Step definitions for plan_cli_cancel_revert_coverage.feature.

Covers uncovered lines in cleveragents/cli/commands/plan.py:
- Line 1817: timestamps.created_at.strftime in list_plans rich table
- Lines 1821-1822: console.print(table) and CleverAgentsError catch
- Lines 1845-1855: cancel_plan command body
- Lines 1878-1888: revert_plan command body
"""

from __future__ import annotations

from datetime import datetime
from unittest.mock import MagicMock, patch

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

from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.domain.models.core.plan import (
    AutomationProfileProvenance,
    AutomationProfileRef,
    InvariantSource,
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanInvariant,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
    ProjectLink,
)

# Valid ULIDs for test plans
_ULID_CANCEL = "01ARZ3NDEKTSV4RRFFQ69G5FB1"
_ULID_REVERT = "01ARZ3NDEKTSV4RRFFQ69G5FB2"
_ULID_LIST_1 = "01ARZ3NDEKTSV4RRFFQ69G5FB3"
_ULID_LIST_2 = "01ARZ3NDEKTSV4RRFFQ69G5FB4"


def _make_plan(
    *,
    plan_id: str = _ULID_CANCEL,
    name: str = "local/test-plan",
    description: str = "Test plan for cancel-revert 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,
    timestamps: PlanTimestamps | None = None,
    reversion_count: int = 0,
    action_name: str = "local/test-action",
) -> Plan:
    """Build a real Plan object for testing."""
    if timestamps is None:
        timestamps = PlanTimestamps(
            created_at=datetime(2025, 6, 15, 10, 30, 0),
            updated_at=datetime(2025, 6, 15, 11, 0, 0),
        )
    return Plan(
        identity=PlanIdentity(plan_id=plan_id),
        namespaced_name=NamespacedName.parse(name),
        action_name=action_name,
        description=description,
        phase=phase,
        processing_state=processing_state,
        project_links=project_links or [],
        automation_profile=automation_profile,
        invariants=invariants or [],
        timestamps=timestamps,
        reversion_count=reversion_count,
        reusable=True,
        read_only=False,
    )


# ---------------------------------------------------------------------------
# Background steps
# ---------------------------------------------------------------------------


@given("a CLI runner for cancel-revert coverage")
def step_cli_runner(context):
    context.runner = CliRunner()


@given("a mocked lifecycle service for cancel-revert coverage")
def step_mocked_lifecycle_service(context):
    context.mock_service = MagicMock()
    patcher = patch(
        "cleveragents.cli.commands.plan._get_lifecycle_service",
        return_value=context.mock_service,
    )
    patcher.start()
    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(patcher.stop)


# ---------------------------------------------------------------------------
# Given steps — list rich format
# ---------------------------------------------------------------------------


@given("the service returns plans with project links and timestamps for rich list")
def step_service_plans_with_links_and_timestamps(context):
    plans = [
        _make_plan(
            plan_id=_ULID_LIST_1,
            name="local/plan-with-links",
            phase=PlanPhase.STRATEGIZE,
            processing_state=ProcessingState.QUEUED,
            project_links=[
                ProjectLink(project_name="proj-alpha"),
                ProjectLink(project_name="proj-beta"),
            ],
            timestamps=PlanTimestamps(
                created_at=datetime(2025, 3, 10, 14, 25, 0),
                updated_at=datetime(2025, 3, 10, 15, 0, 0),
            ),
        ),
    ]
    context.mock_service.list_plans.return_value = plans


@given("the service returns plans with an automation profile for rich list")
def step_service_plans_with_profile(context):
    plans = [
        _make_plan(
            plan_id=_ULID_LIST_1,
            name="local/profiled-plan",
            phase=PlanPhase.EXECUTE,
            processing_state=ProcessingState.PROCESSING,
            project_links=[ProjectLink(project_name="proj-one")],
            automation_profile=AutomationProfileRef(
                profile_name="trusted",
                provenance=AutomationProfileProvenance.ACTION,
            ),
            timestamps=PlanTimestamps(
                created_at=datetime(2025, 4, 1, 9, 0, 0),
                updated_at=datetime(2025, 4, 1, 10, 0, 0),
            ),
        ),
    ]
    context.mock_service.list_plans.return_value = plans


@given("the service returns a plan with three project links for rich list")
def step_service_plan_three_links(context):
    plans = [
        _make_plan(
            plan_id=_ULID_LIST_1,
            name="local/multi-project",
            phase=PlanPhase.STRATEGIZE,
            processing_state=ProcessingState.QUEUED,
            project_links=[
                ProjectLink(project_name="proj-a"),
                ProjectLink(project_name="proj-b"),
                ProjectLink(project_name="proj-c"),
            ],
            timestamps=PlanTimestamps(
                created_at=datetime(2025, 5, 20, 8, 15, 0),
                updated_at=datetime(2025, 5, 20, 9, 0, 0),
            ),
        ),
    ]
    context.mock_service.list_plans.return_value = plans


@given("the service returns plans with invariants for rich list")
def step_service_plans_with_invariants(context):
    plans = [
        _make_plan(
            plan_id=_ULID_LIST_1,
            name="local/invariant-plan",
            phase=PlanPhase.STRATEGIZE,
            processing_state=ProcessingState.QUEUED,
            project_links=[ProjectLink(project_name="proj-one")],
            invariants=[
                PlanInvariant(
                    text="No breaking changes", source=InvariantSource.ACTION
                ),
                PlanInvariant(
                    text="Keep backward compat", source=InvariantSource.PROJECT
                ),
            ],
            timestamps=PlanTimestamps(
                created_at=datetime(2025, 6, 1, 12, 0, 0),
                updated_at=datetime(2025, 6, 1, 13, 0, 0),
            ),
        ),
    ]
    context.mock_service.list_plans.return_value = plans


# ---------------------------------------------------------------------------
# Given steps — cancel_plan
# ---------------------------------------------------------------------------


@given("the service can cancel a plan for cancel-revert coverage")
def step_service_can_cancel(context):
    plan = _make_plan(
        plan_id=_ULID_CANCEL,
        name="local/cancel-target",
        phase=PlanPhase.STRATEGIZE,
        processing_state=ProcessingState.CANCELLED,
        project_links=[ProjectLink(project_name="proj-cancel")],
    )
    context.mock_service.cancel_plan.return_value = plan
    context._cancel_plan_id = _ULID_CANCEL


# ---------------------------------------------------------------------------
# Given steps — revert_plan
# ---------------------------------------------------------------------------


@given("the service can revert a plan for cancel-revert coverage")
def step_service_can_revert(context):
    plan = _make_plan(
        plan_id=_ULID_REVERT,
        name="local/revert-target",
        phase=PlanPhase.STRATEGIZE,
        processing_state=ProcessingState.QUEUED,
        project_links=[ProjectLink(project_name="proj-revert")],
        reversion_count=1,
    )
    context.mock_service.revert_plan.return_value = plan
    context._revert_plan_id = _ULID_REVERT


# ---------------------------------------------------------------------------
# When steps — list
# ---------------------------------------------------------------------------


@when("I invoke list in rich format for cancel-revert coverage")
def step_invoke_list_rich(context):
    context.result = context.runner.invoke(
        plan_app,
        ["list", "--format", "rich"],
    )


# ---------------------------------------------------------------------------
# When steps — cancel_plan
# ---------------------------------------------------------------------------


@when("I invoke cancel in rich format without reason for cancel-revert coverage")
def step_invoke_cancel_rich_no_reason(context):
    context.result = context.runner.invoke(
        plan_app,
        ["cancel", context._cancel_plan_id],
    )


@when(
    'I invoke cancel in rich format with reason "{reason}" for cancel-revert coverage'
)
def step_invoke_cancel_rich_with_reason(context, reason):
    context.result = context.runner.invoke(
        plan_app,
        ["cancel", context._cancel_plan_id, "--reason", reason],
    )


@when('I invoke cancel with format "{fmt}" and no reason for cancel-revert coverage')
def step_invoke_cancel_fmt_no_reason(context, fmt):
    context.result = context.runner.invoke(
        plan_app,
        ["cancel", context._cancel_plan_id, "--format", fmt],
    )


@when(
    'I invoke cancel with format "{fmt}" and reason "{reason}" for cancel-revert coverage'
)
def step_invoke_cancel_fmt_with_reason(context, fmt, reason):
    context.result = context.runner.invoke(
        plan_app,
        ["cancel", context._cancel_plan_id, "--format", fmt, "--reason", reason],
    )


# ---------------------------------------------------------------------------
# When steps — revert_plan
# ---------------------------------------------------------------------------


@when("I invoke revert in rich format with default phase for cancel-revert coverage")
def step_invoke_revert_rich_default(context):
    context.result = context.runner.invoke(
        plan_app,
        ["revert", context._revert_plan_id],
    )


@when(
    'I invoke revert in rich format with reason "{reason}" for cancel-revert coverage'
)
def step_invoke_revert_rich_with_reason(context, reason):
    context.result = context.runner.invoke(
        plan_app,
        ["revert", context._revert_plan_id, "--reason", reason],
    )


@when('I invoke revert with format "{fmt}" for cancel-revert coverage')
def step_invoke_revert_fmt(context, fmt):
    context.result = context.runner.invoke(
        plan_app,
        ["revert", context._revert_plan_id, "--format", fmt],
    )


@when('I invoke revert with invalid phase "{phase}" for cancel-revert coverage')
def step_invoke_revert_invalid_phase(context, phase):
    context.result = context.runner.invoke(
        plan_app,
        ["revert", context._revert_plan_id, "--to-phase", phase],
    )


# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------


def _output(context) -> str:
    return getattr(context.result, "output", "") if hasattr(context, "result") else ""


@then("the cancel-revert command should succeed")
def step_command_succeeds(context):
    assert context.result.exit_code == 0, (
        f"Expected exit code 0 but got {context.result.exit_code}.\n"
        f"Output: {_output(context)}"
    )


@then("the cancel-revert command should abort")
def step_command_aborts(context):
    assert context.result.exit_code != 0, (
        f"Expected non-zero exit code but got 0.\nOutput: {_output(context)}"
    )


@then('the cancel-revert output should contain "{text}"')
def step_output_contains(context, text):
    output = _output(context)
    assert text in output, f"Expected '{text}' in output:\n{output}"


@then('the cancel-revert output should not contain "{text}"')
def step_output_not_contains(context, text):
    output = _output(context)
    assert text not in output, f"Did not expect '{text}' in output:\n{output}"
