"""Step definitions for plan diff/artifacts CLI command coverage.

Covers:
- Lines 1815-1822: _get_apply_service()
- Lines 1845-1855: plan_diff command (success, PlanError, CleverAgentsError)
- Lines 1878-1888: plan_artifacts command (success, PlanError, CleverAgentsError)
- Branch L646->648: build command when actor_registry is None
- Branch L648->652: build command when testing_mode is False
- Branch L1130->1129: _print_lifecycle_plan when arguments_order has extra key
"""

from __future__ import annotations

import os
from io import StringIO
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

from behave import given, then, use_step_matcher, when
from behave.runner import Context
from rich.console import Console
from typer.testing import CliRunner
from ulid import ULID

from cleveragents.cli.commands.plan import app as plan_app
from cleveragents.core.exceptions import CleverAgentsError, PlanError

runner = CliRunner()

_PATCH_GET_APPLY = "cleveragents.cli.commands.plan._get_apply_service"
_PATCH_CONSOLE = "cleveragents.cli.commands.plan.console"


def _make_mock_apply_service(
    diff_return=None,
    diff_side_effect=None,
    artifacts_return=None,
    artifacts_side_effect=None,
):
    svc = MagicMock()
    if diff_side_effect:
        svc.diff.side_effect = diff_side_effect
    else:
        svc.diff.return_value = diff_return or ""
    if artifacts_side_effect:
        svc.artifacts.side_effect = artifacts_side_effect
    else:
        svc.artifacts.return_value = artifacts_return or ""
    return svc


# ---- GIVEN (parse matcher) ----

use_step_matcher("parse")


@given('plan_diff_artifacts the apply service returns diff output "{output}"')
def step_pda_g_diff_out_dq(context: Context, output: str) -> None:
    context.pda_mock_service = _make_mock_apply_service(diff_return=output)


@given("plan_diff_artifacts the apply service returns diff output '{output}'")
def step_pda_g_diff_out_sq(context: Context, output: str) -> None:
    context.pda_mock_service = _make_mock_apply_service(diff_return=output)


@given('plan_diff_artifacts the apply service diff raises a PlanError "{msg}"')
def step_pda_g_diff_planerr(context: Context, msg: str) -> None:
    context.pda_mock_service = _make_mock_apply_service(
        diff_side_effect=PlanError(msg),
    )


@given('plan_diff_artifacts the apply service diff raises a CleverAgentsError "{msg}"')
def step_pda_g_diff_caerr(context: Context, msg: str) -> None:
    context.pda_mock_service = _make_mock_apply_service(
        diff_side_effect=CleverAgentsError(msg),
    )


@given('plan_diff_artifacts the apply service returns artifacts output "{output}"')
def step_pda_g_art_out_dq(context: Context, output: str) -> None:
    context.pda_mock_service = _make_mock_apply_service(artifacts_return=output)


@given("plan_diff_artifacts the apply service returns artifacts output '{output}'")
def step_pda_g_art_out_sq(context: Context, output: str) -> None:
    context.pda_mock_service = _make_mock_apply_service(artifacts_return=output)


@given('plan_diff_artifacts the apply service artifacts raises a PlanError "{msg}"')
def step_pda_g_art_planerr(context: Context, msg: str) -> None:
    context.pda_mock_service = _make_mock_apply_service(
        artifacts_side_effect=PlanError(msg),
    )


@given(
    'plan_diff_artifacts the apply service artifacts raises a CleverAgentsError "{msg}"'
)
def step_pda_g_art_caerr(context: Context, msg: str) -> None:
    context.pda_mock_service = _make_mock_apply_service(
        artifacts_side_effect=CleverAgentsError(msg),
    )


@given("plan_diff_artifacts a mock lifecycle service")
def step_pda_g_mock_lifecycle(context: Context) -> None:
    context.pda_mock_lifecycle = MagicMock()


@given("plan_diff_artifacts a container without actor_registry")
def step_pda_g_container_no_ar(context: Context) -> None:
    container = MagicMock()
    del container.actor_registry

    plan_service = MagicMock()
    plan_service.build_plan.return_value = [
        SimpleNamespace(file_path="a.py", change_type="create")
    ]
    container.plan_service.return_value = plan_service
    container.actor_service.return_value = MagicMock()

    context.pda_container = container
    context.pda_project = SimpleNamespace(name="test-project")
    context.pda_plan_service = plan_service


@given("plan_diff_artifacts a container with actor_registry but testing mode disabled")
def step_pda_g_container_no_testing(context: Context) -> None:
    container = MagicMock()
    actor_registry = MagicMock()
    container.actor_registry.return_value = actor_registry

    plan_service = MagicMock()
    plan_service.build_plan.return_value = [
        SimpleNamespace(file_path="b.py", change_type="modify")
    ]
    container.plan_service.return_value = plan_service
    actor_service = MagicMock()
    container.actor_service.return_value = actor_service

    context.pda_container = container
    context.pda_project = SimpleNamespace(name="test-project-2")
    context.pda_plan_service = plan_service
    context.pda_actor_service = actor_service
    context.pda_actor_registry = actor_registry


@given("plan_diff_artifacts a lifecycle plan with arguments_order having an extra key")
def step_pda_g_plan_extra_arg(context: Context) -> None:
    from cleveragents.domain.models.core.plan import (
        NamespacedName,
        Plan,
        PlanIdentity,
        PlanPhase,
        ProcessingState,
    )

    valid_ulid = str(ULID())
    plan = Plan(
        identity=PlanIdentity(plan_id=valid_ulid),
        namespaced_name=NamespacedName.parse("test/args-plan"),
        description="Test plan for arguments_order branch",
        action_name="local/test-action",
        phase=PlanPhase.STRATEGIZE,
        processing_state=ProcessingState.QUEUED,
        arguments={"keep_me": "value1"},
        arguments_order=["keep_me", "ghost_key"],
    )
    context.pda_plan = plan


