"""Step definitions for retry_service_patterns_coverage.feature.

Targets uncovered lines in retry_service_patterns.py:
- Line 82:  _is_async_callable partial(async coroutine) → True
            (unreachable on Python 3.13 where iscoroutinefunction handles
            partials natively; covered indirectly via partial(callable_obj))
- Line 85:  _is_async_callable partial(callable_obj_with_async_call) → True
- Line 86:  _is_async_callable partial(SomeClass) → False
- Line 90:  _is_async_callable(non_callable) → False
- Line 254: async retry_service_operation CircuitBreakerOpen with no breaker
- Lines 444-446: RetryContext.execute rejects async callables
- Lines 452-453: RetryContext.execute nesting guard
- Lines 507-508: RetryContext.async_execute nesting guard
- Line 482:  RetryContext.execute RuntimeError safeguard
- Line 537:  RetryContext.async_execute RuntimeError safeguard
- Lines 575-577: retry_auto_debug rejects sync callables
- Line 639: retry_auto_debug returns None with zero attempts
"""

import asyncio
from functools import partial
from unittest.mock import patch

from behave import given, then, when

from cleveragents.core.retry_patterns import CircuitBreakerOpen
from cleveragents.core.retry_service_patterns import (
    RetryContext,
    _is_async_callable,
    _retry_depth,
    retry_auto_debug,
    retry_service_operation,
)

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


def _run_async(coro):
    """Run a coroutine in a fresh event loop."""
    loop = asyncio.new_event_loop()
    try:
        return loop.run_until_complete(coro)
    finally:
        loop.close()


# ---------------------------------------------------------------------------
# Scenario: _is_async_callable detects partial wrapping async callable object
# (line 85 — partial wrapping a non-type callable with async __call__)
# ---------------------------------------------------------------------------


@given("I have a functools.partial wrapping a callable object with async __call__")
def step_partial_wrapping_async_callable_obj(context):
    class AsyncCallableObj:
        async def __call__(self, x):
            return x

    context.partial_async_callable = partial(AsyncCallableObj(), 42)


@when("I check if the partial wrapping async callable is async")
def step_check_partial_async_callable(context):
    context.is_async_callable_result = _is_async_callable(
        context.partial_async_callable
    )


@then("the async callable partial result should be True")
def step_assert_partial_async_callable_true(context):
    assert context.is_async_callable_result is True, (
        f"Expected True, got {context.is_async_callable_result}"
    )


# ---------------------------------------------------------------------------
# Scenario: _is_async_callable returns False for partial wrapping a class type
# (line 86)
# ---------------------------------------------------------------------------


@given("I have a functools.partial wrapping a class type")
def step_partial_wrapping_class(context):
    class MyClass:
        def __init__(self, x=10):
            self.x = x

    context.partial_type = partial(MyClass, x=5)


@when("I check if the partial-wrapped type is async callable")
def step_check_partial_type_async(context):
    context.is_async_result_type = _is_async_callable(context.partial_type)


@then("rspcov the result should be False for partial type detection")
def step_assert_partial_type_false(context):
    assert context.is_async_result_type is False, (
        f"Expected False, got {context.is_async_result_type}"
    )


# ---------------------------------------------------------------------------
# Scenario: _is_async_callable returns False for non-callable (line 90)
# ---------------------------------------------------------------------------


@given("I have a plain integer value")
def step_plain_integer(context):
    context.non_callable = 42


@when("I check if the integer is async callable")
def step_check_integer_async(context):
    context.is_async_integer_result = _is_async_callable(context.non_callable)


@then("rspcov the result should be False for the non-callable value")
def step_assert_integer_false(context):
    assert context.is_async_integer_result is False, (
        f"Expected False, got {context.is_async_integer_result}"
    )


# ---------------------------------------------------------------------------
# Scenario: Async retry_service_operation with CircuitBreakerOpen, no breaker
# (line 254)
# ---------------------------------------------------------------------------


