"""Step definitions for plan_cli_coverage_r3.feature.

Targets remaining uncovered lines in cleveragents/cli/commands/plan.py:
- Lines 104, 107-108: validate_namespaced_actor invalid / valid
- Line 184: _plan_spec_dict with execution_environment
- Line 790: build command PlanError handler
- Lines 799-858: apply command (v3 and legacy paths)
- Lines 1205-1224: continue_plan command body
- Lines 1529-1542, 1631-1633, 1751-1752: use_action with actor overrides
- Lines 1789-1792, 1794: use_action with execution_env_priority
- Lines 2740-2741, 2743: plan_diff command body
- Lines 3047-3048, 3051: correct_decision error handlers
"""

from __future__ import annotations

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

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

from cleveragents.cli.commands.plan import (
    _plan_spec_dict,
    validate_namespaced_actor,
)
from cleveragents.cli.commands.plan import (
    app as plan_app,
)
from cleveragents.core.exceptions import (
    CleverAgentsError,
    PlanError,
    ValidationError,
)
from cleveragents.domain.models.core.plan import (
    ExecutionEnvPriority,
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
)

# ──────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────

_ULID_A = "01ARZ3NDEKTSV4RRFFQ69G5FAV"

_PATCH_CONTAINER = "cleveragents.application.container.get_container"
_PATCH_GET_LIFECYCLE = "cleveragents.cli.commands.plan._get_lifecycle_service"
_PATCH_GET_APPLY_SVC = "cleveragents.cli.commands.plan._get_apply_service"
_PATCH_GET_PROJECT = "cleveragents.cli.commands.plan._get_current_project"
_PATCH_NOTIFY_FACADE = "cleveragents.cli.commands.plan._notify_facade"


# ──────────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────────


def _make_plan(
    *,
    plan_id: str = _ULID_A,
    name: str = "local/test-plan",
    phase: PlanPhase = PlanPhase.EXECUTE,
    processing_state: ProcessingState = ProcessingState.COMPLETE,
    read_only: bool = False,
    execution_environment: str | None = None,
    execution_env_priority: ExecutionEnvPriority | None = None,
) -> Plan:
    """Build a real Plan object for testing."""
    plan = Plan(
        identity=PlanIdentity(plan_id=plan_id),
        namespaced_name=NamespacedName.parse(name),
        action_name="local/test-action",
        description="Coverage test plan",
        phase=phase,
        processing_state=processing_state,
        project_links=[],
        timestamps=PlanTimestamps(
            created_at=datetime(2025, 6, 15, 10, 0, 0),
            updated_at=datetime(2025, 6, 15, 11, 0, 0),
        ),
        reusable=True,
        read_only=read_only,
    )
    if execution_environment is not None:
        plan.execution_environment = execution_environment
    if execution_env_priority is not None:
        plan.execution_env_priority = execution_env_priority
    return plan


def _start_patch(context: Context, target: str, **kwargs) -> MagicMock:
    """Start a mock patch and register cleanup on context."""
    patcher = patch(target, **kwargs)
    mock_obj = patcher.start()
    context.add_cleanup(patcher.stop)
    return mock_obj


# ──────────────────────────────────────────────────────────────
# Given — CLI runner
# ──────────────────────────────────────────────────────────────


@given("a plcov3 CLI runner")
def step_plcov3_cli_runner(context: Context) -> None:
    context.plcov3_runner = CliRunner()
    context.plcov3_result = None
    context.plcov3_error = None
    context.plcov3_return_value = None


# ══════════════════════════════════════════════════════════════
# validate_namespaced_actor (lines 104, 107-108)
# ══════════════════════════════════════════════════════════════


@when('I plcov3 call validate_namespaced_actor with value "{value}" and flag "{flag}"')
def step_plcov3_call_validate_actor(context: Context, value: str, flag: str) -> None:
    try:
        context.plcov3_return_value = validate_namespaced_actor(value, flag)
    except Exception as exc:
        context.plcov3_error = exc


@then(
    'the plcov3 error should be a ValidationError with message containing "{fragment}"'
)
def step_plcov3_error_is_validation(context: Context, fragment: str) -> None:
    assert context.plcov3_error is not None, "Expected an error but none was raised"
    assert isinstance(context.plcov3_error, ValidationError), (
        f"Expected ValidationError, got {type(context.plcov3_error).__name__}"
    )
    assert fragment in str(context.plcov3_error), (
        f"Expected '{fragment}' in error message, got: {context.plcov3_error}"
    )


@then('the plcov3 result should equal "{expected}"')
def step_plcov3_result_equals(context: Context, expected: str) -> None:
    assert context.plcov3_error is None, f"Unexpected error: {context.plcov3_error}"
    assert context.plcov3_return_value == expected, (
        f"Expected '{expected}', got '{context.plcov3_return_value}'"
    )


