"""Step definitions for plan_lifecycle_rollback.feature.

Tests for PlanLifecycleService.rollback_plan() — the new service-layer
method that validates plan state and delegates to CheckpointService
before emitting PLAN_ROLLED_BACK domain events.

All step text uses the ``plr-`` prefix to avoid collisions with
existing step definitions.
"""

from __future__ import annotations

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_lifecycle_service import (
    PlanLifecycleService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import (
    BusinessRuleViolation,
    NotFoundError,
    PlanError,
    ResourceNotFoundError,
)
from cleveragents.domain.models.core.checkpoint import RollbackResult
from cleveragents.domain.models.core.plan import (
    PlanPhase,
    ProcessingState,
    ProjectLink,
)
from cleveragents.infrastructure.events.types import EventType

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

_PLAN_ID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
_CHECKPOINT_ID = "01BRZ4NFEK0000000000000000"
_NONEXISTENT_PLAN_ID = "01NONEXISTENT0000000000000"

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


def _make_mock_checkpoint_service(
    plan_id: str = _PLAN_ID,
    checkpoint_id: str = _CHECKPOINT_ID,
    *,
    side_effect: Exception | None = None,
) -> MagicMock:
    """Build a mock CheckpointService that returns a valid RollbackResult."""
    mock_svc = MagicMock()
    if side_effect is not None:
        mock_svc.selective_rollback.side_effect = side_effect
    else:
        mock_svc.selective_rollback.return_value = RollbackResult(
            restored_files_count=3,
            changed_paths=["src/foo.py", "src/bar.py", "tests/test_foo.py"],
            from_checkpoint_id=checkpoint_id,
        )
    return mock_svc


def _make_mock_event_bus() -> MagicMock:
    """Build a mock EventBus."""
    mock_bus = MagicMock()
    mock_bus.emitted_events: list[Any] = []

    def _emit(event: Any) -> None:
        mock_bus.emitted_events.append(event)

    mock_bus.emit.side_effect = _emit
    return mock_bus


def _create_service(
    checkpoint_service: Any = None,
    event_bus: Any = None,
) -> PlanLifecycleService:
    """Create a PlanLifecycleService with optional dependencies."""
    settings = Settings()
    return PlanLifecycleService(
        settings=settings,
        checkpoint_service=checkpoint_service,
        event_bus=event_bus,
    )


def _create_plan_in_service(
    service: PlanLifecycleService,
    phase: PlanPhase = PlanPhase.EXECUTE,
    state: ProcessingState = ProcessingState.PROCESSING,
) -> str:
    """Create an action and plan, then force the plan into the given phase/state."""
    service.create_action(
        name="local/test-rollback-action",
        description="Test action for rollback",
        definition_of_done="Tests pass",
        strategy_actor="local/test-strategist",
        execution_actor="local/test-executor",
    )
    plan = service.use_action(
        action_name="local/test-rollback-action",
        project_links=[ProjectLink(project_name="test-project")],
    )
    plan_id = plan.identity.plan_id

    # Force the plan into the desired phase/state by direct mutation
    # (avoids needing to run full actor pipeline in unit tests)
    plan.phase = phase
    plan.processing_state = state
    service._plans[plan_id] = plan

    return plan_id


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


@given("plr-a plan lifecycle service with a mock checkpoint service")
def step_plr_service_with_mock_checkpoint(context: Context) -> None:
    """Set up a PlanLifecycleService with a mock CheckpointService."""
    context.plr_mock_checkpoint_svc = _make_mock_checkpoint_service()
    context.plr_service = _create_service(
        checkpoint_service=context.plr_mock_checkpoint_svc,
    )
    context.plr_checkpoint_id = _CHECKPOINT_ID


@given("plr-a plan lifecycle service without a checkpoint service")
def step_plr_service_without_checkpoint(context: Context) -> None:
    """Set up a PlanLifecycleService with no CheckpointService."""
    context.plr_service = _create_service(checkpoint_service=None)
    context.plr_checkpoint_id = _CHECKPOINT_ID


@given("plr-a plan lifecycle service with a mock checkpoint service and event bus")
def step_plr_service_with_checkpoint_and_event_bus(context: Context) -> None:
    """Set up a PlanLifecycleService with mock CheckpointService and EventBus."""
    context.plr_mock_checkpoint_svc = _make_mock_checkpoint_service()
    context.plr_mock_event_bus = _make_mock_event_bus()
    context.plr_service = _create_service(
        checkpoint_service=context.plr_mock_checkpoint_svc,
        event_bus=context.plr_mock_event_bus,
    )
    context.plr_checkpoint_id = _CHECKPOINT_ID


@given("plr-a plan lifecycle service with a mock checkpoint service but no event bus")
def step_plr_service_with_checkpoint_no_event_bus(context: Context) -> None:
    """Set up a PlanLifecycleService with mock CheckpointService but no EventBus."""
    context.plr_mock_checkpoint_svc = _make_mock_checkpoint_service()
    context.plr_service = _create_service(
        checkpoint_service=context.plr_mock_checkpoint_svc,
        event_bus=None,
    )
    context.plr_checkpoint_id = _CHECKPOINT_ID


@given(
    "plr-a plan lifecycle service with a checkpoint service that raises ResourceNotFoundError"
)
def step_plr_service_checkpoint_raises_resource_not_found(context: Context) -> None:
    """Set up a service whose checkpoint service raises ResourceNotFoundError."""
    context.plr_mock_checkpoint_svc = _make_mock_checkpoint_service(
        side_effect=ResourceNotFoundError(
            "Checkpoint not found",
            resource_type="checkpoint",
            resource_id=_CHECKPOINT_ID,
        )
    )
    context.plr_service = _create_service(
        checkpoint_service=context.plr_mock_checkpoint_svc,
    )
    context.plr_checkpoint_id = _CHECKPOINT_ID


@given(
    "plr-a plan lifecycle service with a checkpoint service that raises BusinessRuleViolation"
)
def step_plr_service_checkpoint_raises_business_rule(context: Context) -> None:
    """Set up a service whose checkpoint service raises BusinessRuleViolation."""
    context.plr_mock_checkpoint_svc = _make_mock_checkpoint_service(
        side_effect=BusinessRuleViolation("Checkpoint rollback failed"),
    )
    context.plr_service = _create_service(
        checkpoint_service=context.plr_mock_checkpoint_svc,
    )
    context.plr_checkpoint_id = _CHECKPOINT_ID


@given("plr-a plan in Execute phase with a sandbox")
def step_plr_plan_in_execute_with_sandbox(context: Context) -> None:
    """Create a plan in Execute/PROCESSING state."""
    context.plr_plan_id = _create_plan_in_service(
        context.plr_service,
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.PROCESSING,
    )


@given("plr-a plan in Execute phase")
def step_plr_plan_in_execute(context: Context) -> None:
    """Create a plan in Execute/PROCESSING state (no sandbox needed for error tests)."""
    context.plr_plan_id = _create_plan_in_service(
        context.plr_service,
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.PROCESSING,
    )


@given("plr-a plan in APPLIED terminal state")
def step_plr_plan_in_applied_state(context: Context) -> None:
    """Create a plan in APPLIED terminal state."""
    context.plr_plan_id = _create_plan_in_service(
        context.plr_service,
        phase=PlanPhase.APPLY,
        state=ProcessingState.APPLIED,
    )


@given("plr-a plan in CANCELLED terminal state")
def step_plr_plan_in_cancelled_state(context: Context) -> None:
    """Create a plan in CANCELLED terminal state."""
    context.plr_plan_id = _create_plan_in_service(
        context.plr_service,
        phase=PlanPhase.APPLY,
        state=ProcessingState.CANCELLED,
    )


@given("plr-a plan in Execute/PROCESSING state with a sandbox")
def step_plr_plan_in_execute_processing(context: Context) -> None:
    """Create a plan in Execute/PROCESSING state."""
    context.plr_plan_id = _create_plan_in_service(
        context.plr_service,
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.PROCESSING,
    )


@given("plr-a plan in Execute/QUEUED state with a sandbox")
def step_plr_plan_in_execute_queued(context: Context) -> None:
    """Create a plan in Execute/QUEUED state."""
    context.plr_plan_id = _create_plan_in_service(
        context.plr_service,
        phase=PlanPhase.EXECUTE,
        state=ProcessingState.QUEUED,
    )


@given("plr-a plan in Strategize/QUEUED state with a sandbox")
def step_plr_plan_in_strategize_queued(context: Context) -> None:
    """Create a plan in Strategize/QUEUED state."""
    context.plr_plan_id = _create_plan_in_service(
        context.plr_service,
        phase=PlanPhase.STRATEGIZE,
        state=ProcessingState.QUEUED,
    )


@given("plr-a checkpoint exists for the plan")
def step_plr_checkpoint_exists(context: Context) -> None:
    """Checkpoint already set up via mock; this step is a no-op for clarity."""
    # The mock checkpoint service is already configured to return a valid
    # RollbackResult for any plan_id/checkpoint_id combination.
    pass


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


@when("plr-I call rollback_plan with the plan id and checkpoint id")
def step_plr_call_rollback_plan(context: Context) -> None:
    """Call rollback_plan on the service."""
    context.plr_raised_error = None
    context.plr_result = None
    try:
        context.plr_result = context.plr_service.rollback_plan(
            context.plr_plan_id,
            context.plr_checkpoint_id,
        )
    except (PlanError, NotFoundError, Exception) as exc:
        context.plr_raised_error = exc


@when("plr-I call rollback_plan with a non-existent plan id")
def step_plr_call_rollback_nonexistent(context: Context) -> None:
    """Call rollback_plan with a plan id that does not exist."""
    context.plr_raised_error = None
    context.plr_result = None
    try:
        context.plr_result = context.plr_service.rollback_plan(
            _NONEXISTENT_PLAN_ID,
            _CHECKPOINT_ID,
        )
    except (PlanError, NotFoundError, Exception) as exc:
        context.plr_raised_error = exc


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


@then("plr-the rollback should succeed")
def step_plr_rollback_succeeded(context: Context) -> None:
    """Assert that rollback_plan returned without raising an error."""
    assert context.plr_raised_error is None, (
        f"Expected rollback to succeed but got error: {context.plr_raised_error}"
    )
    assert context.plr_result is not None, (
        "Expected rollback_plan to return a RollbackResult, got None"
    )


@then("plr-the result should be a RollbackResult")
def step_plr_result_is_rollback_result(context: Context) -> None:
    """Assert that the result is a RollbackResult instance."""
    assert isinstance(context.plr_result, RollbackResult), (
        f"Expected RollbackResult, got {type(context.plr_result)}"
    )


@then("plr-the checkpoint service selective_rollback should have been called")
def step_plr_selective_rollback_called(context: Context) -> None:
    """Assert that CheckpointService.selective_rollback was called."""
    context.plr_mock_checkpoint_svc.selective_rollback.assert_called_once_with(
        context.plr_plan_id,
        context.plr_checkpoint_id,
    )


@then("plr-the checkpoint service selective_rollback should NOT have been called")
def step_plr_selective_rollback_not_called(context: Context) -> None:
    """Assert that CheckpointService.selective_rollback was NOT called."""
    context.plr_mock_checkpoint_svc.selective_rollback.assert_not_called()


@then('plr-a PlanError should be raised with "{message_fragment}"')
def step_plr_plan_error_raised(context: Context, message_fragment: str) -> None:
    """Assert that a PlanError was raised containing the given message fragment."""
    assert context.plr_raised_error is not None, (
        f"Expected a PlanError to be raised but no error was raised. "
        f"Result was: {context.plr_result}"
    )
    assert isinstance(context.plr_raised_error, PlanError), (
        f"Expected PlanError, got {type(context.plr_raised_error)}: "
        f"{context.plr_raised_error}"
    )
    error_message = str(context.plr_raised_error)
    assert message_fragment in error_message, (
        f"Expected error message to contain '{message_fragment}', "
        f"got: '{error_message}'"
    )


@then("plr-a NotFoundError should be raised")
def step_plr_not_found_error_raised(context: Context) -> None:
    """Assert that a NotFoundError was raised."""
    assert context.plr_raised_error is not None, (
        "Expected a NotFoundError to be raised but no error was raised."
    )
    assert isinstance(context.plr_raised_error, NotFoundError), (
        f"Expected NotFoundError, got {type(context.plr_raised_error)}: "
        f"{context.plr_raised_error}"
    )


@then("plr-a ResourceNotFoundError should be raised")
def step_plr_resource_not_found_error_raised(context: Context) -> None:
    """Assert that a ResourceNotFoundError was raised."""
    assert context.plr_raised_error is not None, (
        "Expected a ResourceNotFoundError to be raised but no error was raised."
    )
    assert isinstance(context.plr_raised_error, ResourceNotFoundError), (
        f"Expected ResourceNotFoundError, got {type(context.plr_raised_error)}: "
        f"{context.plr_raised_error}"
    )


@then("plr-a BusinessRuleViolation should be raised")
def step_plr_business_rule_violation_raised(context: Context) -> None:
    """Assert that a BusinessRuleViolation was raised."""
    assert context.plr_raised_error is not None, (
        "Expected a BusinessRuleViolation to be raised but no error was raised."
    )
    assert isinstance(context.plr_raised_error, BusinessRuleViolation), (
        f"Expected BusinessRuleViolation, got {type(context.plr_raised_error)}: "
        f"{context.plr_raised_error}"
    )


@then("plr-a PLAN_ROLLED_BACK domain event should have been emitted")
def step_plr_plan_rolled_back_event_emitted(context: Context) -> None:
    """Assert that a PLAN_ROLLED_BACK event was emitted."""
    emitted = context.plr_mock_event_bus.emitted_events
    rolled_back_events = [
        e for e in emitted if e.event_type == EventType.PLAN_ROLLED_BACK
    ]
    assert len(rolled_back_events) >= 1, (
        f"Expected at least one PLAN_ROLLED_BACK event, "
        f"got events: {[e.event_type for e in emitted]}"
    )


@then("plr-the event should contain the checkpoint_id")
def step_plr_event_contains_checkpoint_id(context: Context) -> None:
    """Assert that the PLAN_ROLLED_BACK event details contain the checkpoint_id."""
    emitted = context.plr_mock_event_bus.emitted_events
    rolled_back_events = [
        e for e in emitted if e.event_type == EventType.PLAN_ROLLED_BACK
    ]
    assert len(rolled_back_events) >= 1, "No PLAN_ROLLED_BACK event found"
    event = rolled_back_events[0]
    details = event.details or {}
    assert "checkpoint_id" in details, (
        f"Expected 'checkpoint_id' in event details, got: {details}"
    )
    assert details["checkpoint_id"] == context.plr_checkpoint_id, (
        f"Expected checkpoint_id '{context.plr_checkpoint_id}', "
        f"got '{details['checkpoint_id']}'"
    )
