"""Step definitions for async retry decorator and auto-debug coverage tests."""

from __future__ import annotations

import contextlib
from typing import Any

from behave import given, then, when

# ---------------------------------------------------------------------------
# Async retry_service_operation decorator coverage
# ---------------------------------------------------------------------------


@given("I have an async function that fails once then returns ok")
def step_create_async_fail_once_decorator(context: Any) -> None:
    context.async_dec_call_count = 0

    async def async_fail_once() -> str:
        context.async_dec_call_count += 1
        if context.async_dec_call_count <= 1:
            raise RuntimeError("transient async fail")
        return "async_ok"

    context.async_dec_func = async_fail_once


@when("I apply retry_service_operation as async and call")
def step_apply_async_decorator_and_call(context: Any) -> None:
    import asyncio

    from cleveragents.core.retry_patterns import retry_service_operation

    @retry_service_operation(
        service_name="plan_service",
        operation_name="async_dec_test",
        max_attempts=3,
        base_delay=0.01,
        max_delay=0.02,
        total_timeout=10.0,
    )
    async def decorated() -> str:
        return await context.async_dec_func()

    loop = asyncio.new_event_loop()
    try:
        context.async_dec_result = loop.run_until_complete(decorated())
        context.async_dec_error = None
    except Exception as exc:
        context.async_dec_error = exc
        context.async_dec_result = None
    finally:
        loop.close()


@then("the async decorated function should succeed")
def step_check_async_dec_success(context: Any) -> None:
    assert context.async_dec_error is None
    assert context.async_dec_result == "async_ok"


@then("the async decorated function should have been called {n:d} times")
def step_check_async_dec_call_count(context: Any, n: int) -> None:
    assert context.async_dec_call_count == n


# ---------------------------------------------------------------------------
# Async decorator nesting guard
# ---------------------------------------------------------------------------


@when("I call nested async retry_service_operation decorators")
def step_nested_async_decorators(context: Any) -> None:
    import asyncio

    from cleveragents.core.retry_patterns import retry_service_operation

    context.inner_async_call_count = 0

    @retry_service_operation(
        service_name="session_service",
        operation_name="inner_async_dec",
        max_attempts=3,
        base_delay=0.01,
        max_delay=0.02,
        total_timeout=10.0,
    )
    async def inner_async() -> str:
        context.inner_async_call_count += 1
        raise RuntimeError("inner async failure")

    @retry_service_operation(
        service_name="plan_service",
        operation_name="outer_async_dec",
        max_attempts=2,
        base_delay=0.01,
        max_delay=0.02,
        total_timeout=10.0,
    )
    async def outer_async() -> str:
        with contextlib.suppress(RuntimeError):
            await inner_async()
        return "outer_async_ok"

    loop = asyncio.new_event_loop()
    try:
        context.nested_async_result = loop.run_until_complete(outer_async())
    finally:
        loop.close()


@then("the inner async decorator should execute without retries")
def step_check_inner_async_no_retries(context: Any) -> None:
    assert context.nested_async_result == "outer_async_ok"
    assert context.inner_async_call_count == 1


# ---------------------------------------------------------------------------
# _is_async_callable with partial wrapping async callable object
# ---------------------------------------------------------------------------


@when("I check _is_async_callable with partial wrapping an async callable object")
def step_check_is_async_partial_callable_obj(context: Any) -> None:
    from functools import partial

    from cleveragents.core.retry_patterns import _is_async_callable

    class AsyncCallableObj:
        async def __call__(self, x: int) -> int:
            return x

    wrapped = partial(AsyncCallableObj(), 42)
    context.partial_async_callable_result = _is_async_callable(wrapped)


@then("_is_async_callable should return True for partial async callable")
def step_check_partial_async_callable_true(context: Any) -> None:
    assert context.partial_async_callable_result is True


# ---------------------------------------------------------------------------
# Structured log TypeError fallback for service retry
# ---------------------------------------------------------------------------


