"""Step definitions for event_emission_wiring.feature.

Tests that domain services correctly emit events through the EventBus
for all 32 newly wired event types.
"""

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.services.checkpoint_service import CheckpointService
from cleveragents.application.services.decision_service import DecisionService
from cleveragents.application.services.invariant_service import InvariantService
from cleveragents.application.services.plan_lifecycle_service import (
    PlanLifecycleService,
)
from cleveragents.application.services.session_service import (
    PersistentSessionService,
)
from cleveragents.application.services.validation_pipeline import (
    ValidationCommand,
    ValidationPipeline,
)
from cleveragents.domain.models.core.invariant import InvariantScope
from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState
from cleveragents.domain.models.core.session import MessageRole
from cleveragents.domain.models.core.tool import ValidationMode
from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.reactive import ReactiveEventBus

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


def _make_settings() -> Any:
    mock = MagicMock()
    mock.async_enabled = False
    mock.cleveragents_max_context_size = 50 * 1024 * 1024
    return mock


def _get_events(ctx: Context) -> list[DomainEvent]:
    return ctx.ew_event_bus.audit_log  # type: ignore[no-any-return]


def _find_event(ctx: Context, event_type_value: str) -> DomainEvent | None:
    for ev in _get_events(ctx):
        if ev.event_type.value == event_type_value:
            return ev
    return None


# ---------------------------------------------------------------------------
# Plan Lifecycle steps
# ---------------------------------------------------------------------------


@given("an in-memory PlanLifecycleService with a tracking event bus")
def step_plan_service_with_bus(ctx: Context) -> None:
    ctx.ew_event_bus = ReactiveEventBus()
    ctx.ew_plan_svc = PlanLifecycleService(
        settings=_make_settings(),
        event_bus=ctx.ew_event_bus,
    )


def _create_plan_in_phase(
    svc: PlanLifecycleService,
    action_name: str,
    phase: PlanPhase,
) -> str:
    """Helper to create a plan and set it to a specific phase/processing state."""
    svc.create_action(
        name=action_name,
        description="Test action",
        definition_of_done="Done",
        strategy_actor="local/strategy",
        execution_actor="local/execution",
    )
    plan = svc.use_action(action_name)
    plan.processing_state = ProcessingState.PROCESSING
    plan.phase = phase
    svc._commit_plan(plan)
    return plan.identity.plan_id


@given("a plan in Strategize/PROCESSING phase")
def step_plan_strategize_processing(ctx: Context) -> None:
    svc: PlanLifecycleService = ctx.ew_plan_svc
    ctx.ew_plan_id = _create_plan_in_phase(svc, "local/ew-action", PlanPhase.STRATEGIZE)
    ctx.ew_event_bus.clear_audit_log()


@given("a plan in Execute/PROCESSING phase")
def step_plan_execute_processing(ctx: Context) -> None:
    svc: PlanLifecycleService = ctx.ew_plan_svc
    ctx.ew_plan_id = _create_plan_in_phase(
        svc, "local/ew-exec-action", PlanPhase.EXECUTE
    )
    ctx.ew_event_bus.clear_audit_log()


@given("a plan in Apply/PROCESSING phase")
def step_plan_apply_processing(ctx: Context) -> None:
    svc: PlanLifecycleService = ctx.ew_plan_svc
    ctx.ew_plan_id = _create_plan_in_phase(
        svc, "local/ew-apply-action", PlanPhase.APPLY
    )
    ctx.ew_event_bus.clear_audit_log()


@when("I call complete_strategize on that plan")
def step_complete_strategize(ctx: Context) -> None:
    ctx.ew_plan_svc.complete_strategize(ctx.ew_plan_id)


@when('I call fail_strategize with error "{error}"')
def step_fail_strategize(ctx: Context, error: str) -> None:
    ctx.ew_plan_svc.fail_strategize(ctx.ew_plan_id, error)


@when('I call fail_execute with error "{error}"')
def step_fail_execute(ctx: Context, error: str) -> None:
    ctx.ew_plan_svc.fail_execute(ctx.ew_plan_id, error)


@when('I call fail_apply with error "{error}"')
def step_fail_apply(ctx: Context, error: str) -> None:
    ctx.ew_plan_svc.fail_apply(ctx.ew_plan_id, error)


# ---------------------------------------------------------------------------
# Decision steps
# ---------------------------------------------------------------------------