# ══════════════════════════════════════════════════════════════
# _plan_spec_dict with execution_environment (line 184)
# ══════════════════════════════════════════════════════════════


@given('a plcov3 v3 plan with execution_environment "{env}"')
def step_plcov3_plan_with_exec_env(context: Context, env: str) -> None:
    context.plcov3_plan = _make_plan(
        execution_environment=env,
        execution_env_priority=ExecutionEnvPriority.FALLBACK,
    )


@when("I plcov3 call _plan_spec_dict")
def step_plcov3_call_plan_spec_dict(context: Context) -> None:
    context.plcov3_spec_dict = _plan_spec_dict(context.plcov3_plan)


@then('the plcov3 spec dict should contain key "{key}" with value "{value}"')
def step_plcov3_spec_dict_contains(context: Context, key: str, value: str) -> None:
    assert key in context.plcov3_spec_dict, (
        f"Key '{key}' not in spec dict: {list(context.plcov3_spec_dict.keys())}"
    )
    assert str(context.plcov3_spec_dict[key]) == value, (
        f"Expected '{value}', got '{context.plcov3_spec_dict[key]}'"
    )


# ══════════════════════════════════════════════════════════════
# build command PlanError handler (line 790)
# ══════════════════════════════════════════════════════════════


@given("a plcov3 mocked build environment that raises PlanError")
def step_plcov3_mock_build_plan_error(context: Context) -> None:
    container = MagicMock()
    plan_service = MagicMock()
    plan_service.build_plan.side_effect = PlanError("Build failed internally")
    container.plan_service.return_value = plan_service
    container.actor_registry.return_value = MagicMock()

    project = MagicMock()
    project.name = "test-project"

    _start_patch(context, _PATCH_CONTAINER, return_value=container)
    _start_patch(context, _PATCH_GET_PROJECT, return_value=project)


@when("I plcov3 invoke the build command")
def step_plcov3_invoke_build(context: Context) -> None:
    context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["build"])


# ══════════════════════════════════════════════════════════════
# apply command — v3 lifecycle path (lines 799-824)
# ══════════════════════════════════════════════════════════════


@given("a plcov3 mocked lifecycle service for apply happy path")
def step_plcov3_mock_apply_v3(context: Context) -> None:
    service = MagicMock()
    # Walk through the full apply flow
    pre_plan = _make_plan(
        phase=PlanPhase.EXECUTE,
        processing_state=ProcessingState.COMPLETE,
    )
    apply_queued = _make_plan(
        phase=PlanPhase.APPLY,
        processing_state=ProcessingState.QUEUED,
    )
    apply_processing = _make_plan(
        phase=PlanPhase.APPLY,
        processing_state=ProcessingState.PROCESSING,
    )
    apply_applied = _make_plan(
        phase=PlanPhase.APPLY,
        processing_state=ProcessingState.APPLIED,
    )
    service.get_plan.side_effect = [
        pre_plan,
        apply_queued,
        apply_processing,
        apply_applied,
    ]
    service.apply_plan.return_value = None
    service.start_apply.return_value = None
    service.complete_apply.return_value = None

    _start_patch(context, _PATCH_GET_LIFECYCLE, return_value=service)
    _start_patch(context, _PATCH_NOTIFY_FACADE)


@when('I plcov3 invoke apply with plan_id "{plan_id}"')
def step_plcov3_invoke_apply_with_id(context: Context, plan_id: str) -> None:
    context.plcov3_result = context.plcov3_runner.invoke(
        plan_app, ["apply", plan_id, "--yes"]
    )


# ══════════════════════════════════════════════════════════════
# apply command — legacy path with changes (lines 826-858)
# ══════════════════════════════════════════════════════════════


def _make_mock_change(file_path: str = "src/main.py", operation: str = "modify"):
    """Create a mock Change object."""
    change = MagicMock()
    change.file_path = file_path
    change.operation = operation
    return change


@given("a plcov3 mocked lifecycle service with no eligible apply plans")
def step_plcov3_mock_lifecycle_no_eligible_apply(context: Context) -> None:
    """Mock lifecycle service that returns no plans eligible for apply."""
    service = MagicMock()
    service.list_plans.return_value = []
    _start_patch(context, _PATCH_GET_LIFECYCLE, return_value=service)