@when("I trigger the structlog TypeError fallback for service retry logging")
def step_trigger_log_typeerror_fallback(context: Any) -> None:
    from unittest.mock import MagicMock

    import cleveragents.core.retry_service_patterns as rsp_mod
    from cleveragents.core.retry_service_patterns import (
        _log_circuit_open,
        _log_service_retry_attempt,
    )

    original_logger = rsp_mod.logger
    mock_logger = MagicMock()
    mock_logger.warning.side_effect = TypeError("no kwargs")
    mock_logger.error.side_effect = TypeError("no kwargs")
    rsp_mod.logger = mock_logger
    try:
        _log_service_retry_attempt("test_svc", "test_op", 2, RuntimeError("err"))
        _log_circuit_open("test_svc", 5)
        context.fallback_error = None
    except Exception as exc:
        context.fallback_error = exc
    finally:
        rsp_mod.logger = original_logger


@then("the fallback logging should not raise any exception")
def step_check_fallback_no_error(context: Any) -> None:
    assert context.fallback_error is None


# ---------------------------------------------------------------------------
# Auto-debug non-dict result
# ---------------------------------------------------------------------------


@given("I have an async function that returns a non-dict value")
def step_create_non_dict_auto_debug_func(context: Any) -> None:
    async def non_dict_func() -> str:
        return "plain_string_result"

    context.auto_debug_non_dict_func = non_dict_func


@when("I apply auto-debug retry to the non-dict function")
def step_apply_auto_debug_non_dict(context: Any) -> None:
    import asyncio

    from cleveragents.core.retry_patterns import retry_auto_debug

    @retry_auto_debug(max_debug_attempts=2)
    async def wrapped() -> str:
        return await context.auto_debug_non_dict_func()

    loop = asyncio.new_event_loop()
    try:
        context.auto_debug_non_dict_result = loop.run_until_complete(wrapped())
    finally:
        loop.close()


@then("the auto-debug should return the non-dict value directly")
def step_check_auto_debug_non_dict(context: Any) -> None:
    assert context.auto_debug_non_dict_result == "plain_string_result"


# ---------------------------------------------------------------------------
# Auto-debug returns dict result when no error
# ---------------------------------------------------------------------------


@given("I have an async function that returns error-free dict then stops")
def step_create_error_free_dict_func(context: Any) -> None:
    async def ok_dict_func() -> dict:
        return {"status": "ok", "data": 42}

    context.auto_debug_ok_dict_func = ok_dict_func


@given("I have no debug callback")
def step_no_debug_callback(context: Any) -> None:
    context.auto_debug_callback = None


@when("I apply auto-debug retry to the error-free function")
def step_apply_auto_debug_ok_dict(context: Any) -> None:
    import asyncio

    from cleveragents.core.retry_patterns import retry_auto_debug

    @retry_auto_debug(max_debug_attempts=2, debug_callback=None)
    async def wrapped() -> dict:
        return await context.auto_debug_ok_dict_func()

    loop = asyncio.new_event_loop()
    try:
        context.auto_debug_ok_dict_result = loop.run_until_complete(wrapped())
    finally:
        loop.close()


@then("the auto-debug should return the dict result")
def step_check_auto_debug_ok_dict(context: Any) -> None:
    assert context.auto_debug_ok_dict_result == {"status": "ok", "data": 42}


# ---------------------------------------------------------------------------
# Sync decorator with CB and CircuitBreakerOpen
# ---------------------------------------------------------------------------


@given("I have a sync function guarded by retry_service_operation with CB")
def step_create_sync_with_cb_decorator(context: Any) -> None:
    from cleveragents.core.retry_patterns import CircuitBreaker

    context.sync_cb = CircuitBreaker(failure_threshold=1)
    context.sync_cb_call_count = 0


@when("the circuit breaker opens during sync decorated retry")
def step_sync_cb_opens_during_retry(context: Any) -> None:
    from cleveragents.core.retry_patterns import (
        CircuitBreakerOpen,
        retry_service_operation,
    )

    cb = context.sync_cb

    @retry_service_operation(
        service_name="plan_service",
        operation_name="sync_cb_open_test",
        max_attempts=3,
        base_delay=0.01,
        max_delay=0.02,
        circuit_breaker=cb,
        total_timeout=10.0,
    )
    def sync_func() -> str:
        context.sync_cb_call_count += 1
        raise RuntimeError("trigger CB")

    try:
        sync_func()
        context.sync_cb_open_error = None
    except CircuitBreakerOpen as exc:
        context.sync_cb_open_error = exc
    except RuntimeError as exc:
        context.sync_cb_open_error = exc


