"""Step definitions for plan_executor_child_plan_execution.feature.

Tests that cover previously uncovered code paths:
- _execute_child_plan (success, circular, not-found, no-decisions, exception)
- _execute_subplans fallback (lines 521-527, 532)
- _apply_subplan_results_to_plan exec_result=None (lines 569-570)
- Property accessors (lines 433, 438, 443)
- _try_create_checkpoint PlanError re-raise (lines 731-741, 745)
"""

from unittest.mock import MagicMock
from typing import Any

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

from ulid import ULID

from cleveragents.application.services.plan_executor import (
    PlanExecutor,
)
from cleveragents.application.services.subplan_service import (
    SpawnResult,
)
from cleveragents.core.exceptions import PlanError
from cleveragents.domain.models.core.plan import (
    NamespacedName,
    Plan,
    PlanIdentity,
    PlanPhase,
    ProcessingState,
    SubplanStatus,
)
from cleveragents.infrastructure.sandbox.checkpoint import (
    CheckpointManager,
    SandboxCheckpoint,
)


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

_PLAN_ID = str(ULID())
_SUBPLAN_ID = str(ULID())
_ROOT_PLAN_ID = str(ULID())


def _make_subplan_status(
    subplan_id: str | None = None,
    action_name: str = "local/child-action",
) -> SubplanStatus:
    return SubplanStatus(
        subplan_id=subplan_id or _SUBPLAN_ID,
        action_name=action_name,
    )


def _make_plan(
    plan_id: str | None = None,
    phase: PlanPhase = PlanPhase.STRATEGIZE,
    state: ProcessingState = ProcessingState.QUEUED,
    definition_of_done: str | None = "Do the thing",
) -> Plan:
    return Plan(
        identity=PlanIdentity(
            plan_id=plan_id or _PLAN_ID,
            root_plan_id=_ROOT_PLAN_ID,
        ),
        namespaced_name=NamespacedName(namespace="local", name="test-child-plan"),
        description="Test child plan",
        action_name="local/child-action",
        definition_of_done=definition_of_done,
        phase=phase,
        processing_state=state,
    )


def _make_spawn_result(
    num_statuses: int = 1,
) -> SpawnResult:
    statuses = [
        SubplanStatus(
            subplan_id=str(ULID()),
            action_name="local/spawned-action",
        )
        for i in range(num_statuses)
    ]
    return SpawnResult(
        spawned_statuses=statuses,
        metadata={},
        total_spawned=num_statuses,
        execution_mode="sequential",
        child_plans=[],
    )


# ---------------------------------------------------------------------------
# Given: _execute_child_plan success
# ---------------------------------------------------------------------------


def _make_child_plan_execution_lifecycle(
    definition_of_done: str | None = "Do the thing",
) -> MagicMock:
    """Build a mock lifecycle that returns plans in the correct phase order.

    ``_execute_child_plan`` calls ``run_strategize`` (needs STRATEGIZE plan)
    and then ``run_execute`` (needs EXECUTE plan).  Each of those calls
    ``get_plan`` multiple times.  We use a counter-based side_effect to
    return STRATEGIZE plans for the first 3 calls, then EXECUTE plans with
    decision_root_id for the remaining calls.
    """
    lcs = MagicMock()
    call_counter = {"count": 0}

    def _get_plan_side_effect(plan_id: str) -> Any:
        count = call_counter["count"]
        call_counter["count"] = count + 1
        if count < 3:
            return _make_plan(
                phase=PlanPhase.STRATEGIZE,
                state=ProcessingState.QUEUED,
                definition_of_done=definition_of_done,
            )
        p = _make_plan(
            phase=PlanPhase.EXECUTE,
            state=ProcessingState.QUEUED,
            definition_of_done=definition_of_done,
        )
        p.decision_root_id = str(ULID())
        return p

    lcs.get_plan.side_effect = _get_plan_side_effect
    lcs.start_strategize = MagicMock()
    lcs.complete_strategize = MagicMock()
    lcs.start_execute = MagicMock()
    lcs.complete_execute = MagicMock()
    lcs.fail_execute = MagicMock()
    lcs._commit_plan = MagicMock()
    return lcs