@given(
    "I have an async function decorated with retry_service_operation"
    " and no circuit breaker"
)
def step_async_decorated_no_breaker(context):
    @retry_service_operation(
        service_name="test-svc",
        operation_name="test-op",
        max_attempts=2,
        base_delay=0.01,
        max_delay=0.02,
        circuit_breaker=None,
        total_timeout=5.0,
    )
    async def _failing_op():
        raise CircuitBreakerOpen("circuit is open")

    context.async_cb_func = _failing_op


@given("the async function raises CircuitBreakerOpen")
def step_noop_marker(_context):
    pass  # Configured in the previous step


@when("I call the decorated async service operation")
def step_call_async_service_op(context):
    context.cb_error = None
    try:
        _run_async(context.async_cb_func())
    except CircuitBreakerOpen as exc:
        context.cb_error = exc


@then("CircuitBreakerOpen should propagate with failure count zero")
def step_assert_cb_open_propagated(context):
    assert context.cb_error is not None, "Expected CircuitBreakerOpen to be raised"
    assert isinstance(context.cb_error, CircuitBreakerOpen)


# ---------------------------------------------------------------------------
# Scenario: RetryContext.execute rejects async callables (lines 444-446)
# ---------------------------------------------------------------------------


@given('I have a RetryContext for operation "{name}"')
def step_create_retry_context(context, name):
    context.retry_ctx = RetryContext(operation_name=name, max_attempts=2)


@given("I have an async callable to pass to sync execute")
def step_create_async_for_sync(context):
    async def _async_fn():
        return "async_result"

    context.async_for_sync = _async_fn


@when("I call execute with the async callable")
def step_call_execute_with_async(context):
    context.type_error = None
    try:
        context.retry_ctx.execute(context.async_for_sync)
    except TypeError as exc:
        context.type_error = exc


@then("a TypeError should be raised indicating async_execute is needed")
def step_assert_type_error_async(context):
    assert context.type_error is not None, "Expected TypeError to be raised"
    assert "async_execute" in str(context.type_error)


# ---------------------------------------------------------------------------
# Scenario: RetryContext.execute nesting guard (lines 452-453)
# ---------------------------------------------------------------------------


@given("the retry depth is already at maximum")
def step_set_max_depth(context):
    # Set depth to 1 (the max nesting depth) so the guard kicks in
    context.depth_token = _retry_depth.set(1)

    def restore():
        _retry_depth.reset(context.depth_token)

    context.add_cleanup(restore)


@when("I call execute at max nesting depth with a sync function")
def step_call_execute_at_max_depth(context):
    context.nesting_call_count = 0

    def _sync_fn():
        context.nesting_call_count += 1
        return "nested_result"

    context.nesting_result = context.retry_ctx.execute(_sync_fn)


@then("the function should execute once without retry wrapping")
def step_assert_executed_once(context):
    assert context.nesting_result == "nested_result"
    assert context.nesting_call_count == 1


@then("the attempt count should be 1")
def step_assert_attempt_count_1(context):
    assert context.retry_ctx.attempt_count == 1


# ---------------------------------------------------------------------------
# Scenario: RetryContext.async_execute nesting guard (lines 507-508)
# ---------------------------------------------------------------------------


@given("the retry depth is already at maximum for async")
def step_set_max_depth_async(context):
    context.depth_token_async = _retry_depth.set(1)

    def restore():
        _retry_depth.reset(context.depth_token_async)

    context.add_cleanup(restore)


@when("I call async_execute at max nesting depth")
def step_call_async_execute_at_max_depth(context):
    context.async_nesting_call_count = 0

    async def _async_fn():
        context.async_nesting_call_count += 1
        return "async_nested_result"

    context.async_nesting_result = _run_async(
        context.retry_ctx.async_execute(_async_fn)
    )


@then("the async function should execute once without retry wrapping")
def step_assert_async_executed_once(context):
    assert context.async_nesting_result == "async_nested_result"
    assert context.async_nesting_call_count == 1


@then("the async attempt count should be 1")
def step_assert_async_attempt_count_1(context):
    assert context.retry_ctx.attempt_count == 1


# ---------------------------------------------------------------------------
# Scenario: RetryContext.execute RuntimeError safeguard (line 482)
# ---------------------------------------------------------------------------