@given("an in-memory DecisionService with a tracking event bus")
def step_decision_service_with_bus(ctx: Context) -> None:
    ctx.ew_event_bus = ReactiveEventBus()
    ctx.ew_decision_svc = DecisionService(event_bus=ctx.ew_event_bus)


@when("I record a normal decision")
def step_record_normal_decision(ctx: Context) -> None:
    plan_id = str(ULID())
    ctx.ew_decision_svc.record_decision(
        plan_id=plan_id,
        decision_type="strategy_choice",
        question="Which strategy?",
        chosen_option="Option A",
    )


@when("I record a correction decision")
def step_record_correction_decision(ctx: Context) -> None:
    plan_id = str(ULID())
    ctx.ew_decision_svc.record_decision(
        plan_id=plan_id,
        decision_type="strategy_choice",
        question="Which strategy?",
        chosen_option="Option B",
        is_correction=True,
        corrects_decision_id=str(ULID()),
        correction_reason="Correcting previous",
    )


@given("ew two decisions exist for the same plan")
def step_two_decisions(ctx: Context) -> None:
    plan_id = str(ULID())
    ctx.ew_plan_id_for_decisions = plan_id
    d1 = ctx.ew_decision_svc.record_decision(
        plan_id=plan_id,
        decision_type="strategy_choice",
        question="Q1",
        chosen_option="O1",
    )
    d2 = ctx.ew_decision_svc.record_decision(
        plan_id=plan_id,
        decision_type="strategy_choice",
        question="Q2",
        chosen_option="O2",
    )
    ctx.ew_decision_id_1 = d1.decision_id
    ctx.ew_decision_id_2 = d2.decision_id
    ctx.ew_event_bus.clear_audit_log()


@when("ew I mark the first decision as superseded by the second")
def step_mark_superseded(ctx: Context) -> None:
    ctx.ew_decision_svc.mark_superseded(ctx.ew_decision_id_1, ctx.ew_decision_id_2)


# ---------------------------------------------------------------------------
# Invariant steps
# ---------------------------------------------------------------------------


@given("an InvariantService with a tracking event bus")
def step_invariant_service_with_bus(ctx: Context) -> None:
    ctx.ew_event_bus = ReactiveEventBus()
    ctx.ew_invariant_svc = InvariantService(event_bus=ctx.ew_event_bus)


@given("an active invariant exists")
def step_active_invariant(ctx: Context) -> None:
    inv = ctx.ew_invariant_svc.add_invariant(
        text="Must not delete files",
        scope=InvariantScope.GLOBAL,
        source_name="test",
    )
    ctx.ew_invariant = inv


@given("an active invariant exists with known ID")
def step_active_invariant_known_id(ctx: Context) -> None:
    inv = ctx.ew_invariant_svc.add_invariant(
        text="Must not modify production data",
        scope=InvariantScope.GLOBAL,
        source_name="test",
    )
    ctx.ew_invariant = inv
    ctx.ew_invariant_id = inv.id


@when("I enforce invariants for a plan")
def step_enforce_invariants(ctx: Context) -> None:
    ctx.ew_event_bus.clear_audit_log()
    ctx.ew_invariant_svc.enforce_invariants(
        plan_id=str(ULID()),
        invariants=[ctx.ew_invariant],
    )


@when("I enforce invariants with violations for that invariant")
def step_enforce_with_violations(ctx: Context) -> None:
    ctx.ew_event_bus.clear_audit_log()
    ctx.ew_invariant_svc.enforce_invariants(
        plan_id=str(ULID()),
        invariants=[ctx.ew_invariant],
        violated_invariant_ids=[ctx.ew_invariant_id],
    )


# ---------------------------------------------------------------------------
# Session steps
# ---------------------------------------------------------------------------


@given("a PersistentSessionService with a tracking event bus")
def step_session_service_with_bus(ctx: Context) -> None:
    ctx.ew_event_bus = ReactiveEventBus()
    mock_session_repo = MagicMock()
    mock_message_repo = MagicMock()
    mock_session_repo.get_by_id.return_value = MagicMock(updated_at=None)
    mock_message_repo.count_for_session.return_value = 0
    ctx.ew_session_svc = PersistentSessionService(
        session_repo=mock_session_repo,
        message_repo=mock_message_repo,
        event_bus=ctx.ew_event_bus,
    )