@given("a child3 PlanExecutor for child plan execution")
def step_given_child3_executor_for_child_plan(context: Context) -> None:
    lcs = _make_child_plan_execution_lifecycle()
    executor = PlanExecutor(lifecycle_service=lcs)
    context.child3_executor = executor
    context.child3_lcs = lcs
    context.child3_plan = _make_plan(phase=PlanPhase.STRATEGIZE)


@given("child3 a SubplanStatus for a valid child plan")
def step_given_child3_subplan_status(context: Context) -> None:
    context.child3_status = _make_subplan_status()


@given("I child3 pre-add the subplan_id to running_plan_ids")
def step_given_child3_pre_add_running(context: Context) -> None:
    context.child3_executor._running_plan_ids.add(_SUBPLAN_ID)


# ---------------------------------------------------------------------------
# Given: plan not found
# ---------------------------------------------------------------------------


@given("a child3 PlanExecutor with lifecycle returning None for child plan")
def step_given_child3_executor_lifecycle_returns_none(context: Context) -> None:
    lcs = MagicMock()
    lcs.get_plan.return_value = None
    executor = PlanExecutor(lifecycle_service=lcs)
    context.child3_executor = executor
    context.child3_lcs = lcs


# ---------------------------------------------------------------------------
# Given: strategize produces no decisions
# ---------------------------------------------------------------------------


@given("a child3 PlanExecutor for child plan with empty definition")
def step_given_child3_executor_empty_definition(context: Context) -> None:
    lcs = _make_child_plan_execution_lifecycle(definition_of_done="")
    executor = PlanExecutor(lifecycle_service=lcs)
    # Force run_strategize to return a result with no decisions so that
    # the "no decisions" error path in _execute_child_plan is exercised.
    from cleveragents.application.services.plan_executor import StrategizeResult

    empty_result = StrategizeResult(
        decision_root_id="",
        decisions=[],
    )
    executor.run_strategize = MagicMock(return_value=empty_result)

    context.child3_executor = executor
    context.child3_lcs = lcs
    context.child3_plan = _make_plan(phase=PlanPhase.STRATEGIZE, definition_of_done="")


# ---------------------------------------------------------------------------
# Given: exception during execution
# ---------------------------------------------------------------------------


@given("a child3 PlanExecutor whose lifecycle start_strategize raises RuntimeError")
def step_given_child3_executor_start_strategize_raises(context: Context) -> None:
    lcs = MagicMock()
    plan = _make_plan(phase=PlanPhase.STRATEGIZE)
    lcs.get_plan.return_value = plan
    lcs.start_strategize.side_effect = RuntimeError("boom from start_strategize")
    lcs.fail_strategize = MagicMock()
    lcs._commit_plan = MagicMock()

    executor = PlanExecutor(lifecycle_service=lcs)
    context.child3_executor = executor
    context.child3_lcs = lcs


# ---------------------------------------------------------------------------
# Given: _execute_subplans fallback
# ---------------------------------------------------------------------------


@given("a child3 PlanExecutor with subplan_service but no subplan_execution_service")
def step_given_child3_executor_subplan_svc_only(context: Context) -> None:
    lcs = _make_child_plan_execution_lifecycle()
    mock_subplan_svc = MagicMock()
    mock_subplan_svc.get_spawn_decisions.return_value = []

    executor = PlanExecutor(
        lifecycle_service=lcs,
        subplan_service=mock_subplan_svc,
    )
    context.child3_executor = executor
    context.child3_lcs = lcs
    context.child3_mock_subplan_svc = mock_subplan_svc


@given("a child3 PlanExecutor with no subplan services at all")
def step_given_child3_executor_no_subplan_services(context: Context) -> None:
    lcs = MagicMock()
    executor = PlanExecutor(lifecycle_service=lcs)
    context.child3_executor = executor
    context.child3_lcs = lcs


