"""Step definitions for plan_lifecycle_service_coverage_boost_r2.feature.

Targets uncovered lines in PlanLifecycleService (99 lines remaining):
- Lines 263-276: _consult_error_patterns guidance injection path
- Lines 277-282: _consult_error_patterns exception handler
- Line 339:      save_plan public convenience method
- Lines 416-422: _resolve_actor_registry_entry exception handler
- Lines 778-784: use_action event_bus.emit exception handler
- Lines 847-850: list_plans DatabaseError fallback
- Lines 909-913: start_strategize action loading from persistence
- Lines 1070-1080: execute_plan event_bus.emit exception handler
- Lines 1256-1282: complete_apply event_bus emit (success + exception)
- Lines 1366-1388: cancel_plan event_bus emit (success + exception)
- Lines 1417-1426: _cleanup_devcontainers (success + exception)
"""

from __future__ import annotations

import tempfile
from contextlib import contextmanager
from unittest.mock import MagicMock, patch

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

from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import DatabaseError
from cleveragents.domain.models.core.error_pattern import (
    ErrorPattern,
    PreventiveGuidance,
)
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    ProcessingState,
    ProjectLink,
)
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
from cleveragents.infrastructure.events.types import EventType

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


@given("I have a fresh plan lifecycle service for coverage boost r2")
def step_create_fresh_service_r2(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_br2(context: Context, name: str, **kwargs):
    """Helper to create a basic action with sensible defaults."""
    defaults = {
        "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_plan_to_strategize_complete(context: Context):
    """Advance the current plan to Strategize/COMPLETE."""
    pid = context.plan.identity.plan_id
    context.service.start_strategize(pid)
    # Manually set to COMPLETE to avoid auto-progress side effects
    plan = context.service.get_plan(pid)
    plan.processing_state = ProcessingState.COMPLETE
    context.plan = plan


def _advance_plan_to_execute_complete(context: Context):
    """Advance plan from Strategize/COMPLETE through Execute to COMPLETE."""
    pid = context.plan.identity.plan_id
    # First transition to execute
    context.service.execute_plan(pid)
    context.service.start_execute(pid)
    # Manually set to COMPLETE to avoid auto-progress side effects
    plan = context.service.get_plan(pid)
    plan.processing_state = ProcessingState.COMPLETE
    context.plan = plan


def _advance_plan_to_apply_processing(context: Context):
    """Advance plan all the way to Apply/PROCESSING."""
    _advance_plan_to_strategize_complete(context)
    _advance_plan_to_execute_complete(context)
    pid = context.plan.identity.plan_id
    context.service.apply_plan(pid)
    context.service.start_apply(pid)
    context.plan = context.service.get_plan(pid)


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

    def __init__(self):
        self.events = []

    def emit(self, event):
        self.events.append(event)

    def subscribe(self, event_type, handler):
        pass


class FailingEventBus:
    """An event bus that always raises on emit."""

    def emit(self, event):
        raise RuntimeError("Simulated event bus failure")

    def subscribe(self, event_type, handler):
        pass


# =================================================================
# Lines 263-276: _consult_error_patterns — guidance injection
# =================================================================


@given("a plan lifecycle service with a configured error pattern service for r2")
def step_service_with_error_pattern_service(context: Context) -> None:
    """Create a service with an ErrorPatternService that returns guidance."""
    Settings._instance = None
    settings = Settings()

    mock_eps = MagicMock()
    mock_eps.match_patterns.return_value = PreventiveGuidance(
        matched_patterns=(
            ErrorPattern(
                pattern="test pattern",
                historical_failures=("failure-1",),
                preventive_checks=("Check imports", "Validate config"),
                keywords=("test",),
            ),
        ),
        preventive_checks=("Check imports", "Validate config"),
    )

    context.service = PlanLifecycleService(
        settings=settings,
        error_pattern_service=mock_eps,
    )
    context.error = None


@given('an action "{name}" exists for coverage boost r2')
def step_create_action_br2(context: Context, name: str) -> None:
    """Create an action with the given name."""
    context.action = _create_action_br2(context, name)


@given("a plan advanced to strategize complete for coverage boost r2")
def step_plan_to_strategize_complete_br2(context: Context) -> None:
    """Create a plan and advance it to Strategize/COMPLETE."""
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-br2")],
    )
    _advance_plan_to_strategize_complete(context)


@when("I execute the plan so error patterns are consulted")
def step_execute_with_error_patterns(context: Context) -> None:
    """Execute the plan — _consult_error_patterns should inject guidance."""
    context.error = None
    try:
        context.plan = context.service.execute_plan(context.plan.identity.plan_id)
    except Exception as e:
        context.error = e


@then("the plan error_details should contain preventive_guidance")
def step_verify_preventive_guidance(context: Context) -> None:
    """Verify that preventive guidance was injected into error_details."""
    assert context.error is None, f"Unexpected error: {context.error}"
    details = context.plan.error_details
    assert details is not None, "Expected error_details to be set"
    assert "preventive_guidance" in details, (
        f"Expected 'preventive_guidance' in error_details, got keys: {list(details.keys())}"
    )
    assert "PREVENTIVE GUIDANCE" in details["preventive_guidance"], (
        f"Expected formatted guidance text, got: {details['preventive_guidance']}"
    )


# =================================================================
# Lines 277-282: _consult_error_patterns — exception handler
# =================================================================


@given("a plan lifecycle service with a failing error pattern service for r2")
def step_service_with_failing_eps(context: Context) -> None:
    """Create a service with an ErrorPatternService that raises."""
    Settings._instance = None
    settings = Settings()

    mock_eps = MagicMock()
    mock_eps.match_patterns.side_effect = RuntimeError("Simulated pattern DB failure")

    context.service = PlanLifecycleService(
        settings=settings,
        error_pattern_service=mock_eps,
    )
    context.error = None


@when("I execute the plan and error pattern consultation fails")
def step_execute_with_failing_eps(context: Context) -> None:
    """Execute the plan — _consult_error_patterns should catch the exception."""
    context.error = None
    try:
        context.plan = context.service.execute_plan(context.plan.identity.plan_id)
    except Exception as e:
        context.error = e


@then("the plan should still transition to execute phase without error")
def step_verify_execute_despite_eps_failure(context: Context) -> None:
    """Verify the plan transitioned to Execute despite EPS failure."""
    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}"
    )


