"""Step definitions for plan_lifecycle_service_coverage_boost_r4.feature.

Targets remaining uncovered lines in PlanLifecycleService:
- Lines ~386-405: _run_estimation event_bus.emit PLAN_ESTIMATION_COMPLETE success + exception
- Line 376: cost_estimate_usd assignment when estimated_cost_usd is not None
- Line 462: project_name extraction from plan.project_links[0]
- Lines ~481-505: _run_invariant_reconciliation INVARIANT_RECONCILED emit exception
- Lines ~517-536: _run_invariant_reconciliation INVARIANT_VIOLATED emit exception
- Lines ~562-566: _subscribe_correction_reconciliation event_bus.subscribe failure
- Line 577: _handle_correction_applied guard (plan_id is None)
- Lines ~584-595: _handle_correction_applied ReconciliationBlockedError + general exception
- Lines ~1230-1232: unknown automation profile ValidationError
- Lines ~1616-1621, 1684-1688: execute_plan lock acquire/release with lock_service
- Lines ~1830-1835, 1872-1877: apply_plan lock acquire/release with lock_service
- Lines ~2082-2097: prompt_plan blank guidance + wrong phase/state validation
- Lines ~2100-2156: prompt_plan state transition + decision_service interaction
"""

from __future__ import annotations

from typing import Any
from unittest.mock import patch

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

from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
    ReconciliationBlockedError,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import PlanError, ValidationError
from cleveragents.domain.models.core.action import Action
from cleveragents.domain.models.core.estimation import EstimationResult
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    ProcessingState,
    ProjectLink,
)
from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.types import EventType


# -----------------------------------------------------------------
# Background
# -----------------------------------------------------------------


@given("I have a fresh plan lifecycle service for coverage boost r4")
def step_create_fresh_service_r4(context: Context) -> None:
    """Create a clean PlanLifecycleService instance."""
    Settings._instance = None
    settings = Settings()
    context.service = PlanLifecycleService(settings=settings)
    context.error = None


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


def _create_action_r4(context: Context, name: str, **kwargs) -> Action:
    """Helper to create a basic action with sensible defaults."""
    defaults: dict[str, Any] = {
        "name": name,
        "description": f"Action {name}",
        "definition_of_done": "Tests pass",
        "strategy_actor": "openai/gpt-4",
        "execution_actor": "openai/gpt-4",
    }
    defaults.update(kwargs)
    return context.service.create_action(**defaults)


def _advance_to_strategize_complete(context: Context) -> None:
    """Advance plan to strategize-complete state."""
    pid = context.plan.identity.plan_id
    context.service.start_strategize(pid)
    context.service.complete_strategize(pid)
    context.plan = context.service.get_plan(pid)


def _advance_to_execute_complete(context: Context) -> None:
    """Advance plan to execute-complete state (ready for apply)."""
    pid = context.plan.identity.plan_id
    context.service.start_strategize(pid)
    context.service.complete_strategize(pid)
    context.service.execute_plan(pid)
    context.service.start_execute(pid)
    context.service.complete_execute(pid)
    context.plan = context.service.get_plan(pid)


def _advance_to_strategize_processing(context: Context) -> None:
    """Advance plan to strategize/PROCESSING state."""
    pid = context.plan.identity.plan_id
    context.service.start_strategize(pid)
    context.plan = context.service.get_plan(pid)


# -----------------------------------------------------------------
# Mock event buses
# -----------------------------------------------------------------


class FailingEventBusR4:
    """An event bus that raises on every emit call."""

    def emit(self, event: DomainEvent) -> None:
        raise RuntimeError("Simulated event bus emit failure")

    def subscribe(self, event_type: Any, handler: Any) -> None:
        pass


class FailingSubscribeEventBus:
    """An event bus that raises on subscribe but works for emit."""

    def emit(self, event: DomainEvent) -> None:
        pass

    def subscribe(self, event_type: Any, handler: Any) -> None:
        raise RuntimeError("Simulated subscribe failure")