@given("a child3 PlanExecutor with subplan_service and success executor")
def step_given_child3_executor_subplan_svc_and_exec(context: Context) -> None:
    lcs = MagicMock()
    plan = _make_plan()
    lcs.get_plan.return_value = plan
    lcs._commit_plan = MagicMock()

    mock_subplan_svc = MagicMock()
    mock_subplan_svc.get_spawn_decisions.return_value = []

    executor = PlanExecutor(
        lifecycle_service=lcs,
        subplan_service=mock_subplan_svc,
    )
    context.child3_executor = executor
    context.child3_lcs = lcs
    context.child3_mock_subplan_svc = mock_subplan_svc


@given("child3 a SpawnResult with one spawned status")
def step_given_child3_spawn_result_one(context: Context) -> None:
    context.child3_spawn_result = _make_spawn_result(num_statuses=1)


@given("child3 a SpawnResult with no spawned statuses")
def step_given_child3_spawn_result_empty(context: Context) -> None:
    context.child3_spawn_result = _make_spawn_result(num_statuses=0)


@given("a child3 PlanExecutor for apply subplan results")
def step_given_child3_executor_for_apply(context: Context) -> None:
    lcs = MagicMock()
    lcs._commit_plan = MagicMock()
    executor = PlanExecutor(lifecycle_service=lcs)
    context.child3_executor = executor
    context.child3_lcs = lcs


# ---------------------------------------------------------------------------
# Given: property accessors
# ---------------------------------------------------------------------------


@given("a child3 PlanExecutor with all optional services configured")
def step_given_child3_executor_with_all_optional(context: Context) -> None:
    lcs = MagicMock()
    mock_fix_orch = MagicMock()
    mock_subplan_svc = MagicMock()
    mock_exec_svc = MagicMock()

    executor = PlanExecutor(
        lifecycle_service=lcs,
        fix_revalidate_orchestrator=mock_fix_orch,
        subplan_service=mock_subplan_svc,
        subplan_execution_service=mock_exec_svc,
    )
    context.child3_executor = executor
    context.child3_mock_fix_orch = mock_fix_orch
    context.child3_mock_subplan_svc = mock_subplan_svc
    context.child3_mock_exec_svc = mock_exec_svc


# ---------------------------------------------------------------------------
# Given: checkpoint PlanError
# ---------------------------------------------------------------------------


@given(
    "a child3 PlanExecutor with checkpoint manager and lifecycle that fails "
    "on commit_plan"
)
def step_given_child3_checkpoint_planerror(context: Context) -> None:
    lcs = MagicMock()
    plan = _make_plan()
    lcs.get_plan.return_value = plan
    lcs.commit_plan.side_effect = RuntimeError("commit failed")

    mock_checkpoint_mgr = MagicMock(spec=CheckpointManager)
    mock_checkpoint = MagicMock(spec=SandboxCheckpoint)
    mock_checkpoint.checkpoint_id = "CKPT01"
    mock_checkpoint_mgr.create_checkpoint.return_value = mock_checkpoint

    # We need the sandbox to be resolvable so we enter the try block
    # Use execution_context to provide a sandbox manager
    exec_ctx = MagicMock()
    exec_ctx.changeset_store = MagicMock()
    mock_sandbox = MagicMock()
    mock_sandbox.context = MagicMock()
    mock_sandbox.context.sandbox_root = "/tmp/sandbox_root"
    mock_sandbox_mgr = MagicMock()
    mock_sandbox_mgr.get_sandboxes.return_value = [mock_sandbox]
    exec_ctx.sandbox_manager = mock_sandbox_mgr

    executor = PlanExecutor(
        lifecycle_service=lcs,
        checkpoint_manager=mock_checkpoint_mgr,
        execution_context=exec_ctx,
    )
    context.child3_executor = executor
    context.child3_lcs = lcs
    context.child3_checkpoint_mgr = mock_checkpoint_mgr


@given("child3 a sandbox that returns a valid checkpoint")
def step_given_child3_sandbox_valid_checkpoint(context: Context) -> None:
    pass  # Already configured in the previous step


# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------


@when("I child3 call _execute_child_plan on the executor")
def step_when_child3_call_execute_child_plan(context: Context) -> None:
    executor: PlanExecutor = context.child3_executor
    status: SubplanStatus = context.child3_status
    try:
        context.child3_output = executor._execute_child_plan(status)
        context.child3_child_error = None
    except Exception as exc:
        context.child3_output = None
        context.child3_child_error = exc