@given("Retrying is patched to yield zero iterations")
def step_patch_retrying_empty(context):
    # Patch is applied in the When step; this step documents intent.
    pass


@when("I call execute expecting a RuntimeError from empty retrying")
def step_call_execute_empty_retrying(context):
    """Patch Retrying to return an empty iterator, then call execute."""

    class _EmptyRetrying:
        def __init__(self, **kwargs):
            pass

        def __iter__(self):
            return iter([])

    context.execute_runtime_error = None
    with patch("cleveragents.core.retry_service_patterns.Retrying", _EmptyRetrying):
        try:
            context.retry_ctx.execute(lambda: "should_not_run")
        except RuntimeError as exc:
            context.execute_runtime_error = exc


@then("a RuntimeError about Retrying executing at least once should be raised")
def step_assert_retrying_runtime_error(context):
    assert context.execute_runtime_error is not None, (
        "Expected RuntimeError to be raised"
    )
    assert "at least once" in str(context.execute_runtime_error)


# ---------------------------------------------------------------------------
# Scenario: RetryContext.async_execute RuntimeError safeguard (line 537)
# ---------------------------------------------------------------------------


@given("AsyncRetrying is patched to yield zero async iterations")
def step_patch_async_retrying_empty(context):
    pass  # Patch applied in the When step


@when("I call async_execute expecting a RuntimeError from empty async retrying")
def step_call_async_execute_empty_retrying(context):
    """Patch AsyncRetrying to return an empty async iterator."""

    class _EmptyAsyncRetrying:
        def __init__(self, **kwargs):
            pass

        def __aiter__(self):
            return self

        async def __anext__(self):
            raise StopAsyncIteration

    async def _async_fn():
        return "should_not_run"

    context.async_execute_runtime_error = None
    with patch(
        "cleveragents.core.retry_service_patterns.AsyncRetrying",
        _EmptyAsyncRetrying,
    ):
        try:
            _run_async(context.retry_ctx.async_execute(_async_fn))
        except RuntimeError as exc:
            context.async_execute_runtime_error = exc


@then("a RuntimeError about AsyncRetrying executing at least once should be raised")
def step_assert_async_retrying_runtime_error(context):
    assert context.async_execute_runtime_error is not None, (
        "Expected RuntimeError to be raised"
    )
    assert "at least once" in str(context.async_execute_runtime_error)


# ---------------------------------------------------------------------------
# Scenario: retry_auto_debug rejects sync callables (lines 575-577)
# ---------------------------------------------------------------------------


@given("I have a synchronous function decorated with retry_auto_debug")
def step_sync_func_with_auto_debug(context):
    def _sync_fn():
        return "sync_result"

    context.auto_debug_sync_wrapper = retry_auto_debug(max_debug_attempts=3)(_sync_fn)


@when("I invoke the retry_auto_debug wrapper for the sync function")
def step_invoke_auto_debug_sync(context):
    context.auto_debug_type_error = None
    try:
        _run_async(context.auto_debug_sync_wrapper())
    except TypeError as exc:
        context.auto_debug_type_error = exc


@then("a TypeError about async callables should be raised")
def step_assert_auto_debug_type_error(context):
    assert context.auto_debug_type_error is not None, "Expected TypeError to be raised"
    assert "async" in str(context.auto_debug_type_error).lower()


# ---------------------------------------------------------------------------
# Scenario: retry_auto_debug returns None with zero attempts (line 639)
# ---------------------------------------------------------------------------


@given("I have an async function decorated with retry_auto_debug with zero attempts")
def step_auto_debug_zero_attempts(context):
    async def _async_fn():
        return "should_not_be_called"

    context.auto_debug_zero_wrapper = retry_auto_debug(max_debug_attempts=0)(_async_fn)


@when("I invoke the retry_auto_debug wrapper with zero attempts")
def step_invoke_auto_debug_zero(context):
    context.auto_debug_zero_result = _run_async(context.auto_debug_zero_wrapper())


@then("rspcov the result should be None from exhausted attempts")
def step_assert_auto_debug_none(context):
    assert context.auto_debug_zero_result is None, (
        f"Expected None, got {context.auto_debug_zero_result!r}"
    )