class RecordingEventBusR4:
    """A simple event bus that records emitted events."""

    def __init__(self) -> None:
        self.events: list[DomainEvent] = []

    def emit(self, event: DomainEvent) -> None:
        self.events.append(event)

    def subscribe(self, event_type: Any, handler: Any) -> None:
        pass


class FailingDecisionService:
    """A decision service that raises on all operations."""

    def list_decisions(self, plan_id: str) -> list[Any]:
        raise RuntimeError("Simulated decision service failure")

    def record_decision(self, **kwargs: Any) -> Any:
        raise RuntimeError("Simulated decision service failure")


# =================================================================
# _run_estimation via execute_plan — event_bus.emit PLAN_ESTIMATION_COMPLETE
# =================================================================


@given("an action with estimation_actor configured for r4")
def step_action_with_estimation_actor_r4(context: Context) -> None:
    """Create an action with estimation_actor set."""
    context.action = _create_action_r4(
        context,
        "local/est-action-r4",
        estimation_actor="openai/gpt-4",
    )


@given("an action with estimation_actor returning cost for r4")
def step_action_with_estimation_cost_r4(context: Context) -> None:
    """Create an action with estimation_actor and patch stub to return cost."""
    context.action = _create_action_r4(
        context,
        "local/est-cost-action-r4",
        estimation_actor="openai/gpt-4",
    )
    context.mock_estimation_result = EstimationResult(
        summary="Test estimation with cost",
        estimated_cost_usd=1.23,
    )
    context.estimation_patch = patch(
        "cleveragents.application.services.plan_executor.EstimationStubActor.estimate",
        return_value=context.mock_estimation_result,
    )


@given("a plan in strategize-complete state for r4")
def step_plan_strategize_complete_r4(context: Context) -> None:
    """Create a plan in strategize/COMPLETE state."""
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-r4")],
    )
    _advance_to_strategize_complete(context)


@given("a plan in strategize phase with processing state for r4")
def step_plan_strategize_processing_r4(context: Context) -> None:
    """Create a plan in strategize/PROCESSING state."""
    if not hasattr(context, "action") or context.action is None:
        context.action = _create_action_r4(
            context, f"local/corr-handler-r4-{id(context)}"
        )
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-r4")],
    )
    _advance_to_strategize_processing(context)


@given("the plan lifecycle service has a recording event bus for r4")
def step_service_with_recording_bus_r4(context: Context) -> None:
    """Set up recording event bus on existing service."""
    context.recording_bus_r4 = RecordingEventBusR4()
    context.service.event_bus = context.recording_bus_r4


@given("the plan lifecycle service has a failing event bus for r4")
def step_service_with_failing_bus_r4(context: Context) -> None:
    """Set up failing event bus on existing service."""
    context.service.event_bus = FailingEventBusR4()


@when("I execute the plan for r4")
def step_execute_plan_r4(context: Context) -> None:
    """Call execute_plan on the current plan."""
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.plan = context.service.execute_plan(pid)
    except Exception as e:
        context.error = e


@then("the event bus should have recorded PLAN_ESTIMATION_COMPLETE event")
def step_verify_estimation_complete_event_r4(context: Context) -> None:
    """Verify PLAN_ESTIMATION_COMPLETE was emitted."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    est_events = [
        e
        for e in context.recording_bus_r4.events
        if e.event_type == EventType.PLAN_ESTIMATION_COMPLETE
    ]
    assert len(est_events) >= 1, (
        f"Expected at least 1 PLAN_ESTIMATION_COMPLETE event, "
        f"got {len(est_events)}. Events: {[e.event_type for e in context.recording_bus_r4.events]}"
    )


@then("the plan should be in execute phase despite estimation emit failure")
def step_plan_in_executeDespiteEstimationEmitFailure_r4(context: Context) -> None:
    """Verify plan is in EXECUTE even when estimation emit failed."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.phase == PlanPhase.EXECUTE, (
        f"Expected EXECUTE phase, got {context.plan.phase}"
    )