@then("the sync decorator should propagate CircuitBreakerOpen")
def step_check_sync_cb_open_propagated(context: Any) -> None:
    from cleveragents.core.retry_patterns import CircuitBreakerOpen

    # After failure_threshold=1 failure, the CB opens,
    # and subsequent attempts get CircuitBreakerOpen.
    assert context.sync_cb_call_count >= 1
    # U7: Verify that the captured error is actually CircuitBreakerOpen,
    # not just any exception.
    assert isinstance(context.sync_cb_open_error, CircuitBreakerOpen), (
        f"Expected CircuitBreakerOpen but got {type(context.sync_cb_open_error).__name__}"
    )


# ---------------------------------------------------------------------------
# Async decorator with CB and CircuitBreakerOpen
# ---------------------------------------------------------------------------


@given("I have an async function guarded by retry_service_operation with CB")
def step_create_async_with_cb_decorator(context: Any) -> None:
    from cleveragents.core.retry_patterns import CircuitBreaker

    context.async_cb = CircuitBreaker(failure_threshold=1)
    context.async_cb_call_count = 0


@when("the circuit breaker opens during async decorated retry")
def step_async_cb_opens_during_retry(context: Any) -> None:
    import asyncio

    from cleveragents.core.retry_patterns import (
        CircuitBreakerOpen,
        retry_service_operation,
    )

    cb = context.async_cb

    @retry_service_operation(
        service_name="plan_service",
        operation_name="async_cb_open_test",
        max_attempts=3,
        base_delay=0.01,
        max_delay=0.02,
        circuit_breaker=cb,
        total_timeout=10.0,
    )
    async def async_func() -> str:
        context.async_cb_call_count += 1
        raise RuntimeError("trigger CB async")

    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(async_func())
        context.async_cb_open_error = None
    except CircuitBreakerOpen as exc:
        context.async_cb_open_error = exc
    except RuntimeError as exc:
        context.async_cb_open_error = exc
    finally:
        loop.close()


@then("the async decorator should propagate CircuitBreakerOpen")
def step_check_async_cb_open_propagated(context: Any) -> None:
    from cleveragents.core.retry_patterns import CircuitBreakerOpen

    assert context.async_cb_call_count >= 1
    # U7: Verify that the captured error is actually CircuitBreakerOpen.
    assert isinstance(context.async_cb_open_error, CircuitBreakerOpen), (
        f"Expected CircuitBreakerOpen but got {type(context.async_cb_open_error).__name__}"
    )


# ---------------------------------------------------------------------------
# Sync decorator nesting guard with CB
# ---------------------------------------------------------------------------


@when("I call a sync decorated function at max nesting depth with CB")
def step_sync_nesting_guard_with_cb(context: Any) -> None:
    from cleveragents.core.retry_patterns import (
        CircuitBreaker,
        retry_service_operation,
    )

    cb = CircuitBreaker(failure_threshold=5)

    @retry_service_operation(
        service_name="session_service",
        operation_name="inner_cb_nest",
        max_attempts=3,
        base_delay=0.01,
        max_delay=0.02,
        circuit_breaker=cb,
        total_timeout=10.0,
    )
    def inner_with_cb() -> str:
        return "inner_cb_ok"

    @retry_service_operation(
        service_name="plan_service",
        operation_name="outer_cb_nest",
        max_attempts=2,
        base_delay=0.01,
        max_delay=0.02,
        total_timeout=10.0,
    )
    def outer_with_cb() -> str:
        return inner_with_cb()

    context.nesting_cb_result = outer_with_cb()


@then("the sync decorator nesting guard should delegate to CB")
def step_check_nesting_cb_result(context: Any) -> None:
    assert context.nesting_cb_result == "inner_cb_ok"


# ---------------------------------------------------------------------------
# Non-idempotent decorator returns function as-is
# ---------------------------------------------------------------------------