@given("a plcov3 mocked legacy apply environment with pending changes")
def step_plcov3_mock_legacy_apply_with_changes(context: Context) -> None:
    container = MagicMock()
    plan_service = MagicMock()
    changes = [_make_mock_change("src/main.py", "modify")]
    plan_service.get_pending_changes.return_value = changes
    plan_service.apply_changes.return_value = 1
    container.plan_service.return_value = plan_service

    project = MagicMock()
    project.name = "test-project"

    _start_patch(context, _PATCH_CONTAINER, return_value=container)
    _start_patch(context, _PATCH_GET_PROJECT, return_value=project)

    # Set testing env var so confirmation is skipped
    old_val = os.environ.get("CLEVERAGENTS_TESTING_USE_MOCK_AI", "")
    os.environ["CLEVERAGENTS_TESTING_USE_MOCK_AI"] = "true"
    context.add_cleanup(
        lambda: (
            os.environ.__setitem__("CLEVERAGENTS_TESTING_USE_MOCK_AI", old_val)
            if old_val
            else os.environ.pop("CLEVERAGENTS_TESTING_USE_MOCK_AI", None)
        )
    )


@given("a plcov3 mocked legacy apply environment with no pending changes")
def step_plcov3_mock_legacy_apply_no_changes(context: Context) -> None:
    container = MagicMock()
    plan_service = MagicMock()
    plan_service.get_pending_changes.return_value = []
    container.plan_service.return_value = plan_service

    project = MagicMock()
    project.name = "test-project"

    _start_patch(context, _PATCH_CONTAINER, return_value=container)
    _start_patch(context, _PATCH_GET_PROJECT, return_value=project)


@when("I plcov3 invoke apply without plan_id")
def step_plcov3_invoke_apply_no_id(context: Context) -> None:
    context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["apply"])


# ══════════════════════════════════════════════════════════════
# continue_plan command (lines 1205-1224)
# ══════════════════════════════════════════════════════════════


@given("a plcov3 mocked continue environment")
def step_plcov3_mock_continue(context: Context) -> None:
    container = MagicMock()
    plan_service = MagicMock()
    plan_service.continue_plan.return_value = None
    container.plan_service.return_value = plan_service

    project = MagicMock()
    project.name = "test-project"

    _start_patch(context, _PATCH_CONTAINER, return_value=container)
    _start_patch(context, _PATCH_GET_PROJECT, return_value=project)


@given("a plcov3 mocked continue environment with current plan")
def step_plcov3_mock_continue_with_plan(context: Context) -> None:
    container = MagicMock()
    plan_service = MagicMock()
    current_plan = MagicMock()
    current_plan.name = "my-existing-plan"
    plan_service.get_current_plan.return_value = current_plan
    container.plan_service.return_value = plan_service

    project = MagicMock()
    project.name = "test-project"

    _start_patch(context, _PATCH_CONTAINER, return_value=container)
    _start_patch(context, _PATCH_GET_PROJECT, return_value=project)


@when('I plcov3 invoke continue with prompt "{prompt}"')
def step_plcov3_invoke_continue_with_prompt(context: Context, prompt: str) -> None:
    context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["continue", prompt])


@when("I plcov3 invoke continue without prompt")
def step_plcov3_invoke_continue_no_prompt(context: Context) -> None:
    context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["continue"])


# ══════════════════════════════════════════════════════════════
# use_action with overrides (lines 1529-1542, 1631-1633, 1751-1752,
#                             1789-1792, 1794)
# ══════════════════════════════════════════════════════════════


def _make_mock_action():
    """Create a mock Action with a namespaced_name."""
    action = MagicMock()
    action.namespaced_name = NamespacedName.parse("local/test")
    return action


@given("a plcov3 mocked use_action environment")
def step_plcov3_mock_use_action(context: Context) -> None:
    service = MagicMock()
    action = _make_mock_action()
    service.get_action_by_name.return_value = action

    plan = _make_plan(
        phase=PlanPhase.STRATEGIZE,
        processing_state=ProcessingState.QUEUED,
    )
    service.use_action.return_value = plan
    service.save_plan.return_value = None
    service._commit_plan.return_value = None

    _start_patch(context, _PATCH_GET_LIFECYCLE, return_value=service)
    _start_patch(context, _PATCH_NOTIFY_FACADE)


@when('I plcov3 invoke use with action "{action}" and strategy-actor "{actor}"')
def step_plcov3_invoke_use_with_strategy_actor(
    context: Context, action: str, actor: str
) -> None:
    context.plcov3_result = context.plcov3_runner.invoke(
        plan_app,
        ["use", action, "--strategy-actor", actor],
    )


@when(
    'I plcov3 invoke use with action "{action}" and execution-environment "{env}" and priority "{priority}"'
)
def step_plcov3_invoke_use_with_env_priority(
    context: Context, action: str, env: str, priority: str
) -> None:
    context.plcov3_result = context.plcov3_runner.invoke(
        plan_app,
        [
            "use",
            action,
            "--execution-environment",
            env,
            "--execution-env-priority",
            priority,
        ],
    )