@then("the plan should be in execute phase")
def step_plan_in_execute_phase_r4(context: Context) -> None:
    """Verify plan is in EXECUTE phase."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.phase == PlanPhase.EXECUTE, (
        f"Expected EXECUTE phase, got {context.plan.phase}"
    )


@then("the plan cost_estimate_usd should be populated")
def step_verify_cost_estimate_populated_r4(context: Context) -> None:
    """Verify plan.cost_estimate_usd was set from estimation result."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.cost_estimate_usd is not None, (
        "Expected cost_estimate_usd to be set, got None"
    )


# =================================================================
# execute_plan with mocked EstimationStubActor for cost_estimate_usd
# =================================================================


@when("I execute the plan for r4 with mocked estimation")
def step_execute_plan_with_mocked_estimation_r4(context: Context) -> None:
    """Execute plan with EstimationStubActor patched to return cost."""
    mock_result = context.mock_estimation_result
    with patch(
        "cleveragents.application.services.plan_executor.EstimationStubActor.estimate",
        return_value=mock_result,
    ):
        context.error = None
        try:
            pid = context.plan.identity.plan_id
            context.plan = context.service.execute_plan(pid)
        except Exception as e:
            context.error = e


# =================================================================
# execute_plan with lock_service
# =================================================================


class MockLockService:
    """A mock lock service that tracks acquire/release calls."""

    def __init__(self) -> None:
        self.acquired: list[tuple[str, str, str]] = []
        self.released: list[tuple[str, str, str]] = []

    def acquire(self, owner_id: str, resource_type: str, resource_id: str) -> None:
        self.acquired.append((owner_id, resource_type, resource_id))

    def release(self, owner_id: str, resource_type: str, resource_id: str) -> None:
        self.released.append((owner_id, resource_type, resource_id))


@given("an action for execute with lock service for r4")
def step_action_execute_lock_r4(context: Context) -> None:
    """Create an action for testing execute_plan with lock_service."""
    context.action = _create_action_r4(context, "local/exec-lock-r4")


@when("I execute the plan with lock service for r4")
def step_execute_with_lock_r4(context: Context) -> None:
    """Execute plan with a mocked lock service."""
    context.mock_lock_service = MockLockService()
    context.service._lock_service = context.mock_lock_service
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.plan = context.service.execute_plan(pid)
    except Exception as e:
        context.error = e


@then("the lock service should have acquired and released the plan lock")
def step_verify_lock_acquire_release_r4(context: Context) -> None:
    """Verify lock was acquired and released for execute_plan."""
    assert hasattr(context, "mock_lock_service"), "mock_lock_service not found"
    ls = context.mock_lock_service
    assert len(ls.acquired) >= 1, (
        f"Expected at least 1 acquire call, got {len(ls.acquired)}"
    )
    assert len(ls.released) >= 1, (
        f"Expected at least 1 release call, got {len(ls.released)}"
    )
    plan_id = context.plan.identity.plan_id
    for call in ls.acquired:
        assert call[1] == "plan", f"Expected resource_type 'plan', got {call[1]}"
        assert call[2] == plan_id, f"Expected resource_id {plan_id}, got {call[2]}"


# =================================================================
# apply_plan with lock_service
# =================================================================


@given("an action for apply with lock service for r4")
def step_action_apply_lock_r4(context: Context) -> None:
    """Create an action for testing apply_plan with lock_service."""
    context.action = _create_action_r4(context, "local/apply-lock-r4")


@given("a plan in execute-complete state for r4")
def step_plan_execute_complete_r4(context: Context) -> None:
    """Create a plan in execute/COMPLETE state (ready for apply)."""
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-r4")],
    )
    _advance_to_execute_complete(context)


@when("I apply the plan with lock service for r4")
def step_apply_with_lock_r4(context: Context) -> None:
    """Apply plan with a mocked lock service."""
    context.mock_lock_service = MockLockService()
    context.service._lock_service = context.mock_lock_service
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.plan = context.service.apply_plan(pid)
    except Exception as e:
        context.error = e