# =================================================================
# Line 339: save_plan — public convenience method
# =================================================================


@given('a plan created from "{action_name}" for coverage boost r2')
def step_create_plan_br2(context: Context, action_name: str) -> None:
    """Create a plan from the named action."""
    context.plan = context.service.use_action(
        action_name=action_name,
        project_links=[ProjectLink(project_name="proj-br2")],
    )


@when("I call save_plan on the plan")
def step_call_save_plan(context: Context) -> None:
    """Call save_plan — should be a no-op in in-memory mode."""
    context.error = None
    try:
        context.service.save_plan(context.plan)
    except Exception as e:
        context.error = e


@then("save_plan completes without error")
def step_verify_save_plan_no_error(context: Context) -> None:
    """Verify save_plan did not raise."""
    assert context.error is None, (
        f"Expected no error from save_plan, got: {context.error}"
    )


# =================================================================
# Lines 416-422: _resolve_actor_registry_entry exception handler
# =================================================================


@given("a plan lifecycle service with a mock unit of work that fails actor lookup")
def step_service_with_failing_actor_uow(context: Context) -> None:
    """Create a service with a mock UoW where actors.get_by_name raises."""
    Settings._instance = None
    settings = Settings()

    mock_uow = MagicMock()
    mock_ctx = MagicMock()
    mock_ctx.actors.get_by_name.side_effect = RuntimeError("DB connection failed")

    @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


@when('I resolve an actor registry entry for "{actor_name}"')
def step_resolve_actor_entry(context: Context, actor_name: str) -> None:
    """Call _resolve_actor_registry_entry directly."""
    context.resolved_entry = context.service._resolve_actor_registry_entry(actor_name)


@then("the resolved actor entry should be None")
def step_verify_resolved_none(context: Context) -> None:
    """Verify the resolved entry is None (exception was caught)."""
    assert context.resolved_entry is None, (
        f"Expected None, got {context.resolved_entry}"
    )