# ══════════════════════════════════════════════════════════════
# plan diff command (lines 2740-2741, 2743)
# ══════════════════════════════════════════════════════════════


@given("a plcov3 mocked apply service for diff")
def step_plcov3_mock_apply_svc_diff(context: Context) -> None:
    apply_svc = MagicMock()
    apply_svc.diff.return_value = "--- a/file.py\n+++ b/file.py\n@@ unified diff @@"
    _start_patch(context, _PATCH_GET_APPLY_SVC, return_value=apply_svc)


@when('I plcov3 invoke diff with plan_id "{plan_id}"')
def step_plcov3_invoke_diff(context: Context, plan_id: str) -> None:
    context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["diff", plan_id])


# ══════════════════════════════════════════════════════════════
# correct_decision error handlers (lines 3047-3048, 3051)
# ══════════════════════════════════════════════════════════════


@given("a plcov3 mocked correction environment that raises ValidationError")
def step_plcov3_mock_correct_validation_error(context: Context) -> None:
    container = MagicMock()
    # Make the correction service raise ValidationError during request_correction
    correction_svc = MagicMock()
    correction_svc.request_correction.side_effect = ValidationError(
        "Bad correction input"
    )
    container.correction_service.return_value = correction_svc

    # Decision service to get past identifier resolution
    decision_svc = MagicMock()
    decision_svc.list_decisions.return_value = []
    decision_svc.get_influence_edges.return_value = []
    container.decision_service.return_value = decision_svc

    # Lifecycle service to resolve identifier as non-plan
    from cleveragents.core.exceptions import ResourceNotFoundError

    lifecycle_svc = MagicMock()
    lifecycle_svc.get_plan.side_effect = ResourceNotFoundError(
        "Not found", resource_type="plan", resource_id="DEC-001"
    )
    container.plan_lifecycle_service.return_value = lifecycle_svc

    _start_patch(context, _PATCH_CONTAINER, return_value=container)
    # Mock _resolve_active_plan_id to return a plan ID
    _start_patch(
        context,
        "cleveragents.cli.commands.plan._resolve_active_plan_id",
        return_value="PLAN-ACTIVE",
    )


@given("a plcov3 mocked correction environment that raises CleverAgentsError")
def step_plcov3_mock_correct_clever_error(context: Context) -> None:
    container = MagicMock()
    correction_svc = MagicMock()
    correction_svc.request_correction.side_effect = CleverAgentsError(
        "Internal correction error"
    )
    container.correction_service.return_value = correction_svc

    decision_svc = MagicMock()
    decision_svc.list_decisions.return_value = []
    decision_svc.get_influence_edges.return_value = []
    container.decision_service.return_value = decision_svc

    from cleveragents.core.exceptions import ResourceNotFoundError

    lifecycle_svc = MagicMock()
    lifecycle_svc.get_plan.side_effect = ResourceNotFoundError(
        "Not found", resource_type="plan", resource_id="DEC-001"
    )
    container.plan_lifecycle_service.return_value = lifecycle_svc

    _start_patch(context, _PATCH_CONTAINER, return_value=container)
    _start_patch(
        context,
        "cleveragents.cli.commands.plan._resolve_active_plan_id",
        return_value="PLAN-ACTIVE",
    )


@when(
    'I plcov3 invoke correct with identifier "{ident}" mode "{mode}" guidance "{guidance}"'
)
def step_plcov3_invoke_correct(
    context: Context, ident: str, mode: str, guidance: str
) -> None:
    context.plcov3_result = context.plcov3_runner.invoke(
        plan_app,
        ["correct", ident, "--mode", mode, "--guidance", guidance, "--yes"],
    )


# ══════════════════════════════════════════════════════════════
# Shared Then steps
# ══════════════════════════════════════════════════════════════


@then('the plcov3 CLI output should contain "{fragment}"')
def step_plcov3_output_contains(context: Context, fragment: str) -> None:
    result = context.plcov3_result
    assert result is not None, "No CLI result captured"
    combined = (result.output or "") + (getattr(result, "stderr", "") or "")
    assert fragment in combined, f"Expected '{fragment}' in output, got:\n{combined}"


@then("the plcov3 CLI exit code should be {code:d}")
def step_plcov3_exit_code(context: Context, code: int) -> None:
    result = context.plcov3_result
    assert result is not None, "No CLI result captured"
    assert result.exit_code == code, (
        f"Expected exit code {code}, got {result.exit_code}.\n"
        f"Output: {result.output}\n"
        f"Exception: {result.exception}"
    )