@then("the plan should be in apply phase")
def step_plan_in_apply_phase_r4(context: Context) -> None:
    """Verify plan is in APPLY phase."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.phase == PlanPhase.APPLY, (
        f"Expected APPLY phase, got {context.plan.phase}"
    )


# =================================================================
# _subscribe_correction_reconciliation — event_bus.subscribe failure
# =================================================================


@given("I have a plan lifecycle service with event_bus that fails on subscribe for r4")
def step_service_with_failing_subscribe_r4(context: Context) -> None:
    """Create service where event_bus.subscribe raises."""
    Settings._instance = None
    settings = Settings()
    context.error = None
    try:
        context.service = PlanLifecycleService(
            settings=settings,
            event_bus=FailingSubscribeEventBus(),
        )
    except Exception as e:
        context.error = e


@when("the service is created for r4")
def step_service_created_r4(context: Context) -> None:
    """Service creation step (no-op since given already created it)."""
    pass


@then("no exception should be raised for r4")
def step_no_exception_r4(context: Context) -> None:
    """Verify no exception was raised."""
    assert context.error is None, f"Expected no error, got: {context.error}"


# =================================================================
# prompt_plan validation scenarios
# =================================================================


def _advance_to_execute_processing(context: Context) -> None:
    """Advance plan to execute/PROCESSING state."""
    pid = context.plan.identity.plan_id
    context.service.start_strategize(pid)
    context.service.complete_strategize(pid)
    context.service.execute_plan(pid)
    context.service.start_execute(pid)
    context.plan = context.service.get_plan(pid)


def _advance_to_execute_complete_for_prompt(context: Context) -> None:
    """Advance plan to execute/COMPLETE state."""
    pid = context.plan.identity.plan_id
    context.service.start_strategize(pid)
    context.service.complete_strategize(pid)
    context.service.execute_plan(pid)
    context.service.start_execute(pid)
    context.service.complete_execute(pid)
    context.plan = context.service.get_plan(pid)


@given("a plan in execute phase with processing state for prompt_plan r4")
def step_plan_execute_processing_for_prompt_r4(context: Context) -> None:
    """Create a plan in execute/PROCESSING state for prompt_plan testing."""
    context.action = _create_action_r4(context, "local/prompt-test-r4")
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-r4")],
    )
    _advance_to_execute_processing(context)


@given("a plan in strategize phase for prompt_plan r4")
def step_plan_strategize_for_prompt_r4(context: Context) -> None:
    """Create a plan in strategize/PROCESSING state for prompt_plan testing."""
    context.action = _create_action_r4(context, "local/prompt-wrong-phase-r4")
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-r4")],
    )
    _advance_to_state_prompt(context, PlanPhase.STRATEGIZE, ProcessingState.PROCESSING)


@given("a plan in execute-complete state for prompt_plan r4")
def step_plan_execute_complete_for_prompt_r4(context: Context) -> None:
    """Create a plan in execute/COMPLETE state for prompt_plan testing."""
    context.action = _create_action_r4(context, "local/prompt-complete-state-r4")
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-r4")],
    )
    _advance_to_execute_complete_for_prompt(context)


@given("a plan in execute phase with errored state for prompt_plan r4")
def step_plan_execute_errored_for_prompt_r4(context: Context) -> None:
    """Create a plan in execute/ERRORED state for prompt_plan testing."""
    context.action = _create_action_r4(context, "local/prompt-errored-r4")
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-r4")],
    )
    _advance_to_execute_processing(context)
    pid = context.plan.identity.plan_id
    context.service.fail_execute(pid, error_message="Prior test error")
    context.plan = context.service.get_plan(pid)


def _advance_to_state_prompt(
    context: Context, target_phase: PlanPhase, target_state: ProcessingState
) -> None:
    """Advance plan to target phase/state."""
    pid = context.plan.identity.plan_id
    if (
        target_phase == PlanPhase.STRATEGIZE
        and target_state == ProcessingState.PROCESSING
    ):
        context.service.start_strategize(pid)
        context.plan = context.service.get_plan(pid)
    elif (
        target_phase == PlanPhase.EXECUTE and target_state == ProcessingState.PROCESSING
    ):
        context.service.start_strategize(pid)
        context.service.complete_strategize(pid)
        context.service.execute_plan(pid)
        context.service.start_execute(pid)
        context.plan = context.service.get_plan(pid)


@when("I prompt the plan with blank guidance for r4")
def step_prompt_blank_guidance_r4(context: Context) -> None:
    """Call prompt_plan with blank guidance."""
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.result = context.service.prompt_plan(pid, guidance="   ")
    except Exception as e:
        context.error = e


@then("a ValidationError should be raised for blank guidance")
def step_verify_validation_error_r4(context: Context) -> None:
    """Verify ValidationError was raised for blank guidance."""
    assert isinstance(context.error, ValidationError), (
        f"Expected ValidationError, got {type(context.error).__name__}: {context.error}"
    )


@when("I prompt the plan with guidance for wrong phase for r4")
def step_prompt_wrong_phase_r4(context: Context) -> None:
    """Call prompt_plan when plan is not in Execute phase."""
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.result = context.service.prompt_plan(pid, guidance="Do something")
    except Exception as e:
        context.error = e


@then("a PlanError should be raised for wrong phase")
def step_verify_plan_error_wrong_phase_r4(context: Context) -> None:
    """Verify PlanError was raised for wrong phase."""
    assert isinstance(context.error, PlanError), (
        f"Expected PlanError, got {type(context.error).__name__}: {context.error}"
    )


@when("I prompt the plan with guidance for complete state for r4")
def step_prompt_wrong_state_r4(context: Context) -> None:
    """Call prompt_plan when plan is in COMPLETE state (not recoverable)."""
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.result = context.service.prompt_plan(pid, guidance="Resume work")
    except Exception as e:
        context.error = e


@then("a PlanError should be raised for wrong state")
def step_verify_plan_error_wrong_state_r4(context: Context) -> None:
    """Verify PlanError was raised for wrong state."""
    assert isinstance(context.error, PlanError), (
        f"Expected PlanError, got {type(context.error).__name__}: {context.error}"
    )


@when("I prompt the plan with guidance to resume from errored state for r4")
def step_prompt_errored_resume_r4(context: Context) -> None:
    """Call prompt_plan to resume from errored state."""
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.result = context.service.prompt_plan(pid, guidance="Please recover")
        context.plan = context.service.get_plan(pid)
    except Exception as e:
        context.error = e


@then("the plan processing_state should be processing")
def step_verify_processing_state_r4(context: Context) -> None:
    """Verify plan.processing_state is PROCESSING."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.processing_state == ProcessingState.PROCESSING, (
        f"Expected PROCESSING state, got {context.plan.processing_state}"
    )


