"""Steps for executor_error_details.feature (#10874).

Verifies that PlanExecutor._run_execute_with_actor:
  1. Merges error_details instead of replacing (preserves strategy_decisions_json).
  2. Reports the actual execute actor class name in the ``mode`` field.
"""

from __future__ import annotations

import json
from typing import Any
from unittest.mock import MagicMock

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

from cleveragents.application.services.plan_executor import (
    ExecuteResult,
    ExecuteStubActor,
    PlanExecutor,
)
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    PlanTimestamps,
    ProcessingState,
)
from cleveragents.tool.builtins.changeset import ChangeSet

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

EED_PLAN_ID = "01KEEDPLANID000000000PLAN"
EED_ROOT_ID = "01KEEDROOTID000000000ROOT"

_STRATEGY_DECISIONS = [
    {
        "decision_id": "01KEEDDEC0000000000000DEC0",
        "step_text": "Refactor module A",
        "sequence": 0,
        "parent_id": None,
    },
    {
        "decision_id": "01KEEDDEC0000000000000DEC1",
        "step_text": "Add tests for module A",
        "sequence": 1,
        "parent_id": "01KEEDDEC0000000000000DEC0",
    },
]


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


def _eed_make_plan(
    *,
    phase: PlanPhase = PlanPhase.EXECUTE,
    state: ProcessingState = ProcessingState.QUEUED,
    error_details: dict[str, Any] | None = None,
) -> MagicMock:
    """Build a mock plan with strategy_decisions_json in error_details."""
    plan = MagicMock()
    plan.phase = phase
    plan.state = state
    plan.definition_of_done = "Refactor module A\nAdd tests for module A"
    plan.decision_root_id = EED_ROOT_ID
    plan.invariants = []
    plan.timestamps = PlanTimestamps()
    plan.changeset_id = None
    plan.sandbox_refs = []
    plan.error_details = error_details
    plan.read_only = False
    plan.identity.plan_id = EED_PLAN_ID
    return plan


def _eed_make_lifecycle(plan: MagicMock) -> MagicMock:
    """Build a mock lifecycle service that returns the given plan."""
    lcs = MagicMock()
    lcs.get_plan.return_value = plan
    lcs.start_execute = MagicMock()
    lcs.complete_execute = MagicMock()
    lcs.fail_execute = MagicMock()
    lcs.commit_plan = MagicMock()
    return lcs


# ----------------------------------------------------------------------
# Given steps
# ----------------------------------------------------------------------


@given("a eed mock lifecycle service with strategy_decisions_json in error_details")
def step_eed_mock_lifecycle(context: Context) -> None:
    """Set up a plan in EXECUTE/QUEUED with strategy_decisions_json preserved."""
    decisions_json = json.dumps(_STRATEGY_DECISIONS)
    error_details = {
        "strategy_decisions": "2",
        "strategy_decisions_json": decisions_json,
        "invariant_records": "0",
    }
    plan = _eed_make_plan(error_details=error_details)
    lifecycle = _eed_make_lifecycle(plan)
    context.eed_plan = plan
    context.eed_lifecycle = lifecycle
    context.eed_plan_id = EED_PLAN_ID


@given("a eed PlanExecutor with a succeeding execute actor")
def step_eed_executor_succeeding(context: Context) -> None:
    """Create a PlanExecutor whose execute actor returns a valid result."""
    context.eed_executor = PlanExecutor(
        lifecycle_service=context.eed_lifecycle,
        execution_context=None,
        error_recovery_service=None,
    )
    mock_actor = MagicMock(spec=ExecuteStubActor)
    mock_actor.execute.return_value = ExecuteResult(
        changeset_id="cs-001",
        changeset=ChangeSet(plan_id=EED_PLAN_ID, entries=[]),
        tool_calls_count=3,
        sandbox_refs=[],
    )
    context.eed_execute_actor = mock_actor
    context.eed_executor._execute_actor = mock_actor