# =================================================================
# Lines 778-784: use_action — event_bus.emit exception
# =================================================================


@given("a plan lifecycle service with a failing event bus for r2")
def step_service_with_failing_event_bus(context: Context) -> None:
    """Create a service with an event bus that raises on emit."""
    Settings._instance = None
    settings = Settings()
    context.service = PlanLifecycleService(
        settings=settings,
        event_bus=FailingEventBus(),
    )
    context.error = None


@when('I use action "{action_name}" and event bus emit fails')
def step_use_action_with_failing_bus(context: Context, action_name: str) -> None:
    """Use an action — the event bus will raise but the plan should still be created."""
    context.error = None
    try:
        context.plan = context.service.use_action(
            action_name=action_name,
            project_links=[ProjectLink(project_name="proj-event-fail")],
        )
    except Exception as e:
        context.error = e


@then("the plan should still be created in strategize phase")
def step_verify_plan_created_despite_bus_fail(context: Context) -> None:
    """Verify the plan was created even though event bus failed."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan is not None
    assert context.plan.phase == PlanPhase.STRATEGIZE, (
        f"Expected STRATEGIZE, got {context.plan.phase}"
    )


# =================================================================
# Lines 847-850: list_plans — DatabaseError fallback
# =================================================================


@given("a plan lifecycle service with a mock unit of work that fails list_all")
def step_service_with_failing_list_all(context: Context) -> None:
    """Create a service with a mock UoW where lifecycle_plans.list_all raises DatabaseError."""
    Settings._instance = None
    settings = Settings()

    mock_uow = MagicMock()
    mock_ctx = MagicMock()
    mock_ctx.lifecycle_plans.list_all.side_effect = DatabaseError("DB read failed")

    @contextmanager
    def _fake_transaction():
        yield mock_ctx

    mock_uow.transaction = _fake_transaction

    context.service = PlanLifecycleService(
        settings=settings,
        unit_of_work=mock_uow,
    )
    # Also need to mock create so create_action doesn't fail
    mock_ctx.actions.create.return_value = None
    context.error = None


@given("an in-memory plan exists for the fallback test")
def step_add_inmemory_plan(context: Context) -> None:
    """Create an action and plan in-memory so list_plans returns something."""
    # We create the action directly in-memory (no persistence needed)
    action = _create_action_br2(context, "local/fallback-test")
    context.plan = context.service.use_action(
        action_name=str(action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-fallback")],
    )


@when("I call list_plans on the service with the broken persistence")
def step_call_list_plans_with_broken_persistence(context: Context) -> None:
    """Call list_plans — should fall back to in-memory cache."""
    context.error = None
    try:
        context.plan_list = context.service.list_plans()
    except Exception as e:
        context.error = e


@then("the result should contain the in-memory plan")
def step_verify_fallback_result(context: Context) -> None:
    """Verify the in-memory plan is in the result list."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert len(context.plan_list) >= 1, (
        f"Expected at least 1 plan, got {len(context.plan_list)}"
    )
    plan_ids = [p.identity.plan_id for p in context.plan_list]
    assert context.plan.identity.plan_id in plan_ids, (
        f"Expected plan {context.plan.identity.plan_id} in results {plan_ids}"
    )


# =================================================================
# Lines 909-913: start_strategize — action loading from persistence
# =================================================================


@given("a plan lifecycle service with a persisted action for cache miss test")
def step_service_with_persisted_action_cache_miss(context: Context) -> None:
    """Create a service backed by a real DB with an action + plan, then clear cache."""
    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as db_file:
        db_path = db_file.name
    db_url = f"sqlite:///{db_path}"
    uow = UnitOfWork(database_url=db_url)
    uow.init_database()

    Settings._instance = None
    settings = Settings()
    context.service = PlanLifecycleService(settings=settings, unit_of_work=uow)
    context.uow = uow

    # Create action and plan (both persisted to DB)
    context.action = _create_action_br2(context, "local/cache-miss-test")
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-cache-miss")],
    )
    context.error = None


