"""Step definitions for plan_executor_edge_cases_coverage.feature.

Targets uncovered lines and branches in plan_executor.py:
  - Line 378 / branch 377→378: _try_rollback_to_last_checkpoint with non-empty checkpoints
  - Lines 410-411: _resolve_sandbox_for_checkpoint → _SandboxRootProxy fallback
  - Lines 161-163 / branches 160→161, 162→163: _parse_steps non-empty path
  - Lines 417-419: run_strategize function definition

All step names use the 'edge3' prefix to avoid collisions with existing step files.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context

from cleveragents.application.services.plan_executor import (
    ExecuteResult,
    PlanExecutor,
)
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
)
from cleveragents.infrastructure.sandbox.checkpoint import SandboxCheckpoint

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

EDGE3_PLAN_ID = "01KEDGE3PLANID00000000PLN"
EDGE3_ROOT_ID = "01KEDGE3ROOTID00000000RTD"
EDGE3_SANDBOX_ROOT = "/tmp/edge3-sandbox"


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


def _edge3_make_plan(
    *,
    phase: PlanPhase = PlanPhase.EXECUTE,
    state: ProcessingState = ProcessingState.QUEUED,
    definition_of_done: str | None = "Implement feature\nWrite tests",
    decision_root_id: str | None = EDGE3_ROOT_ID,
) -> MagicMock:
    """Build a mock plan object with sensible defaults."""
    plan = MagicMock()
    plan.phase = phase
    plan.state = state
    plan.definition_of_done = definition_of_done
    plan.decision_root_id = decision_root_id
    plan.invariants = []
    plan.timestamps = PlanTimestamps()
    plan.changeset_id = None
    plan.sandbox_refs = []
    plan.error_details = None
    plan.read_only = False
    return plan


def _edge3_make_lifecycle(plan: Any | None = None) -> MagicMock:
    """Build a mock lifecycle service."""
    lcs = MagicMock()
    if plan is not None:
        lcs.get_plan.return_value = plan
    lcs.start_strategize = MagicMock()
    lcs.complete_strategize = MagicMock()
    lcs.fail_strategize = MagicMock()
    lcs.start_execute = MagicMock()
    lcs.complete_execute = MagicMock()
    lcs.fail_execute = MagicMock()
    lcs._commit_plan = MagicMock()
    return lcs


def _edge3_make_checkpoint_mock() -> SandboxCheckpoint:
    """Create a mock SandboxCheckpoint."""
    from datetime import UTC, datetime

    return SandboxCheckpoint(
        checkpoint_id="01KEDGE3CPID0000000000CP",
        sandbox_id="root-EDGE3PLAN",
        plan_id="EDGE3PLAN",
        phase="pre_execute",
        created_at=datetime.now(tz=UTC),
        metadata={},
        snapshot_path="/tmp/edge3-snapshot",
    )


# ---------------------------------------------------------------------------
# _resolve_sandbox_for_checkpoint → _SandboxRootProxy path
# ---------------------------------------------------------------------------


@given(
    "an edge3 PlanExecutor with checkpoint manager and sandbox root but no execution context"
)
def step_edge3_given_executor_sandbox_root_no_ctx(context: Context) -> None:
    """Create PlanExecutor with checkpoint_manager and sandbox_root, no exec context."""
    context.edge3_checkpoint_mgr = MagicMock()
    context.edge3_executor = PlanExecutor(
        lifecycle_service=_edge3_make_lifecycle(),
        sandbox_root=EDGE3_SANDBOX_ROOT,
        execution_context=None,
        checkpoint_manager=context.edge3_checkpoint_mgr,
    )


@given(
    "an edge3 PlanExecutor with checkpoint manager and sandbox root and context without sandbox manager"
)
def step_edge3_given_executor_ctx_no_sbmgr(context: Context) -> None:
    """Create PlanExecutor with exec context that has no sandbox_manager attr."""
    context.edge3_checkpoint_mgr = MagicMock()
    # Execution context without sandbox_manager attribute
    exec_ctx = MagicMock(spec=[])  # empty spec → no attributes
    context.edge3_executor = PlanExecutor(
        lifecycle_service=_edge3_make_lifecycle(),
        sandbox_root=EDGE3_SANDBOX_ROOT,
        execution_context=exec_ctx,
        checkpoint_manager=context.edge3_checkpoint_mgr,
    )


@given(
    "an edge3 PlanExecutor with checkpoint manager and sandbox root and context with empty sandboxes"
)
def step_edge3_given_executor_ctx_empty_sandboxes(context: Context) -> None:
    """Create PlanExecutor with exec context whose sandbox_manager returns []."""
    context.edge3_checkpoint_mgr = MagicMock()
    exec_ctx = MagicMock()
    exec_ctx.sandbox_manager = MagicMock()
    exec_ctx.sandbox_manager.list_sandboxes.return_value = []
    context.edge3_executor = PlanExecutor(
        lifecycle_service=_edge3_make_lifecycle(),
        sandbox_root=EDGE3_SANDBOX_ROOT,
        execution_context=exec_ctx,
        checkpoint_manager=context.edge3_checkpoint_mgr,
    )


@given(
    "an edge3 PlanExecutor with checkpoint manager but no sandbox root and no execution context"
)
def step_edge3_given_executor_no_sandbox_no_ctx(context: Context) -> None:
    """Create PlanExecutor with checkpoint_manager but no sandbox_root and no exec context."""
    context.edge3_checkpoint_mgr = MagicMock()
    context.edge3_executor = PlanExecutor(
        lifecycle_service=_edge3_make_lifecycle(),
        sandbox_root=None,
        execution_context=None,
        checkpoint_manager=context.edge3_checkpoint_mgr,
    )


@when('I edge3 resolve sandbox for checkpoint with plan id "{plan_id}"')
def step_edge3_resolve_sandbox(context: Context, plan_id: str) -> None:
    """Call _resolve_sandbox_for_checkpoint directly."""
    context.edge3_resolved_sandbox = (
        context.edge3_executor._resolve_sandbox_for_checkpoint(plan_id)
    )


@then("the edge3 resolved sandbox should not be None")
def step_edge3_resolved_not_none(context: Context) -> None:
    """Verify resolved sandbox is not None."""
    assert context.edge3_resolved_sandbox is not None, (
        "Expected a sandbox-like object but got None"
    )


@then("the edge3 resolved sandbox should have a synthetic sandbox id")
def step_edge3_resolved_has_sandbox_id(context: Context) -> None:
    """Verify the resolved sandbox has a sandbox_id starting with 'root-'."""
    sid = context.edge3_resolved_sandbox.sandbox_id
    assert sid.startswith("root-"), (
        f"Expected sandbox_id starting with 'root-', got '{sid}'"
    )


@then("the edge3 resolved sandbox context should have the sandbox path")
def step_edge3_resolved_has_sandbox_path(context: Context) -> None:
    """Verify the resolved sandbox context has the correct sandbox_path."""
    ctx = context.edge3_resolved_sandbox.context
    assert ctx is not None, "Expected context to be non-None"
    assert ctx.sandbox_path == EDGE3_SANDBOX_ROOT, (
        f"Expected sandbox_path='{EDGE3_SANDBOX_ROOT}', got '{ctx.sandbox_path}'"
    )


@then("the edge3 resolved sandbox should be None")
def step_edge3_resolved_is_none(context: Context) -> None:
    """Verify resolved sandbox is None."""
    assert context.edge3_resolved_sandbox is None, (
        f"Expected None but got {context.edge3_resolved_sandbox}"
    )


# ---------------------------------------------------------------------------
# _try_rollback_to_last_checkpoint with non-empty checkpoints
# ---------------------------------------------------------------------------


@given(
    "the edge3 checkpoint manager has existing checkpoints that rollback successfully"
)
def step_edge3_given_checkpoints_rollback_ok(context: Context) -> None:
    """Configure checkpoint_manager to return checkpoints and succeed on rollback."""
    cp = _edge3_make_checkpoint_mock()
    context.edge3_checkpoint_mgr.list_checkpoints.return_value = [cp]
    context.edge3_checkpoint_mgr.rollback_to.return_value = True


@given("the edge3 checkpoint manager has checkpoints but rollback raises an exception")
def step_edge3_given_checkpoints_rollback_raises(context: Context) -> None:
    """Configure checkpoint_manager with checkpoints but rollback_to raises."""
    cp = _edge3_make_checkpoint_mock()
    context.edge3_checkpoint_mgr.list_checkpoints.return_value = [cp]
    context.edge3_checkpoint_mgr.rollback_to.side_effect = RuntimeError(
        "edge3 rollback boom"
    )


@given("the edge3 checkpoint manager returns no checkpoints")
def step_edge3_given_no_checkpoints(context: Context) -> None:
    """Configure checkpoint_manager to return empty checkpoints list."""
    context.edge3_checkpoint_mgr.list_checkpoints.return_value = []


@when('I edge3 try rollback to last checkpoint for plan "{plan_id}"')
def step_edge3_try_rollback(context: Context, plan_id: str) -> None:
    """Call _try_rollback_to_last_checkpoint directly."""
    context.edge3_rollback_result = (
        context.edge3_executor._try_rollback_to_last_checkpoint(plan_id)
    )


@then("the edge3 rollback result should be True")
def step_edge3_rollback_true(context: Context) -> None:
    """Verify rollback returned True."""
    assert context.edge3_rollback_result is True, (
        f"Expected True, got {context.edge3_rollback_result}"
    )


@then("the edge3 rollback result should be False")
def step_edge3_rollback_false(context: Context) -> None:
    """Verify rollback returned False."""
    assert context.edge3_rollback_result is False, (
        f"Expected False, got {context.edge3_rollback_result}"
    )


# ---------------------------------------------------------------------------
# _try_create_checkpoint via _SandboxRootProxy
# ---------------------------------------------------------------------------


@given("the edge3 checkpoint manager accepts checkpoint creation")
def step_edge3_given_checkpoint_create_ok(context: Context) -> None:
    """Configure checkpoint_manager.create_checkpoint to return a checkpoint."""
    cp = _edge3_make_checkpoint_mock()
    context.edge3_checkpoint_mgr.create_checkpoint.return_value = cp


@given("the edge3 checkpoint manager raises on create_checkpoint")
def step_edge3_given_checkpoint_create_fails(context: Context) -> None:
    """Configure checkpoint_manager.create_checkpoint to raise."""
    context.edge3_checkpoint_mgr.create_checkpoint.side_effect = RuntimeError(
        "edge3 create boom"
    )


@when('I edge3 try create checkpoint for plan "{plan_id}" with phase "{phase}"')
def step_edge3_try_create_checkpoint(
    context: Context, plan_id: str, phase: str
) -> None:
    """Call _try_create_checkpoint directly."""
    context.edge3_checkpoint_result = context.edge3_executor._try_create_checkpoint(
        plan_id, phase
    )


@then("the edge3 checkpoint result should not be None")
def step_edge3_checkpoint_not_none(context: Context) -> None:
    """Verify checkpoint creation returned a checkpoint."""
    assert context.edge3_checkpoint_result is not None, (
        "Expected a SandboxCheckpoint but got None"
    )


@then("the edge3 checkpoint result should be None")
def step_edge3_checkpoint_is_none(context: Context) -> None:
    """Verify checkpoint creation returned None (non-fatal failure)."""
    assert context.edge3_checkpoint_result is None, (
        f"Expected None but got {context.edge3_checkpoint_result}"
    )


@then("the edge3 checkpoint manager should have been called with sandbox path metadata")
def step_edge3_checkpoint_called_with_path(context: Context) -> None:
    """Verify create_checkpoint was called with sandbox_path in metadata."""
    context.edge3_checkpoint_mgr.create_checkpoint.assert_called_once()
    call_kwargs = context.edge3_checkpoint_mgr.create_checkpoint.call_args
    # Keyword arg 'metadata' or positional
    metadata = call_kwargs.kwargs.get("metadata") or call_kwargs[1].get("metadata", {})
    assert "sandbox_path" in metadata, (
        f"Expected 'sandbox_path' in metadata, got keys: {list(metadata.keys())}"
    )
    assert metadata["sandbox_path"] == EDGE3_SANDBOX_ROOT, (
        f"Expected sandbox_path='{EDGE3_SANDBOX_ROOT}', got '{metadata['sandbox_path']}'"
    )


# ---------------------------------------------------------------------------
# Stub execute with checkpoint rollback on failure
# ---------------------------------------------------------------------------


@given("an edge3 mock lifecycle service for execute")
def step_edge3_given_lifecycle_execute(context: Context) -> None:
    """Create a mock lifecycle service for execute scenarios."""
    context.edge3_lifecycle = _edge3_make_lifecycle()
    context.edge3_plan_id = EDGE3_PLAN_ID


@given("an edge3 plan in Execute-Queued state with decision root")
def step_edge3_given_plan_execute_queued(context: Context) -> None:
    """Set up a plan in Execute-Queued state with decision root."""
    plan = _edge3_make_plan(
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.QUEUED,
        definition_of_done="Implement feature\nWrite tests",
        decision_root_id=EDGE3_ROOT_ID,
    )
    context.edge3_lifecycle.get_plan.return_value = plan
    context.edge3_mock_plan = plan


@given(
    "an edge3 PlanExecutor with checkpoint manager sandbox root and failing execute actor"
)
def step_edge3_given_executor_failing_stub_with_cp(context: Context) -> None:
    """Create PlanExecutor with checkpoint manager, sandbox root, and a failing execute actor."""
    context.edge3_checkpoint_mgr = MagicMock()
    context.edge3_executor = PlanExecutor(
        lifecycle_service=context.edge3_lifecycle,
        sandbox_root=EDGE3_SANDBOX_ROOT,
        execution_context=None,
        checkpoint_manager=context.edge3_checkpoint_mgr,
    )
    # Make execute actor always fail
    context.edge3_executor._execute_actor = MagicMock()
    context.edge3_executor._execute_actor.execute.side_effect = RuntimeError(
        "edge3 stub execute boom"
    )


@when("I edge3 call run execute expecting failure")
def step_edge3_run_execute_fail(context: Context) -> None:
    """Call run_execute expecting an exception."""
    context.edge3_raised = None
    try:
        context.edge3_executor.run_execute(context.edge3_plan_id)
    except Exception as exc:
        context.edge3_raised = exc


@then("an edge3 exception should have been raised")
def step_edge3_exception_raised(context: Context) -> None:
    """Verify an exception was raised."""
    assert context.edge3_raised is not None, "Expected an exception but none was raised"


@then("the edge3 checkpoint manager rollback_to should have been called")
def step_edge3_rollback_called(context: Context) -> None:
    """Verify checkpoint_manager.rollback_to was called."""
    context.edge3_checkpoint_mgr.rollback_to.assert_called_once()


@then("the edge3 lifecycle should have called fail_execute for edge3")
def step_edge3_check_fail_execute(context: Context) -> None:
    """Verify lifecycle.fail_execute was called."""
    context.edge3_lifecycle.fail_execute.assert_called_once()
    call_args = context.edge3_lifecycle.fail_execute.call_args[0]
    assert context.edge3_plan_id in call_args, (
        f"Expected plan_id in fail_execute args, got {call_args}"
    )


# ---------------------------------------------------------------------------
# Runtime execute with checkpoint rollback on failure
# ---------------------------------------------------------------------------


@given("an edge3 mock execution context for runtime")
def step_edge3_given_exec_ctx_runtime(context: Context) -> None:
    """Create a mock execution context for runtime mode."""
    from cleveragents.application.services.plan_execution_context import (
        PlanExecutionContext,
    )

    context.edge3_exec_ctx = PlanExecutionContext(plan_id=EDGE3_PLAN_ID)


@given(
    "an edge3 PlanExecutor with runtime context checkpoint manager sandbox root and failing runtime actor"
)
def step_edge3_given_executor_runtime_failing_with_cp(context: Context) -> None:
    """Create PlanExecutor with runtime context, checkpoint manager, and failing runtime."""
    from cleveragents.application.services.plan_execution_context import (
        RuntimeExecuteActor,
    )
    from cleveragents.tool.registry import ToolRegistry
    from cleveragents.tool.runner import ToolRunner

    context.edge3_checkpoint_mgr = MagicMock()
    runner = ToolRunner(registry=ToolRegistry())
    context.edge3_executor = PlanExecutor(
        lifecycle_service=context.edge3_lifecycle,
        tool_runner=runner,
        sandbox_root=EDGE3_SANDBOX_ROOT,
        execution_context=context.edge3_exec_ctx,
        checkpoint_manager=context.edge3_checkpoint_mgr,
    )
    # Patch RuntimeExecuteActor.execute to raise
    patcher = patch.object(
        RuntimeExecuteActor,
        "execute",
        side_effect=RuntimeError("edge3 runtime execute boom"),
    )
    patcher.start()
    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(patcher.stop)


# ---------------------------------------------------------------------------
# _parse_steps non-empty path exercised via run_strategize
# ---------------------------------------------------------------------------


@given("an edge3 mock lifecycle service for strategize")
def step_edge3_given_lifecycle_strategize(context: Context) -> None:
    """Create a mock lifecycle service for strategize scenarios."""
    context.edge3_lifecycle = _edge3_make_lifecycle()
    context.edge3_plan_id = EDGE3_PLAN_ID


@given('an edge3 plan in Strategize phase with multi-line definition "{defn}"')
def step_edge3_given_plan_strategize_multiline(context: Context, defn: str) -> None:
    """Set up a plan in Strategize phase with a multi-line definition."""
    raw = defn.replace("\\n", "\n")
    plan = _edge3_make_plan(
        phase=PlanPhase.STRATEGIZE,
        state=ProcessingState.QUEUED,
        definition_of_done=raw,
        decision_root_id=None,
    )
    context.edge3_lifecycle.get_plan.return_value = plan
    context.edge3_mock_plan = plan


@given("an edge3 plan in Strategize phase with empty definition")
def step_edge3_given_plan_strategize_empty_defn(context: Context) -> None:
    """Set up a plan in Strategize phase with empty definition_of_done."""
    plan = _edge3_make_plan(
        phase=PlanPhase.STRATEGIZE,
        state=ProcessingState.QUEUED,
        definition_of_done="",
        decision_root_id=None,
    )
    context.edge3_lifecycle.get_plan.return_value = plan
    context.edge3_mock_plan = plan


@given("an edge3 PlanExecutor for strategize without execution context")
def step_edge3_given_executor_strategize_no_ctx(context: Context) -> None:
    """Create a PlanExecutor for strategize without execution context."""
    context.edge3_executor = PlanExecutor(
        lifecycle_service=context.edge3_lifecycle,
        execution_context=None,
    )


@when("I edge3 call run strategize successfully")
def step_edge3_run_strategize(context: Context) -> None:
    """Call run_strategize and store the result."""
    context.edge3_raised = None
    try:
        context.edge3_strat_result = context.edge3_executor.run_strategize(
            context.edge3_plan_id
        )
    except Exception as exc:
        context.edge3_raised = exc


@then("the edge3 strategize result should have {n:d} decisions")
def step_edge3_check_decision_count(context: Context, n: int) -> None:
    """Verify the strategize result decision count."""
    assert context.edge3_raised is None, f"Unexpected error: {context.edge3_raised}"
    actual = len(context.edge3_strat_result.decisions)
    assert actual == n, f"Expected {n} decisions, got {actual}"


@then("the edge3 lifecycle should have called complete_strategize for edge3")
def step_edge3_check_complete_strategize(context: Context) -> None:
    """Verify complete_strategize was called."""
    context.edge3_lifecycle.complete_strategize.assert_called_once_with(
        context.edge3_plan_id
    )


# ---------------------------------------------------------------------------
# Successful stub execute with checkpoint creation via sandbox root proxy
# ---------------------------------------------------------------------------


@given(
    "an edge3 PlanExecutor with checkpoint manager and sandbox root for stub execute"
)
def step_edge3_given_executor_cp_sandbox_stub(context: Context) -> None:
    """Create PlanExecutor with checkpoint manager and sandbox root for stub execute."""
    context.edge3_checkpoint_mgr = MagicMock()
    context.edge3_executor = PlanExecutor(
        lifecycle_service=context.edge3_lifecycle,
        sandbox_root=EDGE3_SANDBOX_ROOT,
        execution_context=None,
        checkpoint_manager=context.edge3_checkpoint_mgr,
    )


@when("I edge3 call run execute successfully")
def step_edge3_run_execute_success(context: Context) -> None:
    """Call run_execute expecting success."""
    context.edge3_raised = None
    try:
        context.edge3_exec_result = context.edge3_executor.run_execute(
            context.edge3_plan_id
        )
    except Exception as exc:
        context.edge3_raised = exc


@then("the edge3 execute result should be an ExecuteResult")
def step_edge3_check_exec_result_type(context: Context) -> None:
    """Verify the result is an ExecuteResult."""
    assert context.edge3_raised is None, f"Unexpected error: {context.edge3_raised}"
    assert isinstance(context.edge3_exec_result, ExecuteResult), (
        f"Expected ExecuteResult, got {type(context.edge3_exec_result).__name__}"
    )


@then(
    "the edge3 checkpoint manager create_checkpoint should have been called at least twice"
)
def step_edge3_checkpoint_create_called_twice(context: Context) -> None:
    """Verify create_checkpoint was called at least twice (pre and post execute)."""
    call_count = context.edge3_checkpoint_mgr.create_checkpoint.call_count
    assert call_count >= 2, (
        f"Expected create_checkpoint called >= 2 times, got {call_count}"
    )