@given("a session exists")
def step_session_exists(ctx: Context) -> None:
    session = ctx.ew_session_svc.create()
    ctx.ew_session_id = session.session_id
    ctx.ew_event_bus.clear_audit_log()


@when("I append a message to the session")
def step_append_message(ctx: Context) -> None:
    ctx.ew_session_svc.append_message(
        ctx.ew_session_id,
        MessageRole.USER,
        "Hello, world!",
    )


# ---------------------------------------------------------------------------
# Checkpoint / Sandbox steps
# ---------------------------------------------------------------------------


@given("a CheckpointService with a tracking event bus")
def step_checkpoint_service_with_bus(ctx: Context) -> None:
    ctx.ew_event_bus = ReactiveEventBus()
    ctx.ew_checkpoint_svc = CheckpointService(event_bus=ctx.ew_event_bus)


@when("I create a checkpoint")
def step_create_checkpoint(ctx: Context) -> None:
    ctx.ew_checkpoint_svc.create_checkpoint(
        plan_id=str(ULID()),
        sandbox_ref="abc123",
        reason="Test checkpoint",
    )


@when("I register a sandbox for a plan")
def step_register_sandbox(ctx: Context) -> None:
    ctx.ew_checkpoint_svc.register_sandbox(
        plan_id=str(ULID()),
        sandbox_ref="sandbox-ref-abc",
    )


@when("I mark a plan as applied")
def step_mark_applied(ctx: Context) -> None:
    ctx.ew_checkpoint_svc.mark_plan_applied(str(ULID()))


# ---------------------------------------------------------------------------
# Validation steps
# ---------------------------------------------------------------------------


@given("a ValidationPipeline with a tracking event bus and a passing validation")
def step_validation_passing(ctx: Context) -> None:
    ctx.ew_event_bus = ReactiveEventBus()

    def _executor(name: str, config: dict[str, Any]) -> dict[str, Any]:
        return {"passed": True, "message": "OK"}

    cmd = ValidationCommand(
        validation_name="test-check",
        resource_id="res-1",
        resource_name="test-resource",
        mode=ValidationMode.REQUIRED,
    )
    ctx.ew_pipeline = ValidationPipeline(
        commands=[cmd],
        executor=_executor,
        event_bus=ctx.ew_event_bus,
    )


@given("a ValidationPipeline with a tracking event bus and a failing validation")
def step_validation_failing(ctx: Context) -> None:
    ctx.ew_event_bus = ReactiveEventBus()

    def _executor(name: str, config: dict[str, Any]) -> dict[str, Any]:
        return {"passed": False, "message": "FAIL"}

    cmd = ValidationCommand(
        validation_name="test-check-fail",
        resource_id="res-2",
        resource_name="test-resource-fail",
        mode=ValidationMode.REQUIRED,
    )
    ctx.ew_pipeline = ValidationPipeline(
        commands=[cmd],
        executor=_executor,
        event_bus=ctx.ew_event_bus,
    )


@when("I run the pipeline")
def step_run_pipeline(ctx: Context) -> None:
    ctx.ew_pipeline.run()


# ---------------------------------------------------------------------------
# Common assertion steps
# ---------------------------------------------------------------------------


@then('ew the audit log should contain a "{event_type}" event')
def step_ew_event_bus_received(ctx: Context, event_type: str) -> None:
    ev = _find_event(ctx, event_type)
    assert ev is not None, (
        f"Expected event '{event_type}' not found. "
        f"Got: {[e.event_type.value for e in _get_events(ctx)]}"
    )


@then('ew the event details should contain key "{key}"')
def step_ew_event_details_key(ctx: Context, key: str) -> None:
    events = _get_events(ctx)
    found = False
    for ev in events:
        if key in ev.details:
            found = True
            break
    assert found, (
        f"Key '{key}' not found in any event details. "
        f"Events: {[(e.event_type.value, list(e.details.keys())) for e in events]}"
    )


# ---------------------------------------------------------------------------
# Tool Lifecycle steps
# ---------------------------------------------------------------------------


