"""Step definitions for ServiceRetryWiring async and advanced tests."""

from __future__ import annotations

import asyncio
from typing import Any

from behave import given, then, when

from cleveragents.core.retry_patterns import CircuitBreakerOpen


@then('all service policies should have backoff_strategy "{strategy}"')
def step_check_all_backoff(context: Any, strategy: str) -> None:
    for name in context.wiring.registry.registered_services():
        policy = context.wiring.registry.get(name)
        actual = (
            policy.retry.backoff_strategy.value
            if hasattr(policy.retry.backoff_strategy, "value")
            else str(policy.retry.backoff_strategy)
        )
        assert actual == strategy, (
            f"Service {name} has backoff_strategy={actual}, expected {strategy}"
        )


@when('I async_execute a function for "{name}" with open circuit')
def step_async_execute_open_circuit(context: Any, name: str) -> None:
    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(
            context.wiring.async_execute(
                service_name=name,
                operation_name="test_async_cb_open",
                func=_async_ok_func,
                idempotent=True,
            )
        )
        context.async_cb_error = None
    except CircuitBreakerOpen as exc:
        context.async_cb_error = exc
    except Exception as exc:
        context.async_cb_error = exc
    finally:
        loop.close()


async def _async_ok_func() -> str:
    return "ok"


@then("a CircuitBreakerOpen exception should be raised for async_execute")
def step_check_async_cb_open(context: Any) -> None:
    assert isinstance(context.async_cb_error, CircuitBreakerOpen)


@when("I execute the function through the wiring with structlog captured")
def step_execute_with_structlog_captured(context: Any) -> None:
    import structlog

    import cleveragents.core.retry_service_patterns as _rsp_mod

    captured_entries: list[dict[str, Any]] = []

    def capture_processor(
        _logger: Any, _method: str, event_dict: dict[str, Any]
    ) -> dict[str, Any]:
        captured_entries.append(event_dict.copy())
        raise structlog.DropEvent

    old_config = structlog.get_config()
    structlog.configure(
        processors=[capture_processor],
        wrapper_class=structlog.stdlib.BoundLogger,
        context_class=dict,
        logger_factory=structlog.PrintLoggerFactory(),
        cache_logger_on_first_use=False,
    )

    # Replace the module-level logger in retry_service_patterns with a
    # fresh lazy proxy.  When configure_structlog() (called during
    # container init in before_all) sets cache_logger_on_first_use=True,
    # earlier scenarios cause the proxy to cache a resolved BoundLogger
    # that ignores subsequent structlog.configure() calls.  Creating a
    # new proxy ensures our capture processor is used.
    saved_rsp_logger = _rsp_mod.logger
    _rsp_mod.logger = structlog.get_logger(_rsp_mod.__name__)

    try:
        context.result = context.wiring.execute(
            service_name="plan_service",
            operation_name="test_structlog_op",
            func=context.test_func,
            idempotent=True,
        )
        context.exec_error = None
    except Exception as exc:
        context.exec_error = exc
    finally:
        # Restore original module logger and structlog configuration
        _rsp_mod.logger = saved_rsp_logger
        structlog.configure(**old_config)

    context.captured_log_entries = captured_entries


@when("I execute a succeeding function for an unregistered service name")
def step_execute_for_unregistered_service(context: Any) -> None:
    context.unregistered_name = "unregistered_lazy_test_svc"
    # Ensure the name is NOT in the pre-built cache
    assert context.unregistered_name not in context.wiring._wait_strategies

    context.lazy_result = context.wiring.execute(
        service_name=context.unregistered_name,
        operation_name="lazy_build_op",
        func=lambda: "lazy_ok",
        idempotent=True,
    )


@then("the wait strategy cache should contain the unregistered service")
def step_check_wait_strategy_cache(context: Any) -> None:
    assert context.lazy_result == "lazy_ok"
    assert context.unregistered_name in context.wiring._wait_strategies


@when("I async_execute a nested retry operation through the wiring")
def step_async_execute_nested(context: Any) -> None:
    context.async_inner_call_count = 0

    async def async_inner() -> str:
        context.async_inner_call_count += 1
        raise RuntimeError("async inner failure")

    async def async_outer() -> str:
        import contextlib as _ctxlib

        with _ctxlib.suppress(RuntimeError):
            await context.wiring.async_execute(
                service_name="session_service",
                operation_name="async_nested_inner",
                func=async_inner,
                idempotent=True,
            )
        return "async_outer_ok"

    loop = asyncio.new_event_loop()
    try:
        context.async_nested_result = loop.run_until_complete(
            context.wiring.async_execute(
                service_name="plan_service",
                operation_name="async_nested_outer",
                func=async_outer,
                idempotent=True,
            )
        )
    finally:
        loop.close()


@then("the async inner function should have been called only once")
def step_check_async_inner_once(context: Any) -> None:
    assert context.async_nested_result == "async_outer_ok"
    # Inner runs exactly once — nesting guard skips retries at depth >= 1
    assert context.async_inner_call_count == 1