@when("I create a non-idempotent retry_service_operation decorator")
def step_create_non_idempotent_decorator(context: Any) -> None:
    from cleveragents.core.retry_patterns import retry_service_operation

    def original_func() -> str:
        return "original"

    decorated = retry_service_operation(
        service_name="plan_service",
        operation_name="non_idem_test",
        max_attempts=3,
        idempotent=False,
        total_timeout=10.0,
    )(original_func)

    context.non_idem_original = original_func
    context.non_idem_decorated = decorated


@then("the decorator should return the original function unchanged")
def step_check_non_idempotent_is_identity(context: Any) -> None:
    assert context.non_idem_decorated is context.non_idem_original


# ---------------------------------------------------------------------------
# _is_async_callable with partial wrapping plain async function
# ---------------------------------------------------------------------------


@when("I check _is_async_callable with partial wrapping a plain async function")
def step_check_is_async_partial_plain(context: Any) -> None:
    from functools import partial

    from cleveragents.core.retry_service_patterns import _is_async_callable

    async def plain_async(x: int) -> int:
        return x

    wrapped = partial(plain_async, 42)
    context.partial_plain_async_result = _is_async_callable(wrapped)


@then("_is_async_callable should return True for partial plain async")
def step_check_partial_plain_async_true(context: Any) -> None:
    assert context.partial_plain_async_result is True


# ---------------------------------------------------------------------------
# Async decorator nesting guard with CB
# ---------------------------------------------------------------------------


@when("I call an async decorated function at max nesting depth with CB")
def step_async_nesting_guard_with_cb(context: Any) -> None:
    import asyncio

    from cleveragents.core.retry_patterns import (
        CircuitBreaker,
        retry_service_operation,
    )

    cb = CircuitBreaker(failure_threshold=5)

    @retry_service_operation(
        service_name="session_service",
        operation_name="inner_async_cb_nest",
        max_attempts=3,
        base_delay=0.01,
        max_delay=0.02,
        circuit_breaker=cb,
        total_timeout=10.0,
    )
    async def inner_async_with_cb() -> str:
        return "inner_async_cb_ok"

    @retry_service_operation(
        service_name="plan_service",
        operation_name="outer_async_cb_nest",
        max_attempts=2,
        base_delay=0.01,
        max_delay=0.02,
        total_timeout=10.0,
    )
    async def outer_async_with_cb() -> str:
        return await inner_async_with_cb()

    loop = asyncio.new_event_loop()
    try:
        context.async_nesting_cb_result = loop.run_until_complete(outer_async_with_cb())
    finally:
        loop.close()


@then("the async nesting CB guard should delegate successfully")
def step_check_async_nesting_cb_result(context: Any) -> None:
    assert context.async_nesting_cb_result == "inner_async_cb_ok"


# ---------------------------------------------------------------------------
# Auto-debug with fix callback
# ---------------------------------------------------------------------------


@given("I have an async function that returns error then succeeds after fix")
def step_create_auto_debug_fixable_func(context: Any) -> None:
    context.auto_debug_fix_call_count = 0

    async def fixable_func() -> dict:
        context.auto_debug_fix_call_count += 1
        if context.auto_debug_fix_call_count <= 1:
            return {"error": "broken", "status": "fail"}
        return {"status": "ok", "data": "fixed"}

    context.auto_debug_fixable_func = fixable_func


@given("I have a debug callback that returns fixed")
def step_create_fix_callback(context: Any) -> None:
    async def fix_callback(error: Any, attempt: int) -> dict:
        return {"fixed": True}

    context.auto_debug_fix_callback = fix_callback


@when("I apply auto-debug retry with fix callback")
def step_apply_auto_debug_with_fix(context: Any) -> None:
    import asyncio

    from cleveragents.core.retry_patterns import retry_auto_debug

    @retry_auto_debug(
        max_debug_attempts=3,
        debug_callback=context.auto_debug_fix_callback,
    )
    async def wrapped() -> dict:
        return await context.auto_debug_fixable_func()

    loop = asyncio.new_event_loop()
    try:
        context.auto_debug_fix_result = loop.run_until_complete(wrapped())
    finally:
        loop.close()


@then("the auto-debug should eventually succeed")
def step_check_auto_debug_fix_result(context: Any) -> None:
    assert context.auto_debug_fix_result == {"status": "ok", "data": "fixed"}
    assert context.auto_debug_fix_call_count == 2