@given("a ToolRuntime with a tracking event bus and a registered tool")
def step_tool_runtime_with_bus(ctx: Context) -> None:
    from cleveragents.tool.lifecycle import (
        ToolDescriptor,
        ToolExecutionContext,
        ToolResult,
        ToolRuntime,
    )

    ctx.ew_event_bus = ReactiveEventBus()

    class _PassingTool:
        def discover(self) -> ToolDescriptor:
            return ToolDescriptor(name="test-tool", description="A test tool")

        def activate(self, ctx: ToolExecutionContext) -> None:
            pass

        def execute(
            self, params: dict[str, Any], ctx: ToolExecutionContext
        ) -> ToolResult:
            return ToolResult(success=True, data={"result": "ok"})

        def deactivate(self, ctx: ToolExecutionContext) -> None:
            pass

    from cleveragents.domain.models.core.tool import Tool, ToolCapability

    tool = Tool(
        name="local/test-tool",
        description="Test",
        source="builtin",
        tool_type="tool",
        timeout=30,
        capability=ToolCapability(),
    )
    ctx.ew_tool_runtime = ToolRuntime(
        tools={"local/test-tool": tool},
        instances={"local/test-tool": _PassingTool()},
        event_bus=ctx.ew_event_bus,
    )
    ctx.ew_tool_ctx = ToolExecutionContext(plan_id="test-plan-001")


@when("I execute the tool")
def step_execute_tool(ctx: Context) -> None:
    ctx.ew_tool_runtime.execute("local/test-tool", {}, ctx.ew_tool_ctx)


@given("a ToolRuntime with a tracking event bus and a failing tool")
def step_tool_runtime_failing(ctx: Context) -> None:
    from cleveragents.tool.lifecycle import (
        ToolDescriptor,
        ToolExecutionContext,
        ToolResult,
        ToolRuntime,
    )

    ctx.ew_event_bus = ReactiveEventBus()

    class _FailingTool:
        def discover(self) -> ToolDescriptor:
            return ToolDescriptor(name="fail-tool", description="A failing tool")

        def activate(self, ctx: ToolExecutionContext) -> None:
            pass

        def execute(
            self, params: dict[str, Any], ctx: ToolExecutionContext
        ) -> ToolResult:
            raise RuntimeError("Tool execution failed!")

        def deactivate(self, ctx: ToolExecutionContext) -> None:
            pass

    from cleveragents.domain.models.core.tool import Tool, ToolCapability

    tool = Tool(
        name="local/fail-tool",
        description="Failing test",
        source="builtin",
        tool_type="tool",
        timeout=30,
        capability=ToolCapability(),
    )
    ctx.ew_tool_runtime = ToolRuntime(
        tools={"local/fail-tool": tool},
        instances={"local/fail-tool": _FailingTool()},
        event_bus=ctx.ew_event_bus,
    )
    ctx.ew_tool_ctx = ToolExecutionContext(plan_id="test-plan-002")


@when("I execute the failing tool expecting an error")
def step_execute_failing_tool(ctx: Context) -> None:
    import contextlib

    from cleveragents.tool.lifecycle import ToolExecutionError

    with contextlib.suppress(ToolExecutionError):
        ctx.ew_tool_runtime.execute("local/fail-tool", {}, ctx.ew_tool_ctx)


# ---------------------------------------------------------------------------
# Actor Runtime steps
# ---------------------------------------------------------------------------


@given("a ToolCallingRuntime with a tracking event bus and a simple LLM")
def step_actor_runtime_with_bus(ctx: Context) -> None:
    from cleveragents.tool.actor_runtime import (
        LLMResponse,
        ToolCallingRuntime,
    )
    from cleveragents.tool.registry import ToolRegistry
    from cleveragents.tool.runner import ToolRunner

    ctx.ew_event_bus = ReactiveEventBus()

    registry = ToolRegistry()
    runner = ToolRunner(registry=registry)

    class _SimpleLLM:
        def invoke(
            self,
            prompt: str,
            tool_schemas: list[dict[str, Any]],
            tool_results: list[dict[str, Any]] | None = None,
            actor_config: dict[str, Any] | None = None,
        ) -> LLMResponse:
            return LLMResponse(content="Hello!")

    ctx.ew_actor_runtime = ToolCallingRuntime(
        registry=registry,
        runner=runner,
        llm_caller=_SimpleLLM(),
        event_bus=ctx.ew_event_bus,
    )


@when("I run the tool loop with a simple prompt")
def step_run_tool_loop(ctx: Context) -> None:
    ctx.ew_actor_runtime.run_tool_loop("Say hello")
