"""Step definitions for plan_cancelled_enrichment.feature.

Tests that the PLAN_CANCELLED domain event is enriched with:
- Progress context: cancelled_phase, cancelled_processing_state,
  last_completed_step, subplan_count
- Resource cleanup context: sandbox_refs, changeset_id,
  resources_pending_cleanup
- Cancellation reason (existing field, verified still present)
- Backward-compatible fields: project_names, reason

Related: Forgejo issue #717
"""

from __future__ import annotations

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,
)
from cleveragents.config.settings import Settings
from cleveragents.domain.models.core.plan import ProjectLink
from cleveragents.infrastructure.events.types import EventType

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


class _RecordingBus:
    """Minimal event bus that records every emitted event."""

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

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

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


def _get_cancelled_event(context: Context):
    """Return the first PLAN_CANCELLED event from the recording bus."""
    events = [
        e
        for e in context.recording_bus.events
        if e.event_type == EventType.PLAN_CANCELLED
    ]
    assert events, (
        f"No PLAN_CANCELLED event found. "
        f"All events: {[e.event_type for e in context.recording_bus.events]}"
    )
    return events[0]


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


@given("I have a plan lifecycle service for cancelled enrichment tests")
def step_create_service_for_enrichment(context: Context) -> None:
    """Create a PlanLifecycleService with a recording event bus."""
    Settings._instance = None
    settings = Settings()
    context.recording_bus = _RecordingBus()
    context.service = PlanLifecycleService(
        settings=settings,
        event_bus=context.recording_bus,
    )
    context.error = None
    context.plan = None


# ---------------------------------------------------------------------------
# Given: action and plan creation
# ---------------------------------------------------------------------------


@given('an action "{action_name}" exists for enrichment tests')
def step_create_action_for_enrichment(context: Context, action_name: str) -> None:
    """Create an action with the given namespaced name."""
    context.action = context.service.create_action(
        name=action_name,
        description=f"Action {action_name} for enrichment tests",
        definition_of_done="Tests pass",
        strategy_actor="openai/gpt-4",
        execution_actor="openai/gpt-4",
    )


@given('a plan created from "{action_name}" for enrichment tests')
def step_create_plan_for_enrichment(context: Context, action_name: str) -> None:
    """Create a plan from the given action."""
    context.plan = context.service.use_action(
        action_name=action_name,
        project_links=[ProjectLink(project_name="test-project")],
    )


@given('a plan with sandbox refs created from "{action_name}"')
def step_create_plan_with_sandbox_refs(context: Context, action_name: str) -> None:
    """Create a plan and inject sandbox refs to simulate active sandboxes."""
    plan = context.service.use_action(
        action_name=action_name,
        project_links=[ProjectLink(project_name="test-project")],
    )
    # Inject sandbox refs directly (model is frozen, use model_copy)
    updated_plan = plan.model_copy(
        update={"sandbox_refs": ["sandbox-001", "sandbox-002"]}
    )
    # Update the in-memory store so cancel_plan retrieves the updated plan
    context.service._plans[updated_plan.identity.plan_id] = updated_plan
    context.plan = updated_plan


# ---------------------------------------------------------------------------
# When: cancel the plan
# ---------------------------------------------------------------------------


@when('I cancel the plan for enrichment tests with reason "{reason}"')
def step_cancel_plan_with_reason(context: Context, reason: str) -> None:
    """Cancel the plan with the given reason."""
    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=reason,
            )
    except Exception as exc:
        context.error = exc


@when("I cancel the plan for enrichment tests without a reason")
def step_cancel_plan_without_reason(context: Context) -> None:
    """Cancel the plan without providing a reason."""
    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,
            )
    except Exception as exc:
        context.error = exc


# ---------------------------------------------------------------------------
# Then: assertions on event details
# ---------------------------------------------------------------------------


@then('the PLAN_CANCELLED event details should include "{field}"')
def step_event_details_include_field(context: Context, field: str) -> None:
    """Assert that the PLAN_CANCELLED event details dict contains the field."""
    assert context.error is None, f"Unexpected error: {context.error}"
    event = _get_cancelled_event(context)
    assert field in event.details, (
        f"Expected field '{field}' in PLAN_CANCELLED event details. "
        f"Actual details keys: {list(event.details.keys())}"
    )


@then('the PLAN_CANCELLED event details should have reason "{expected_reason}"')
def step_event_details_reason(context: Context, expected_reason: str) -> None:
    """Assert the reason field in the event details matches expected value."""
    assert context.error is None, f"Unexpected error: {context.error}"
    event = _get_cancelled_event(context)
    actual = event.details.get("reason")
    assert actual == expected_reason, (
        f"Expected reason '{expected_reason}', got '{actual}'"
    )


@then("the PLAN_CANCELLED event reason field should be empty string")
def step_event_details_reason_empty(context: Context) -> None:
    """Assert the reason field in the event details is an empty string."""
    assert context.error is None, f"Unexpected error: {context.error}"
    event = _get_cancelled_event(context)
    actual = event.details.get("reason")
    assert actual == "", f"Expected empty string reason, got '{actual}'"


@then('the PLAN_CANCELLED event details should have cancelled_phase "{expected_phase}"')
def step_event_details_cancelled_phase(context: Context, expected_phase: str) -> None:
    """Assert the cancelled_phase field matches the expected phase string."""
    assert context.error is None, f"Unexpected error: {context.error}"
    event = _get_cancelled_event(context)
    actual = event.details.get("cancelled_phase")
    assert actual == expected_phase, (
        f"Expected cancelled_phase '{expected_phase}', got '{actual}'"
    )


@then(
    "the PLAN_CANCELLED event details should have last_completed_step {expected_step:d}"
)
def step_event_details_last_completed_step(
    context: Context, expected_step: int
) -> None:
    """Assert the last_completed_step field matches the expected value."""
    assert context.error is None, f"Unexpected error: {context.error}"
    event = _get_cancelled_event(context)
    actual = event.details.get("last_completed_step")
    assert actual == expected_step, (
        f"Expected last_completed_step {expected_step}, got {actual}"
    )


@then(
    "the PLAN_CANCELLED event details should have sandbox_refs count {expected_count:d}"
)
def step_event_details_sandbox_refs_count(
    context: Context, expected_count: int
) -> None:
    """Assert the sandbox_refs list has the expected number of entries."""
    assert context.error is None, f"Unexpected error: {context.error}"
    event = _get_cancelled_event(context)
    sandbox_refs = event.details.get("sandbox_refs", [])
    actual_count = len(sandbox_refs)
    assert actual_count == expected_count, (
        f"Expected {expected_count} sandbox_refs, got {actual_count}: {sandbox_refs}"
    )


@then(
    "the PLAN_CANCELLED event details should have resources_pending_cleanup"
    " {expected_count:d}"
)
def step_event_details_resources_pending_cleanup(
    context: Context, expected_count: int
) -> None:
    """Assert the resources_pending_cleanup count matches expected value."""
    assert context.error is None, f"Unexpected error: {context.error}"
    event = _get_cancelled_event(context)
    actual = event.details.get("resources_pending_cleanup")
    assert actual == expected_count, (
        f"Expected resources_pending_cleanup {expected_count}, got {actual}"
    )