# ---- WHEN (regex matcher for parameterized steps) ----

use_step_matcher("re")


@when(
    r'plan_diff_artifacts I run the diff subcommand for plan "(?P<plan_id>[^"]+)" using format "(?P<fmt>[^"]+)"'
)
def step_pda_w_diff_fmt(context: Context, plan_id: str, fmt: str) -> None:
    with patch(_PATCH_GET_APPLY, return_value=context.pda_mock_service):
        context.pda_result = runner.invoke(plan_app, ["diff", plan_id, "--format", fmt])


@when(r'plan_diff_artifacts I run the diff subcommand for plan "(?P<plan_id>[^"]+)"')
def step_pda_w_diff(context: Context, plan_id: str) -> None:
    with patch(_PATCH_GET_APPLY, return_value=context.pda_mock_service):
        context.pda_result = runner.invoke(plan_app, ["diff", plan_id])


@when(
    r'plan_diff_artifacts I run the artifacts subcommand for plan "(?P<plan_id>[^"]+)" using format "(?P<fmt>[^"]+)"'
)
def step_pda_w_artifacts_fmt(context: Context, plan_id: str, fmt: str) -> None:
    with patch(_PATCH_GET_APPLY, return_value=context.pda_mock_service):
        context.pda_result = runner.invoke(
            plan_app, ["artifacts", plan_id, "--format", fmt]
        )


@when(
    r'plan_diff_artifacts I run the artifacts subcommand for plan "(?P<plan_id>[^"]+)"'
)
def step_pda_w_artifacts(context: Context, plan_id: str) -> None:
    with patch(_PATCH_GET_APPLY, return_value=context.pda_mock_service):
        context.pda_result = runner.invoke(plan_app, ["artifacts", plan_id])


# Switch back to parse matcher for non-parameterized steps
use_step_matcher("parse")


@when("plan_diff_artifacts I call _get_apply_service")
def step_pda_w_get_apply(context: Context) -> None:
    from cleveragents.cli.commands.plan import _get_apply_service

    mock_pas_instance = MagicMock()
    with (
        patch(
            "cleveragents.cli.commands.plan._get_lifecycle_service",
            return_value=context.pda_mock_lifecycle,
        ),
        patch(
            "cleveragents.application.services.plan_apply_service.PlanApplyService",
            return_value=mock_pas_instance,
        ),
    ):
        context.pda_apply_service_result = _get_apply_service()


@when("plan_diff_artifacts I invoke the build command")
def step_pda_w_build(context: Context) -> None:
    with (
        patch(
            "cleveragents.application.container.get_container",
            return_value=context.pda_container,
        ),
        patch(
            "cleveragents.cli.commands.plan._get_current_project",
            return_value=context.pda_project,
        ),
        patch.dict(os.environ, {"CLEVERAGENTS_TESTING_USE_MOCK_AI": ""}, clear=False),
    ):
        context.pda_result = runner.invoke(plan_app, ["build"])


@when("plan_diff_artifacts I print the lifecycle plan")
def step_pda_w_print_plan(context: Context) -> None:
    from cleveragents.cli.commands.plan import _print_lifecycle_plan

    output = StringIO()
    test_console = Console(file=output, force_terminal=False, width=120)
    with patch(_PATCH_CONSOLE, test_console):
        _print_lifecycle_plan(context.pda_plan, title="Test Plan")
    context.pda_console_output = output.getvalue()


# ---- THEN ----


@then("plan_diff_artifacts the exit code should be {code:d}")
def step_pda_t_exit_code(context: Context, code: int) -> None:
    assert context.pda_result.exit_code == code, (
        f"Expected exit code {code}, got {context.pda_result.exit_code}. "
        f"Output: {context.pda_result.output}"
    )


@then('plan_diff_artifacts the output should contain "{text}"')
def step_pda_t_output_contains(context: Context, text: str) -> None:
    full = context.pda_result.output
    assert text in full, f"Expected '{text}' in output. Got:\n{full}"


@then("plan_diff_artifacts the command should be aborted")
def step_pda_t_aborted(context: Context) -> None:
    assert context.pda_result.exit_code != 0, (
        f"Expected non-zero exit, got {context.pda_result.exit_code}. "
        f"Output: {context.pda_result.output}"
    )


@then("plan_diff_artifacts a PlanApplyService should be returned")
def step_pda_t_apply_svc(context: Context) -> None:
    assert context.pda_apply_service_result is not None


@then("plan_diff_artifacts the build should complete without actor registry calls")
def step_pda_t_no_ar(context: Context) -> None:
    assert not hasattr(context.pda_container, "actor_registry")


@then("plan_diff_artifacts ensure_default_mock_actor should not be called")
def step_pda_t_no_mock_actor(context: Context) -> None:
    context.pda_actor_service.ensure_default_mock_actor.assert_not_called()


@then("plan_diff_artifacts the output should contain the valid argument")
def step_pda_t_valid_arg(context: Context) -> None:
    assert "keep_me" in context.pda_console_output, (
        f"Expected 'keep_me' in: {context.pda_console_output}"
    )
    assert "value1" in context.pda_console_output, (
        f"Expected 'value1' in: {context.pda_console_output}"
    )


@then("plan_diff_artifacts the output should not contain the missing key value")
def step_pda_t_no_ghost(context: Context) -> None:
    assert "ghost_key =" not in context.pda_console_output, (
        f"Did not expect 'ghost_key =' in: {context.pda_console_output}"
    )