@when("I child3 call _execute_subplans on the executor")
def step_when_child3_call_execute_subplans(context: Context) -> None:
    executor: PlanExecutor = context.child3_executor
    spawn_result: SpawnResult = context.child3_spawn_result
    plan = getattr(context, "child3_plan", None) or _make_plan()
    try:
        context.child3_exec_result = executor._execute_subplans(plan, spawn_result)
        context.child3_child_error = None
    except Exception as exc:
        context.child3_exec_result = None
        context.child3_child_error = exc


@when("I child3 call _apply_subplan_results_to_plan with exec_result None")
def step_when_child3_call_apply_subplan_results(context: Context) -> None:
    executor: PlanExecutor = context.child3_executor
    spawn_result: SpawnResult = context.child3_spawn_result
    plan = _make_plan()
    try:
        executor._apply_subplan_results_to_plan(plan, spawn_result, None)
        context.child3_apply_error = None
    except Exception as exc:
        context.child3_apply_error = exc
    context.child3_plan = plan


@when("I child3 attempt _try_create_checkpoint expecting PlanError")
def step_when_child3_try_create_checkpoint_planerror(context: Context) -> None:
    executor: PlanExecutor = context.child3_executor
    context.child3_checkpoint_error = None
    try:
        executor._try_create_checkpoint(
            plan_id=_PLAN_ID,
            phase="pre_execute",
        )
    except PlanError as exc:
        context.child3_checkpoint_error = exc
    except Exception as exc:
        context.child3_checkpoint_error = exc


# ---------------------------------------------------------------------------
# Then: _execute_child_plan success
# ---------------------------------------------------------------------------


@then("child3 the SubplanExecutionOutput should indicate success")
def step_then_child3_output_success(context: Context) -> None:
    assert context.child3_child_error is None, (
        f"Unexpected error: {context.child3_child_error}"
    )
    assert context.child3_output is not None, "Expected output but got None"
    assert context.child3_output.success is True, (
        f"Expected success, got: {context.child3_output}"
    )


@then("child3 the SubplanExecutionOutput should have a changeset_summary")
def step_then_child3_output_has_summary(context: Context) -> None:
    assert context.child3_output.changeset_summary is not None, (
        "Expected changeset_summary to be set"
    )


@then("child3 the running_plan_ids should be empty")
def step_then_child3_running_plan_ids_empty(context: Context) -> None:
    assert len(context.child3_executor._running_plan_ids) == 0, (
        f"Expected empty running_plan_ids, got: {context.child3_executor._running_plan_ids}"
    )


# ---------------------------------------------------------------------------
# Then: circular detection
# ---------------------------------------------------------------------------


@then("child3 the SubplanExecutionOutput should indicate failure")
def step_then_child3_output_failure(context: Context) -> None:
    assert context.child3_output is not None, "Expected output but got None"
    assert context.child3_output.success is False, (
        f"Expected failure, got: {context.child3_output}"
    )


@then("child3 the SubplanExecutionOutput error should mention circular")
def step_then_child3_output_error_circular(context: Context) -> None:
    assert context.child3_output.error is not None, "Expected error but got None"
    assert "circular" in context.child3_output.error.lower(), (
        f"Expected error to mention 'circular', got: {context.child3_output.error}"
    )


# ---------------------------------------------------------------------------
# Then: plan not found
# ---------------------------------------------------------------------------


@then('child3 the SubplanExecutionOutput error should mention "not found"')
def step_then_child3_output_error_not_found(context: Context) -> None:
    assert context.child3_output.error is not None, "Expected error but got None"
    assert "not found" in context.child3_output.error.lower(), (
        f"Expected error to mention 'not found', got: {context.child3_output.error}"
    )


# ---------------------------------------------------------------------------
# Then: strategize produces no decisions
# ---------------------------------------------------------------------------