@given("a plan whose action is removed from the in-memory cache")
def step_clear_actions_cache(context: Context) -> None:
    """Remove the action from the in-memory _actions dict to force DB lookup."""
    context.service._actions.clear()


@when("I start strategize on the plan with the cleared cache")
def step_start_strategize_cache_miss(context: Context) -> None:
    """Start strategize — should load action from persistence layer."""
    context.error = None
    try:
        pid = context.plan.identity.plan_id
        context.plan = context.service.start_strategize(pid)
    except Exception as e:
        context.error = e


@then("start_strategize should complete successfully")
def step_verify_strategize_started(context: Context) -> None:
    """Verify start_strategize completed and the action was loaded from DB."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.processing_state == ProcessingState.PROCESSING, (
        f"Expected PROCESSING, got {context.plan.processing_state}"
    )
    # Verify the action was loaded back into the cache
    assert len(context.service._actions) >= 1, (
        "Expected action to be loaded back into _actions cache"
    )


# =================================================================
# Lines 1070-1080: execute_plan — event_bus.emit exception
# =================================================================


@when("I execute the plan and event bus emit fails during phase change")
def step_execute_plan_with_failing_bus(context: Context) -> None:
    """Execute the plan — event bus will raise but plan should still transition."""
    context.error = None
    try:
        context.plan = context.service.execute_plan(context.plan.identity.plan_id)
    except Exception as e:
        context.error = e


@then("the plan should still transition to execute phase")
def step_verify_execute_despite_bus_fail(context: Context) -> None:
    """Verify the plan transitioned to Execute despite event bus failure."""
    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}"
    )


# =================================================================
# Lines 1256-1276: complete_apply — event_bus emit success
# =================================================================


@given("a plan lifecycle service with a recording event bus for r2")
def step_service_with_recording_event_bus(context: Context) -> None:
    """Create a service with a recording event bus."""
    Settings._instance = None
    settings = Settings()
    context.recording_bus = RecordingEventBus()
    context.service = PlanLifecycleService(
        settings=settings,
        event_bus=context.recording_bus,
    )
    context.error = None


@given("a plan advanced to apply processing for coverage boost r2")
def step_plan_to_apply_processing_br2(context: Context) -> None:
    """Create a plan and advance it to Apply/PROCESSING."""
    context.plan = context.service.use_action(
        action_name=str(context.action.namespaced_name),
        project_links=[ProjectLink(project_name="proj-br2")],
    )
    _advance_plan_to_apply_processing(context)


@when("I complete apply on the plan with event bus")
def step_complete_apply_with_event_bus(context: Context) -> None:
    """Complete apply — should emit PLAN_APPLIED event."""
    context.error = None
    try:
        with patch(
            "cleveragents.application.services.plan_lifecycle_service.CleanupService",
            create=True,
        ) as mock_cleanup_cls:
            mock_cleanup_cls.stop_active_devcontainers.return_value = []
            # Patch the import inside _cleanup_devcontainers
            with patch(
                "cleveragents.application.services.cleanup_service.stop_all_active_containers",
                return_value=[],
            ):
                context.plan = context.service.complete_apply(
                    context.plan.identity.plan_id
                )
    except Exception as e:
        context.error = e


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


# =================================================================
# Lines 1277-1282: complete_apply — event_bus emit exception
# =================================================================


@when("I complete apply on the plan and event bus emit fails")
def step_complete_apply_with_failing_bus(context: Context) -> None:
    """Complete apply — event bus will raise but plan should still be applied."""
    context.error = None
    try:
        with patch(
            "cleveragents.application.services.cleanup_service.stop_all_active_containers",
            return_value=[],
        ):
            context.plan = context.service.complete_apply(context.plan.identity.plan_id)
    except Exception as e:
        context.error = e


@then("the plan should still be in applied state")
def step_verify_applied_despite_bus_fail(context: Context) -> None:
    """Verify the plan is in APPLIED state despite event bus failure."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.processing_state == ProcessingState.APPLIED, (
        f"Expected APPLIED, got {context.plan.processing_state}"
    )


# =================================================================
# Lines 1366-1378: cancel_plan — event_bus emit success
# =================================================================