@given("a eed PlanExecutor with a failing execute actor")
def step_eed_executor_failing(context: Context) -> None:
    """Create a PlanExecutor whose execute actor raises on execute."""
    context.eed_executor = PlanExecutor(
        lifecycle_service=context.eed_lifecycle,
        execution_context=None,
        error_recovery_service=None,
    )
    mock_actor = MagicMock(spec=ExecuteStubActor)
    mock_actor.__class__.__name__ = "FakeExecuteActor"
    mock_actor.execute.side_effect = RuntimeError("eed simulated failure")
    context.eed_execute_actor = mock_actor
    context.eed_executor._execute_actor = mock_actor


# ----------------------------------------------------------------------
# When steps
# ----------------------------------------------------------------------


@when("I eed run execute on the plan")
def step_eed_run_execute(context: Context) -> None:
    """Run execute and capture the result."""
    context.eed_result = context.eed_executor.run_execute(context.eed_plan_id)
    # Capture the error_details that were committed
    commit_calls = context.eed_lifecycle.commit_plan.call_args_list
    if commit_calls:
        last_plan = commit_calls[-1][0][0]
        context.eed_committed_error_details = last_plan.error_details
    else:
        context.eed_committed_error_details = {}


@when("I eed run execute expecting failure on the plan")
def step_eed_run_execute_failure(context: Context) -> None:
    """Run execute expecting it to raise, capture the committed error_details."""
    try:
        context.eed_executor.run_execute(context.eed_plan_id)
        context.eed_raised = False
    except Exception:
        context.eed_raised = True
    # Capture the error_details that were committed
    commit_calls = context.eed_lifecycle.commit_plan.call_args_list
    if commit_calls:
        last_plan = commit_calls[-1][0][0]
        context.eed_committed_error_details = last_plan.error_details
    else:
        context.eed_committed_error_details = {}


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


@then("the eed committed error_details should contain strategy_decisions_json")
def step_eed_check_strategy_json(context: Context) -> None:
    """Verify strategy_decisions_json was NOT destroyed by execute."""
    details = context.eed_committed_error_details
    assert "strategy_decisions_json" in details, (
        f"strategy_decisions_json missing from error_details: {details.keys()}"
    )
    parsed = json.loads(details["strategy_decisions_json"])
    assert len(parsed) == 2, f"Expected 2 strategy decisions, got {len(parsed)}"
    assert parsed[0]["step_text"] == "Refactor module A"


@then("the eed committed error_details should contain tool_calls_count")
def step_eed_check_tool_calls(context: Context) -> None:
    details = context.eed_committed_error_details
    assert "tool_calls_count" in details, (
        f"tool_calls_count missing from error_details: {details.keys()}"
    )


@then("the eed committed error_details should contain sandbox_refs_count")
def step_eed_check_sandbox_refs(context: Context) -> None:
    details = context.eed_committed_error_details
    assert "sandbox_refs_count" in details, (
        f"sandbox_refs_count missing from error_details: {details.keys()}"
    )


@then("the eed committed error_details should contain exception_type")
def step_eed_check_exception_type(context: Context) -> None:
    details = context.eed_committed_error_details
    assert "exception_type" in details, (
        f"exception_type missing from error_details: {details.keys()}"
    )
    assert details["exception_type"] == "RuntimeError"


@then("the eed committed error_details should contain traceback")
def step_eed_check_traceback(context: Context) -> None:
    details = context.eed_committed_error_details
    assert "traceback" in details, (
        f"traceback missing from error_details: {details.keys()}"
    )


@then("the eed committed error_details mode should be the execute actor class name")
def step_eed_check_mode(context: Context) -> None:
    """Verify mode reflects the actual actor type, not hardcoded 'stub'."""
    details = context.eed_committed_error_details
    assert "mode" in details, f"mode missing from error_details: {details.keys()}"
    actual_mode = details["mode"]
    assert actual_mode != "stub", (
        "mode is still hardcoded as 'stub', expected actor class name"
    )
    expected = type(context.eed_execute_actor).__name__
    assert actual_mode == expected, f"mode is '{actual_mode}', expected '{expected}'"