@then("the plan error_message should be cleared")
def step_verify_error_cleared_r4(context: Context) -> None:
    """Verify plan.error_message was cleared after resuming from errored."""
    assert context.plan.error_message is None, (
        f"Expected error_message to be cleared, got: {context.plan.error_message}"
    )


@given("the plan lifecycle service has a failing decision service for r4")
def step_failing_decision_service_r4(context: Context) -> None:
    """Replace decision_service with one that always fails."""
    context.service.decision_service = FailingDecisionService()


@when("I prompt the plan with guidance for r4")
def step_prompt_with_failing_decision_service_r4(context: Context) -> None:
    """Call prompt_plan when decision_service raises."""
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.result = context.service.prompt_plan(pid, guidance="Continue anyway")
        context.plan = context.service.get_plan(pid)
    except Exception as e:
        context.error = e


# =================================================================
# _run_invariant_reconciliation — event_bus.emit exception scenarios
# =================================================================


@given("an action with invariant_actor for reconciliation success r4")
def step_action_invariant_success_r4(context: Context) -> None:
    """Create action with invariant_actor set for successful reconciliation."""
    context.action = _create_action_r4(
        context,
        "local/inv-success-r4",
        invariant_actor="openai/gpt-4",
    )


@given("a plan in strategize-complete state with project links for r4")
def step_plan_strategize_complete_with_links_r4(context: Context) -> None:
    """Create plan with project_links for testing project_name extraction."""
    if (
        not hasattr(context.service, "invariant_service")
        or context.service.invariant_service is None
    ):
        from cleveragents.application.services.invariant_service import InvariantService
        from cleveragents.application.services.decision_service import DecisionService

        context.service.invariant_service = InvariantService()
        context.service.decision_service = DecisionService()
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="test-project-r4")],
    )
    _advance_to_strategize_complete(context)