@when("I cancel the plan with event bus recording")
def step_cancel_plan_with_recording_bus(context: Context) -> None:
    """Cancel the plan — should emit PLAN_CANCELLED event."""
    context.error = None
    try:
        with patch(
            "cleveragents.application.services.cleanup_service.stop_all_active_containers",
            return_value=[],
        ):
            context.plan = context.service.cancel_plan(
                context.plan.identity.plan_id,
                reason="test cancellation",
            )
    except Exception as e:
        context.error = e


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


# =================================================================
# Lines 1382-1388: cancel_plan — event_bus emit exception
# =================================================================


@when("I cancel the plan and event bus emit fails")
def step_cancel_plan_with_failing_bus(context: Context) -> None:
    """Cancel the plan — event bus will raise but plan should still be cancelled."""
    context.error = None
    try:
        with patch(
            "cleveragents.application.services.cleanup_service.stop_all_active_containers",
            return_value=[],
        ):
            context.plan = context.service.cancel_plan(
                context.plan.identity.plan_id,
                reason="test cancellation failure",
            )
    except Exception as e:
        context.error = e


@then("the plan should still be in cancelled state")
def step_verify_cancelled_despite_bus_fail(context: Context) -> None:
    """Verify the plan is in CANCELLED state despite event bus failure."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.processing_state == ProcessingState.CANCELLED, (
        f"Expected CANCELLED, got {context.plan.processing_state}"
    )


# =================================================================
# Lines 1417-1421: _cleanup_devcontainers — success path
# =================================================================


@given("a plan lifecycle service with mocked devcontainer cleanup for r2")
def step_service_with_mocked_cleanup(context: Context) -> None:
    """Create a service where devcontainer cleanup returns stopped containers."""
    Settings._instance = None
    settings = Settings()
    context.service = PlanLifecycleService(settings=settings)
    context.cleanup_called = False
    context.error = None


@when("I complete apply and devcontainer cleanup returns stopped containers")
def step_complete_apply_with_cleanup_success(context: Context) -> None:
    """Complete apply with mocked cleanup that returns container IDs."""
    context.error = None
    try:
        with patch(
            "cleveragents.application.services.cleanup_service.stop_all_active_containers",
            return_value=["container-1", "container-2"],
        ):
            context.plan = context.service.complete_apply(context.plan.identity.plan_id)
            context.cleanup_called = True
    except Exception as e:
        context.error = e


@then("the plan should be applied and cleanup should have been called")
def step_verify_applied_with_cleanup(context: Context) -> None:
    """Verify the plan is APPLIED and cleanup was invoked."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.processing_state == ProcessingState.APPLIED, (
        f"Expected APPLIED, got {context.plan.processing_state}"
    )
    assert context.cleanup_called, "Expected cleanup to have been called"


# =================================================================
# Lines 1422-1426: _cleanup_devcontainers — exception path
# =================================================================


@given("a plan lifecycle service with failing devcontainer cleanup for r2")
def step_service_with_failing_cleanup(context: Context) -> None:
    """Create a service where devcontainer cleanup will raise."""
    Settings._instance = None
    settings = Settings()
    context.service = PlanLifecycleService(settings=settings)
    context.error = None


@when("I complete apply and devcontainer cleanup raises an exception")
def step_complete_apply_with_cleanup_failure(context: Context) -> None:
    """Complete apply with mocked cleanup that raises."""
    context.error = None
    try:
        with patch(
            "cleveragents.application.services.cleanup_service.stop_all_active_containers",
            side_effect=RuntimeError("Docker daemon not available"),
        ):
            context.plan = context.service.complete_apply(context.plan.identity.plan_id)
    except Exception as e:
        context.error = e


@then("the plan should still be applied despite cleanup failure")
def step_verify_applied_despite_cleanup_failure(context: Context) -> None:
    """Verify the plan is APPLIED even though cleanup failed."""
    assert context.error is None, f"Expected no error, got: {context.error}"
    assert context.plan.processing_state == ProcessingState.APPLIED, (
        f"Expected APPLIED, got {context.plan.processing_state}"
    )
