"""Step definitions for event_bus.feature.

Tests EventType, DomainEvent, EventBus protocol, ReactiveEventBus,
LoggingEventBus, service event emission, and DI container registration.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]
from ulid import ULID

from cleveragents.application.container import Container
from cleveragents.application.services.decision_service import DecisionService
from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
)
from cleveragents.domain.models.core.decision import DecisionType
from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState
from cleveragents.infrastructure.events import (
    DomainEvent,
    EventBus,
    EventType,
    LoggingEventBus,
    ReactiveEventBus,
)

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


def _make_event(event_type_str: str, **kwargs: Any) -> DomainEvent:
    return DomainEvent(event_type=EventType(event_type_str), **kwargs)


class _EventCollector:
    """Simple in-test stub that collects emitted events."""

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

    def __call__(self, event: DomainEvent) -> None:
        self.received.append(event)


# ---------------------------------------------------------------------------
# EventType enum steps
# ---------------------------------------------------------------------------


@then('EventType should include "{value}"')
def step_event_type_includes(ctx: Context, value: str) -> None:
    values = {e.value for e in EventType}
    assert value in values, f"{value!r} not found in EventType values: {values}"


@then('EventType member {member} should equal string "{value}"')
def step_event_type_member_equals(ctx: Context, member: str, value: str) -> None:
    et = EventType[member]
    assert et == value, f"EventType.{member} = {et!r}, expected {value!r}"


# ---------------------------------------------------------------------------
# DomainEvent steps
# ---------------------------------------------------------------------------


@when('I create a minimal DomainEvent with event_type "{et}"')
def step_create_domain_event(ctx: Context, et: str) -> None:
    ctx.domain_event = _make_event(et)


@when(
    'I create a DomainEvent with event_type "{et}" plan_id "{pid}"'
    ' and details key "{key}" value "{val}"'
)
def step_create_domain_event_with_details(
    ctx: Context, et: str, pid: str, key: str, val: str
) -> None:
    ctx.domain_event = _make_event(et, plan_id=pid, details={key: val})


@then('the DomainEvent event_type should be "{et}"')
def step_domain_event_type(ctx: Context, et: str) -> None:
    assert str(ctx.domain_event.event_type) == et


@then("the DomainEvent should have a non-empty correlation_id")
def step_domain_event_correlation_id(ctx: Context) -> None:
    cid = ctx.domain_event.correlation_id
    assert cid and len(cid) == 26, f"Expected 26-char ULID, got {cid!r}"


@then("the DomainEvent should have a timestamp")
def step_domain_event_timestamp(ctx: Context) -> None:
    assert ctx.domain_event.timestamp is not None


@then("the DomainEvent plan_id should be None")
def step_domain_event_plan_id_none(ctx: Context) -> None:
    assert ctx.domain_event.plan_id is None


@then("the DomainEvent actor_name should be None")
def step_domain_event_actor_name_none(ctx: Context) -> None:
    assert ctx.domain_event.actor_name is None


@then("the DomainEvent details should be empty")
def step_domain_event_details_empty(ctx: Context) -> None:
    assert ctx.domain_event.details == {}


@then('the DomainEvent plan_id should be "{pid}"')
def step_domain_event_plan_id(ctx: Context, pid: str) -> None:
    assert ctx.domain_event.plan_id == pid


@then('the DomainEvent details should contain key "{key}"')
def step_domain_event_details_key(ctx: Context, key: str) -> None:
    assert key in ctx.domain_event.details


@then("modifying the DomainEvent should raise an error")
def step_domain_event_immutable(ctx: Context) -> None:
    raised = False
    try:
        ctx.domain_event.event_type = EventType.PLAN_CANCELLED  # type: ignore[misc]
    except Exception:
        raised = True
    assert raised, "Expected an exception when mutating a frozen DomainEvent"


# ---------------------------------------------------------------------------
# Protocol conformance
# ---------------------------------------------------------------------------


@then("ReactiveEventBus should satisfy the EventBus protocol")
def step_reactive_satisfies_protocol(ctx: Context) -> None:
    bus = ReactiveEventBus()
    assert isinstance(bus, EventBus), "ReactiveEventBus does not satisfy EventBus"


@then("LoggingEventBus should satisfy the EventBus protocol")
def step_logging_satisfies_protocol(ctx: Context) -> None:
    bus = LoggingEventBus()
    assert isinstance(bus, EventBus), "LoggingEventBus does not satisfy EventBus"


# ---------------------------------------------------------------------------
# ReactiveEventBus given
# ---------------------------------------------------------------------------


@given("a ReactiveEventBus")
def step_given_reactive_bus(ctx: Context) -> None:
    ctx.bus = ReactiveEventBus()
    ctx.collectors: list[_EventCollector] = []


@given("a LoggingEventBus")
def step_given_logging_bus(ctx: Context) -> None:
    ctx.bus = LoggingEventBus()
    ctx.collectors = []


# ---------------------------------------------------------------------------
# ReactiveEventBus / LoggingEventBus when
# ---------------------------------------------------------------------------


@when('I subscribe to "{et}" events')
def step_subscribe_single(ctx: Context, et: str) -> None:
    collector = _EventCollector()
    ctx.collectors.append(collector)
    ctx.bus.subscribe(EventType(et), collector)


@when('I subscribe two handlers to "{et}" events')
def step_subscribe_two(ctx: Context, et: str) -> None:
    for _ in range(2):
        collector = _EventCollector()
        ctx.collectors.append(collector)
        ctx.bus.subscribe(EventType(et), collector)


@when('I emit a "{et}" DomainEvent')
def step_emit_event(ctx: Context, et: str) -> None:
    ctx.bus.emit(_make_event(et))


# ---------------------------------------------------------------------------
# ReactiveEventBus then
# ---------------------------------------------------------------------------


@then("the handler should have received {n:d} event")
def step_handler_received_n(ctx: Context, n: int) -> None:
    assert ctx.collectors, "No collectors registered"
    count = len(ctx.collectors[0].received)
    assert count == n, f"Expected {n} events, got {count}"


@then('the received event type should be "{et}"')
def step_received_event_type(ctx: Context, et: str) -> None:
    assert ctx.collectors[0].received, "No events received"
    assert str(ctx.collectors[0].received[0].event_type) == et


@then("each handler should have received {n:d} event")
def step_each_handler_received(ctx: Context, n: int) -> None:
    for i, col in enumerate(ctx.collectors):
        assert len(col.received) == n, (
            f"Handler {i} received {len(col.received)}, expected {n}"
        )


@then("the bus should expose an observable stream")
def step_bus_has_stream(ctx: Context) -> None:
    stream = ctx.bus.stream
    assert stream is not None, "Expected a non-None observable stream"


@then("emitting a non-DomainEvent should raise TypeError")
def step_emit_non_domain_event(ctx: Context) -> None:
    raised = False
    try:
        ctx.bus.emit("not-an-event")  # type: ignore[arg-type]
    except TypeError:
        raised = True
    assert raised, "Expected TypeError for non-DomainEvent"


@then("subscribing with a non-EventType should raise TypeError")
def step_subscribe_bad_type(ctx: Context) -> None:
    raised = False
    try:
        ctx.bus.subscribe("plan.created", lambda e: None)  # type: ignore[arg-type]
    except TypeError:
        raised = True
    assert raised, "Expected TypeError for non-EventType event_type"


@then("subscribing with a non-callable handler should raise TypeError")
def step_subscribe_non_callable(ctx: Context) -> None:
    raised = False
    try:
        ctx.bus.subscribe(EventType.PLAN_CREATED, "not-callable")  # type: ignore[arg-type]
    except TypeError:
        raised = True
    assert raised, "Expected TypeError for non-callable handler"


@then("the EventBus Protocol stubs should be callable")
def step_protocol_stubs_callable(ctx: Context) -> None:
    """Exercise Protocol method stubs directly to ensure 100% coverage."""

    class _BareImpl(EventBus):  # type: ignore[misc]
        pass

    obj = _BareImpl()
    event = _make_event("plan.created")
    obj.emit(event)
    obj.subscribe(EventType.PLAN_CREATED, lambda e: None)


# ---------------------------------------------------------------------------
# DecisionService event emission steps
# ---------------------------------------------------------------------------


@given("a DecisionService with an injected event bus")
def step_given_decision_service_with_bus(ctx: Context) -> None:
    from unittest.mock import MagicMock, create_autospec

    from cleveragents.config.settings import Settings
    from cleveragents.infrastructure.database.unit_of_work import UnitOfWork

    ctx.all_events: list[DomainEvent] = []
    ctx.event_bus = ReactiveEventBus()
    ctx.event_bus.subscribe(EventType.DECISION_CREATED, ctx.all_events.append)

    mock_uow = create_autospec(UnitOfWork, instance=True)
    mock_tx_ctx = MagicMock()
    mock_tx_ctx.decisions = MagicMock()
    mock_uow.transaction.return_value.__enter__.return_value = mock_tx_ctx
    mock_uow.transaction.return_value.__exit__.return_value = False

    ctx.decision_service = DecisionService(
        settings=create_autospec(Settings, instance=True),
        unit_of_work=mock_uow,
        event_bus=ctx.event_bus,
    )
    ctx.test_plan_id = str(ULID())


@given("a DecisionService without an event bus")
def step_given_decision_service_no_bus(ctx: Context) -> None:
    from unittest.mock import MagicMock, create_autospec

    from cleveragents.config.settings import Settings
    from cleveragents.infrastructure.database.unit_of_work import UnitOfWork

    mock_uow = create_autospec(UnitOfWork, instance=True)
    mock_tx_ctx = MagicMock()
    mock_tx_ctx.decisions = MagicMock()
    mock_uow.transaction.return_value.__enter__.return_value = mock_tx_ctx
    mock_uow.transaction.return_value.__exit__.return_value = False

    ctx.decision_service = DecisionService(
        settings=create_autospec(Settings, instance=True),
        unit_of_work=mock_uow,
    )
    ctx.test_plan_id = str(ULID())


@when("I call record_decision")
def step_call_record_decision(ctx: Context) -> None:
    ctx.exception = None
    try:
        ctx.decision_service.record_decision(
            plan_id=ctx.test_plan_id,
            decision_type=DecisionType.STRATEGY_CHOICE,
            question="Which approach?",
            chosen_option="TDD",
            rationale="Test-first",
        )
    except Exception as exc:
        ctx.exception = exc


@then('the event bus should have received a "{et}" event')
def step_event_bus_received(ctx: Context, et: str) -> None:
    all_events: list[DomainEvent] = getattr(ctx, "all_events", [])
    received_types = [str(e.event_type) for e in all_events]
    assert et in received_types, (
        f"Expected {et!r} in received events, got {received_types}"
    )


@then("the emitted event plan_id should match the decision plan_id")
def step_emitted_plan_id_matches(ctx: Context) -> None:
    all_events: list[DomainEvent] = getattr(ctx, "all_events", [])
    assert all_events, "No events received"
    emitted_plan_id = all_events[0].plan_id
    assert emitted_plan_id == ctx.test_plan_id, (
        f"Expected plan_id {ctx.test_plan_id!r}, got {emitted_plan_id!r}"
    )


@then("no event bus exception should be raised")
def step_no_event_bus_exception(ctx: Context) -> None:
    assert ctx.exception is None, f"Unexpected exception: {ctx.exception}"


# ---------------------------------------------------------------------------
# PlanLifecycleService event emission steps
# ---------------------------------------------------------------------------


def _make_settings_stub() -> MagicMock:
    from unittest.mock import create_autospec

    from cleveragents.config.settings import Settings

    return create_autospec(Settings, instance=True)


@given("a PlanLifecycleService with an injected event bus")
def step_given_lifecycle_service_with_bus(ctx: Context) -> None:
    ctx.all_events: list[DomainEvent] = []
    ctx.event_bus = ReactiveEventBus()
    ctx.event_bus.subscribe(EventType.PLAN_CREATED, ctx.all_events.append)

    ctx.lifecycle_service = PlanLifecycleService(
        settings=_make_settings_stub(),
        event_bus=ctx.event_bus,
    )
    ctx.exception = None


@given("a PlanLifecycleService with an injected event bus and a strategized plan")
def step_given_lifecycle_with_strategized_plan(ctx: Context) -> None:
    ctx.all_events: list[DomainEvent] = []
    ctx.event_bus = ReactiveEventBus()
    ctx.event_bus.subscribe(EventType.PLAN_PHASE_CHANGED, ctx.all_events.append)

    ctx.lifecycle_service = PlanLifecycleService(
        settings=_make_settings_stub(),
        event_bus=ctx.event_bus,
    )
    ctx.exception = None

    ctx.lifecycle_service.create_action(
        name="test/action",
        description="benchmark action",
        definition_of_done="Done",
        strategy_actor="local/strategist",
        execution_actor="local/executor",
    )
    plan = ctx.lifecycle_service.use_action(action_name="test/action")
    plan_id = plan.identity.plan_id
    ctx.lifecycle_service._plans[plan_id] = plan.model_copy(
        update={
            "phase": PlanPhase.STRATEGIZE,
            "processing_state": ProcessingState.COMPLETE,
        }
    )
    ctx.test_plan_id = plan_id


@given("a PlanLifecycleService without an event bus")
def step_given_lifecycle_service_no_bus(ctx: Context) -> None:
    ctx.lifecycle_service = PlanLifecycleService(
        settings=_make_settings_stub(),
    )
    ctx.exception = None


@when("I call use_action to create a plan")
def step_call_use_action(ctx: Context) -> None:
    try:
        ctx.lifecycle_service.create_action(
            name="test/action",
            description="test action",
            definition_of_done="Done when implemented",
            strategy_actor="local/strategist",
            execution_actor="local/executor",
        )
        ctx.lifecycle_service.use_action(action_name="test/action")
    except Exception as exc:
        ctx.exception = exc


@when("I call execute_plan")
def step_call_execute_plan(ctx: Context) -> None:
    try:
        ctx.lifecycle_service.execute_plan(ctx.test_plan_id)
    except Exception as exc:
        ctx.exception = exc


# ---------------------------------------------------------------------------
# DI container steps
# ---------------------------------------------------------------------------


@when("I resolve event_bus from the DI container twice")
def step_resolve_event_bus_twice(ctx: Context) -> None:
    container = Container()
    ctx.event_bus_a = container.event_bus()
    ctx.event_bus_b = container.event_bus()


@then("both resolutions should return the same instance")
def step_same_instance(ctx: Context) -> None:
    assert ctx.event_bus_a is ctx.event_bus_b, (
        "event_bus Singleton returned different instances"
    )


@when("I resolve decision_service from the DI container")
def step_resolve_decision_service(ctx: Context) -> None:
    container = Container()
    ctx.resolved_decision_service = container.decision_service()


@then("the decision_service should have an event_bus attribute")
def step_decision_service_has_event_bus(ctx: Context) -> None:
    svc = ctx.resolved_decision_service
    assert hasattr(svc, "event_bus"), (
        "DecisionService resolved from DI has no event_bus attribute"
    )
    assert svc.event_bus is not None, "DecisionService.event_bus should not be None"