@when("I call wrap_service_method twice with the same arguments")
def step_call_wrap_twice(context: Any) -> None:
    context.wrap_first = context.wiring.wrap_service_method(
        "plan_service", "cache_test_op", idempotent=True
    )
    context.wrap_second = context.wiring.wrap_service_method(
        "plan_service", "cache_test_op", idempotent=True
    )


@then("both wrap_service_method calls should return the same object")
def step_check_wrap_same_object(context: Any) -> None:
    assert context.wrap_first is context.wrap_second


@when("I async_execute the always-failing function as idempotent")
def step_async_execute_always_failing(context: Any) -> None:
    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(
            context.wiring.async_execute(
                service_name="plan_service",
                operation_name="test_async_exhaustion",
                func=context.async_test_func,
                idempotent=True,
            )
        )
        context.async_exhaustion_error = None
    except RuntimeError as exc:
        context.async_exhaustion_error = exc
    except Exception as exc:
        context.async_exhaustion_error = exc
    finally:
        loop.close()


@then("the async RuntimeError should propagate after exhausting retries")
def step_check_async_exhaustion_error(context: Any) -> None:
    assert isinstance(context.async_exhaustion_error, RuntimeError)
    assert "Permanent async failure" in str(context.async_exhaustion_error)


@then("the async always-failing function should have been called max_attempts times")
def step_check_async_always_fail_count(context: Any) -> None:
    policy = context.wiring.get_policy("plan_service")
    assert context.async_call_count == policy.retry.max_attempts


@then("the captured log should contain a retry warning message")
def step_check_captured_retry_log(context: Any) -> None:
    assert context.exec_error is None
    assert context.result == "success"
    # Find a retry log entry
    retry_entries = [
        e
        for e in context.captured_log_entries
        if "retry" in str(e.get("event", "")).lower()
        or e.get("operation") == "test_structlog_op"
    ]
    assert len(retry_entries) > 0, (
        f"No retry log entries found in {context.captured_log_entries}"
    )


# ---------------------------------------------------------------------------
# _build_cached_wait with string backoff strategy
# ---------------------------------------------------------------------------


@given("I have a ServiceRetryWiring instance with string backoff policy")
def step_create_wiring_string_backoff(context: Any) -> None:
    from cleveragents.application.services.service_retry_wiring import (
        ServiceRetryWiring,
    )
    from cleveragents.config.settings import Settings

    settings = Settings(
        database_url="sqlite:///test_retry.db",
        mock_providers=True,
        env="test",
    )
    context.wiring = ServiceRetryWiring(settings)
    # Manually set a string backoff_strategy on a policy to test the fallback
    policy = context.wiring.get_policy("plan_service")
    policy.retry.backoff_strategy = "fixed"  # type: ignore[assignment]
    context.string_backoff_svc = "plan_service"


@when("I request the wait strategy for the string-backoff service")
def step_request_string_backoff_wait(context: Any) -> None:
    # Force rebuild by clearing cache
    context.wiring._wait_strategies.pop(context.string_backoff_svc, None)
    context.string_backoff_wait = context.wiring._get_wait_strategy(
        context.string_backoff_svc
    )


@then("the wait strategy should be built successfully")
def step_check_string_backoff_built(context: Any) -> None:
    assert context.string_backoff_wait is not None


# ---------------------------------------------------------------------------
# Async execute without circuit breaker
# ---------------------------------------------------------------------------


@when("I async_execute the function for the unprotected service")
def step_async_execute_unprotected(context: Any) -> None:
    loop = asyncio.new_event_loop()
    try:
        context.async_result = loop.run_until_complete(
            context.wiring.async_execute(
                service_name="plan_service",
                operation_name="test_async_no_cb",
                func=context.async_test_func,
                idempotent=True,
            )
        )
        context.async_exec_error = None
    except Exception as exc:
        context.async_exec_error = exc
        context.async_result = None
    finally:
        loop.close()


# ---------------------------------------------------------------------------
# Sync nesting guard without CB
# ---------------------------------------------------------------------------


@when("I execute a nested sync operation for the unprotected service")
def step_execute_nested_no_cb(context: Any) -> None:
    context.nested_no_cb_inner_count = 0

    def inner_func() -> str:
        context.nested_no_cb_inner_count += 1
        return "inner_no_cb_ok"

    def outer_func() -> str:
        # This nested call should hit the nesting guard, CB=None path
        return context.wiring.execute(
            service_name="plan_service",
            operation_name="nested_no_cb_inner",
            func=inner_func,
            idempotent=True,
        )

    context.nested_no_cb_result = context.wiring.execute(
        service_name="plan_service",
        operation_name="nested_no_cb_outer",
        func=outer_func,
        idempotent=True,
    )


@then("the nested inner function should execute directly without CB")
def step_check_nested_no_cb(context: Any) -> None:
    assert context.nested_no_cb_result == "inner_no_cb_ok"
    assert context.nested_no_cb_inner_count == 1