@then('child3 the SubplanExecutionOutput error should mention "no decisions"')
def step_then_child3_output_error_no_decisions(context: Context) -> None:
    assert context.child3_output.error is not None, "Expected error but got None"
    assert "no decisions" in context.child3_output.error.lower(), (
        f"Expected error to mention 'no decisions', got: {context.child3_output.error}"
    )


# ---------------------------------------------------------------------------
# Then: exception during execution
# ---------------------------------------------------------------------------


@then("child3 the SubplanExecutionOutput error should contain the exception message")
def step_then_child3_output_error_contains_exception(context: Context) -> None:
    assert context.child3_output.error is not None, "Expected error but got None"
    assert "boom from start_strategize" in context.child3_output.error, (
        f"Expected error to contain exception message, got: {context.child3_output.error}"
    )


# ---------------------------------------------------------------------------
# Then: _execute_subplans
# ---------------------------------------------------------------------------


@then("child3 the SubplanExecutionResult should not be None")
def step_then_child3_exec_result_not_none(context: Context) -> None:
    assert context.child3_child_error is None, (
        f"Unexpected error: {context.child3_child_error}"
    )
    assert context.child3_exec_result is not None, (
        "Expected SubplanExecutionResult but got None"
    )


@then("child3 the SubplanExecutionResult should be None")
def step_then_child3_exec_result_none(context: Context) -> None:
    assert context.child3_child_error is None, (
        f"Unexpected error: {context.child3_child_error}"
    )
    assert context.child3_exec_result is None, (
        f"Expected None but got: {context.child3_exec_result}"
    )


# ---------------------------------------------------------------------------
# Then: _apply_subplan_results
# ---------------------------------------------------------------------------


@then("child3 the plan subplan_statuses should match the spawn result statuses")
def step_then_child3_plan_statuses_match_spawn(context: Context) -> None:
    assert context.child3_apply_error is None, (
        f"Unexpected error: {context.child3_apply_error}"
    )
    plan = context.child3_plan
    spawn_result: SpawnResult = context.child3_spawn_result
    assert plan.subplan_statuses is not None, "Expected subplan_statuses to be set"
    assert len(plan.subplan_statuses) == len(spawn_result.spawned_statuses), (
        f"Expected {len(spawn_result.spawned_statuses)} statuses, "
        f"got {len(plan.subplan_statuses)}"
    )


# ---------------------------------------------------------------------------
# Then: property accessors
# ---------------------------------------------------------------------------


@then(
    "child3 the fix_revalidate_orchestrator property should return the "
    "configured instance"
)
def step_then_child3_fix_orch_property(context: Context) -> None:
    executor: PlanExecutor = context.child3_executor
    result = executor.fix_revalidate_orchestrator
    assert result is context.child3_mock_fix_orch, (
        f"Expected {context.child3_mock_fix_orch}, got {result}"
    )


@then("child3 the subplan_service property should return the configured instance")
def step_then_child3_subplan_svc_property(context: Context) -> None:
    executor: PlanExecutor = context.child3_executor
    result = executor.subplan_service
    assert result is context.child3_mock_subplan_svc, (
        f"Expected {context.child3_mock_subplan_svc}, got {result}"
    )


@then(
    "child3 the subplan_execution_service property should return the "
    "configured instance"
)
def step_then_child3_exec_svc_property(context: Context) -> None:
    executor: PlanExecutor = context.child3_executor
    result = executor.subplan_execution_service
    assert result is context.child3_mock_exec_svc, (
        f"Expected {context.child3_mock_exec_svc}, got {result}"
    )


# ---------------------------------------------------------------------------
# Then: checkpoint PlanError
# ---------------------------------------------------------------------------


@then("a child3 PlanError should be raised about persisting checkpoint metadata")
def step_then_child3_checkpoint_planerror(context: Context) -> None:
    assert context.child3_checkpoint_error is not None, (
        "Expected PlanError but none raised"
    )
    assert isinstance(context.child3_checkpoint_error, PlanError), (
        f"Expected PlanError, got: {type(context.child3_checkpoint_error)}"
    )
    assert "checkpoint" in str(context.child3_checkpoint_error).lower(), (
        f"Expected error about checkpoints, got: {context.child3_checkpoint_error}"
    )