@when("I execute the plan for invariant reconciliation emit failure r4")
def step_execute_invariant_emit_fail_r4(context: Context) -> None:
    """Execute plan with mocked reconciliation actor (succeeds) and failing emit."""
    from cleveragents.actor.reconciliation import ReconciliationResult
    from cleveragents.actor.reconciliation import InvariantReconciliationActor
    from cleveragents.domain.models.core.invariant import InvariantSet

    context.error = None
    mock_result = ReconciliationResult(
        reconciled_set=InvariantSet(invariants=[]),
        conflicts=[],
        enforced_decision_ids=[],
    )

    class PatchedActor(InvariantReconciliationActor):
        def run(self, **kwargs: Any) -> Any:
            return mock_result

    with patch(
        "cleveragents.actor.reconciliation.InvariantReconciliationActor",
        PatchedActor,
    ):
        try:
            pid = context.plan.identity.plan_id
            context.plan = context.service.execute_plan(pid)
        except Exception as e:
            context.error = e


@then("the event_bus should have been called for INVARIANT_RECONCILED emit failure")
def step_verify_invariant_reconciled_emit_fail_r4(context: Context) -> None:
    """Verify plan reached EXECUTE despite emit failure in reconciliation."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.phase == PlanPhase.EXECUTE, (
        f"Expected EXECUTE, got {context.plan.phase}"
    )


@given("an action with invariant_actor causing reconciliation failure for r4")
def step_action_invariant_fail_r4(context: Context) -> None:
    """Create action with invariant_actor that will cause failure."""
    context.action = _create_action_r4(
        context,
        "local/inv-fail-r4",
        invariant_actor="openai/gpt-4",
    )


@when("I execute the plan expecting reconciliation failure for r4")
def step_execute_invariant_fail_r4(context: Context) -> None:
    """Execute plan where reconciliation raises (triggers INVARIANT_VIOLATED emit fail path)."""
    from cleveragents.actor.reconciliation import InvariantReconciliationActor

    class FailingActor(InvariantReconciliationActor):
        def run(self, **kwargs: Any) -> Any:
            raise RuntimeError("Simulated invariant violation")

    with patch(
        "cleveragents.actor.reconciliation.InvariantReconciliationActor",
        FailingActor,
    ):
        context.error = None
        try:
            pid = context.plan.identity.plan_id
            context.plan = context.service.execute_plan(pid)
        except ReconciliationBlockedError as e:
            context.error = e
        except Exception as e:
            context.error = e


@then("ReconciliationBlockedError should be raised")
def step_verify_reconciliation_blocked_error_r4(context: Context) -> None:
    """Verify ReconciliationBlockedError was raised."""
    assert isinstance(context.error, ReconciliationBlockedError), (
        f"Expected ReconciliationBlockedError, got {type(context.error).__name__}: {context.error}"
    )


@then("the event_bus should have been called for INVARIANT_VIOLATED emit failure")
def step_verify_invariant_violated_emit_fail_r4(context: Context) -> None:
    """Verify INVARIANT_VIOLATED emit exception was handled (error was still raised)."""
    assert isinstance(context.error, ReconciliationBlockedError), (
        "Expected ReconciliationBlockedError to be raised despite emit failure"
    )


# =================================================================
# Unknown automation profile validation (lines 1230-1232)
# =================================================================


@given("an action with unknown automation profile for r4")
def step_action_unknown_profile_r4(context: Context) -> None:
    """Create action with an unknown automation profile name."""
    context.action = _create_action_r4(
        context,
        "local/unknown-profile-r4",
        automation_profile="nonexistent-profile-xyz",
    )


@when("I use the action to create a plan for r4")
def step_use_action_unknown_profile_r4(context: Context) -> None:
    """Try to use_action with unknown automation profile (raises during profile resolution)."""
    context.error = None
    try:
        context.plan = context.service.use_action(
            action_name=str(context.action.namespaced_name),
            project_links=[ProjectLink(project_name="proj-r4")],
        )
    except ValidationError as e:
        context.error = e
    except Exception as e:
        context.error = e


@then("a ValidationError should be raised for unknown profile")
def step_verify_validation_error_unknown_profile_r4(context: Context) -> None:
    """Verify ValidationError was raised for unknown automation profile."""
    assert isinstance(context.error, ValidationError), (
        f"Expected ValidationError, got {type(context.error).__name__}: {context.error}"
    )


# =================================================================
# _handle_correction_applied — event handler scenarios
# =================================================================


class MockEventBusForHandler:
    """Event bus that records correction applied events for handler testing."""

    def __init__(self) -> None:
        self.events: list[DomainEvent] = []
        self._handler: Any = None

    def emit(self, event: DomainEvent) -> None:
        self.events.append(event)

    def subscribe(self, event_type: Any, handler: Any) -> None:
        self._handler = handler


@when("I handle a CORRECTION_APPLIED event with no plan_id for r4")
def step_handle_correction_no_plan_id_r4(context: Context) -> None:
    """Call _handle_correction_applied with an event that has no plan_id."""
    context.error = None
    event = DomainEvent(
        event_type=EventType.CORRECTION_APPLIED,
        plan_id=None,
        details={},
    )
    try:
        context.service._handle_correction_applied(event)
    except Exception as e:
        context.error = e


@given("the plan lifecycle service has a recording event bus for correction handler r4")
def step_service_recording_bus_for_handler_r4(context: Context) -> None:
    """Set up service with recording event bus for _handle_correction_applied testing."""
    Settings._instance = None
    settings = Settings()
    context.mock_bus_handler = MockEventBusForHandler()
    context.service = PlanLifecycleService(
        settings=settings, event_bus=context.mock_bus_handler
    )


@given("the plan lifecycle service is configured for r4")
def step_service_configured_r4(context: Context) -> None:
    """Ensure service is configured (reuse existing or create new)."""
    if not hasattr(context, "service") or context.service is None:
        Settings._instance = None
        settings = Settings()
        context.service = PlanLifecycleService(settings=settings)


@given("the invariant reconciliation raises ReconciliationBlockedError for r4")
def step_invariant_raises_blocked_error_r4(context: Context) -> None:
    """Patch _run_invariant_reconciliation to raise ReconciliationBlockedError."""

    def _patched_run_invariant(plan: Any) -> None:
        raise ReconciliationBlockedError(
            plan_id=plan.identity.plan_id,
            phase=plan.phase,
            reason="Simulated blocked error",
        )

    context.service._run_invariant_reconciliation = _patched_run_invariant


@when("I handle a CORRECTION_APPLIED event for blocked reconciliation for r4")
def step_handle_correction_blocked_r4(context: Context) -> None:
    """Call _handle_correction_applied when reconciliation raises ReconciliationBlockedError."""
    context.error = None
    event = DomainEvent(
        event_type=EventType.CORRECTION_APPLIED,
        plan_id=context.plan.identity.plan_id,
        details={},
    )
    try:
        context.service._handle_correction_applied(event)
    except Exception as e:
        context.error = e


@given("get_plan will fail for the plan for r4")
def step_get_plan_fails_r4(context: Context) -> None:
    """Patch get_plan to raise an exception."""

    def _failing_get_plan(plan_id: str) -> Any:
        raise RuntimeError("Simulated get_plan failure")

    context.service.get_plan = _failing_get_plan


@when("I handle a CORRECTION_APPLIED event when get_plan fails for r4")
def step_handle_correction_get_plan_fails_r4(context: Context) -> None:
    """Call _handle_correction_applied when get_plan raises."""
    context.error = None
    event = DomainEvent(
        event_type=EventType.CORRECTION_APPLIED,
        plan_id="nonexistent-plan-id",
        details={},
    )
    try:
        context.service._handle_correction_applied(event)
    except Exception as e:
        context.error = e


# =================================================================
# resolve_actor_provider_model — namespace/name → provider/model
# Lines 731-764
# =================================================================


@given("the plan lifecycle service has a unit of work for actor resolution r4")
def step_service_with_uow_for_actor_r4(context: Context) -> None:
    """Create a service with a basic unit of work for actor resolution."""
    from unittest.mock import MagicMock
    from contextlib import contextmanager

    Settings._instance = None
    settings = Settings()
    mock_uow = MagicMock()
    mock_ctx = MagicMock()
    mock_ctx.actors.get_by_name.return_value = None

    @contextmanager
    def _fake_transaction():
        yield mock_ctx

    mock_uow.transaction = _fake_transaction
    context.service = PlanLifecycleService(settings=settings, unit_of_work=mock_uow)
    context.error = None


@given(
    "the plan lifecycle service has a unit of work with a known actor "
    '"{actor_name}" using "{provider_model}" for r4'
)
def step_service_with_known_actor_r4(
    context: Context, actor_name: str, provider_model: str
) -> None:
    """Create a service where actor lookup succeeds for the given name."""
    from unittest.mock import MagicMock
    from contextlib import contextmanager

    provider, model = provider_model.split("/", 1)
    mock_actor = MagicMock()
    mock_actor.provider = provider
    mock_actor.model = model

    Settings._instance = None
    settings = Settings()
    mock_uow = MagicMock()
    mock_ctx = MagicMock()
    mock_ctx.actors.get_by_name.return_value = mock_actor

    @contextmanager
    def _fake_transaction():
        yield mock_ctx

    mock_uow.transaction = _fake_transaction
    context.service = PlanLifecycleService(settings=settings, unit_of_work=mock_uow)
    context.error = None


@given(
    'the plan lifecycle service has a unit of work without actor "{actor_name}" for r4'
)
def step_service_without_actor_r4(context: Context, actor_name: str) -> None:
    """Create a service where actor lookup returns None."""
    from unittest.mock import MagicMock
    from contextlib import contextmanager

    Settings._instance = None
    settings = Settings()
    mock_uow = MagicMock()
    mock_ctx = MagicMock()
    mock_ctx.actors.get_by_name.return_value = None

    @contextmanager
    def _fake_transaction():
        yield mock_ctx

    mock_uow.transaction = _fake_transaction
    context.service = PlanLifecycleService(settings=settings, unit_of_work=mock_uow)
    context.error = None


@given(
    "the plan lifecycle service has a unit of work that raises on actor lookup for r4"
)
def step_service_with_raising_actor_lookup_r4(context: Context) -> None:
    """Create a service where actor lookup raises an exception."""
    from unittest.mock import MagicMock
    from contextlib import contextmanager

    Settings._instance = None
    settings = Settings()
    mock_uow = MagicMock()
    mock_ctx = MagicMock()
    mock_ctx.actors.get_by_name.side_effect = RuntimeError("DB failure")

    @contextmanager
    def _fake_transaction():
        yield mock_ctx

    mock_uow.transaction = _fake_transaction
    context.service = PlanLifecycleService(settings=settings, unit_of_work=mock_uow)
    context.error = None


@given("the plan lifecycle service has no unit of work for r4")
def step_service_no_uow_r4(context: Context) -> None:
    """Create a service with no unit_of_work set."""
    Settings._instance = None
    settings = Settings()
    context.service = PlanLifecycleService(
        settings=settings,
        unit_of_work=None,
    )
    context.error = None


@when('I resolve actor provider model for "{actor_name}"')
def step_resolve_actor_provider_model(context: Context, actor_name: str) -> None:
    """Call resolve_actor_provider_model directly."""
    context.resolved_model = context.service.resolve_actor_provider_model(actor_name)


@when('I resolve actor provider model for ""')
def step_resolve_actor_provider_model_empty(context: Context) -> None:
    """Call resolve_actor_provider_model with empty string."""
    context.resolved_model = context.service.resolve_actor_provider_model("")


@then('the resolved actor provider model should be "{expected}"')
def step_verify_resolved_provider_model(context: Context, expected: str) -> None:
    """Verify the resolved provider/model string matches."""
    assert context.resolved_model == expected, (
        f"Expected '{expected}', got '{context.resolved_model}'"
    )


@then("the resolved actor provider model should be None")
def step_verify_resolved_provider_model_none(context: Context) -> None:
    """Verify the resolved provider/model is None."""
    assert context.resolved_model is None, (
        f"Expected None, got '{context.resolved_model}'"
    )
