"""Step definitions for retry_patterns_coverage_boost.feature.

These steps target specific uncovered lines in retry_patterns.py:
- Lines 592-599: get_retry_decorator with a known category
- RetryContext.execute / async_execute accepts None as valid return value
  (the _UNSET sentinel replaced None as the uninitialised marker)
- Line 551/553: auto-debug retry returns dict without error key
- Line 555: auto-debug retry returns non-dict result
- S12 fix: auto-debug returns error dict directly when no debug_callback
- Line 98: log_after_retry success-path TypeError fallback
- Line 82: else None branch in exception ternary of log_after_retry
"""

import asyncio
from types import SimpleNamespace

from behave import given, then, when

from cleveragents.core import retry_patterns as retry_patterns_module
from cleveragents.core.retry_patterns import (
    RETRY_CATEGORIES,
    RetryContext,
    get_retry_decorator,
    log_after_retry,
    retry_auto_debug,
)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the retry patterns coverage module is imported")
def step_coverage_module_imported(context):
    """Ensure the module is importable."""
    assert get_retry_decorator is not None
    assert RetryContext is not None
    assert retry_auto_debug is not None


# ---------------------------------------------------------------------------
# get_retry_decorator with known category  (lines 592-599)
# ---------------------------------------------------------------------------
@when('I request the retry decorator for known category "{category}"')
def step_request_known_category_decorator(context, category):
    """Request a retry decorator for a known category."""
    context.known_category = category
    context.known_decorator = get_retry_decorator(category)


@then("the returned decorator should use the network category configuration")
def step_verify_network_decorator(context):
    """Verify the decorator was built from the network config."""
    # The decorator should be callable (a tenacity retry wrapper)
    assert callable(context.known_decorator)
    # Confirm the category exists and has the expected max_attempts
    config = RETRY_CATEGORIES["network"]
    assert config["max_attempts"] == 5


@then("calling the decorated function should succeed")
def step_call_known_decorated_function(context):
    """Decorate and call a simple function to verify it works."""

    @context.known_decorator
    def sample():
        return "known_category_ok"

    result = sample()
    assert result == "known_category_ok"


@then("the returned decorator should use the database category configuration")
def step_verify_database_decorator(context):
    """Verify the decorator was built from the database config."""
    assert callable(context.known_decorator)
    config = RETRY_CATEGORIES["database"]
    assert config["max_attempts"] == 3

    @context.known_decorator
    def sample():
        return "db_ok"

    assert sample() == "db_ok"


# ---------------------------------------------------------------------------
# RetryContext.execute raises RuntimeError for None result  (line 469)
# ---------------------------------------------------------------------------
@given('I have a retry context named "{name}"')
def step_create_named_retry_context(context, name):
    """Create a RetryContext with the given operation name."""
    context.coverage_retry_ctx = RetryContext(
        operation_name=name,
        max_attempts=1,
    )


@given("I have a function that always returns None")
def step_function_returning_none(context):
    """Create a sync function that returns None."""

    def none_func():
        return None

    context.none_returning_func = none_func


@when("I execute the None-returning function with the retry context")
def step_execute_none_returning(context):
    """Execute the None-returning function via RetryContext.execute."""
    try:
        context.coverage_retry_ctx.execute(context.none_returning_func)
        context.none_exec_error = None
    except RuntimeError as exc:
        context.none_exec_error = exc


@given("I have an async function that always returns None")
def step_async_function_returning_none(context):
    """Create an async function that returns None."""

    async def async_none_func():
        return None

    context.async_none_returning_func = async_none_func


@when("I execute the async None-returning function with the retry context")
def step_execute_async_none_returning(context):
    """Execute the async None-returning function via RetryContext.async_execute."""
    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(
            context.coverage_retry_ctx.async_execute(context.async_none_returning_func)
        )
        context.none_exec_error = None
    except RuntimeError as exc:
        context.none_exec_error = exc
    finally:
        loop.close()


@then("None should be accepted as a valid return value")
def step_verify_none_accepted_sync(context):
    """Verify that None is returned without raising RuntimeError.

    Since the _UNSET sentinel replaced None as the uninitialized marker,
    a function returning None is perfectly valid.
    """
    assert context.none_exec_error is None, (
        f"Expected no error, got {context.none_exec_error!r}"
    )


@then("None should be accepted as a valid async return value")
def step_verify_none_accepted_async(context):
    """Verify that None is returned from async path without RuntimeError."""
    assert context.none_exec_error is None, (
        f"Expected no error, got {context.none_exec_error!r}"
    )


# ---------------------------------------------------------------------------
# Auto-debug retry: dict result without error key  (line 551/553)
# ---------------------------------------------------------------------------
@given("I have an async function that returns a dict without an error key")
def step_async_func_dict_no_error(context):
    """Create async func returning a dict with no 'error' key."""

    async def success_dict_func():
        return {"status": "ok", "data": 42}

    context.auto_debug_target = success_dict_func


@when("I apply auto-debug retry for dict success")
def step_apply_auto_debug_dict_success(context):
    """Apply auto-debug and run the function."""
    decorated = retry_auto_debug(max_debug_attempts=3)(context.auto_debug_target)
    loop = asyncio.new_event_loop()
    try:
        context.auto_debug_output = loop.run_until_complete(decorated())
    finally:
        loop.close()


@then("the auto-debug result should be the success dict")
def step_verify_auto_debug_dict(context):
    """Verify the dict was returned directly."""
    assert isinstance(context.auto_debug_output, dict)
    assert context.auto_debug_output["status"] == "ok"
    assert context.auto_debug_output["data"] == 42


# ---------------------------------------------------------------------------
# Auto-debug retry: non-dict result  (line 555)
# ---------------------------------------------------------------------------
@given("I have an async function that returns a non-dict result")
def step_async_func_non_dict(context):
    """Create async func returning a plain string."""

    async def string_func():
        return "plain_string_result"

    context.auto_debug_target = string_func


@when("I apply auto-debug retry for non-dict success")
def step_apply_auto_debug_non_dict(context):
    """Apply auto-debug and run the function."""
    decorated = retry_auto_debug(max_debug_attempts=3)(context.auto_debug_target)
    loop = asyncio.new_event_loop()
    try:
        context.auto_debug_output = loop.run_until_complete(decorated())
    finally:
        loop.close()


@then("the auto-debug result should be the non-dict value")
def step_verify_auto_debug_non_dict(context):
    """Verify the non-dict value was returned."""
    assert context.auto_debug_output == "plain_string_result"


# ---------------------------------------------------------------------------
# Auto-debug retry: returns error dict when no debug callback (S12 fix)
# ---------------------------------------------------------------------------
@given("I have an async function that returns a dict with an error key")
def step_async_func_returns_error_dict(context):
    """Create a func returning a dict with an ``error`` key.

    With the S12 fix, when no ``debug_callback`` is provided the dict
    is returned immediately — the loop does **not** exhaust.
    """

    async def error_dict_func():
        return {"error": "something went wrong", "detail": "transient"}

    context.auto_debug_target = error_dict_func


@when("I apply auto-debug retry without debug callback")
def step_apply_auto_debug_no_callback(context):
    """Apply auto-debug with no callback; expect the dict to be returned."""
    decorated = retry_auto_debug(max_debug_attempts=2, debug_callback=None)(
        context.auto_debug_target
    )
    loop = asyncio.new_event_loop()
    try:
        context.auto_debug_output = loop.run_until_complete(decorated())
        context.auto_debug_none_error = None
    except Exception as exc:
        context.auto_debug_output = None
        context.auto_debug_none_error = exc
    finally:
        loop.close()


@then("the auto-debug result should be the error dict returned directly")
def step_verify_auto_debug_error_dict_returned(context):
    """Verify the error dict is returned directly per S12 fix."""
    assert context.auto_debug_none_error is None, (
        f"Expected no error, got {context.auto_debug_none_error!r}"
    )
    assert isinstance(context.auto_debug_output, dict)
    assert context.auto_debug_output["error"] == "something went wrong"


# ---------------------------------------------------------------------------
# log_after_retry success TypeError fallback  (line 98)
# ---------------------------------------------------------------------------
@given("a logger that rejects keyword arguments is installed for coverage boost")
def step_install_keyword_rejecting_logger_boost(context):
    """Install a logger that rejects kwargs to force the TypeError fallback."""
    context.boost_logged = []

    class KwargRejectingLogger:
        def __init__(self, messages):
            self._messages = messages

        def debug(self, message, *args, **kwargs):
            if kwargs:
                raise TypeError("kwargs not supported")
            self._messages.append(message)

        def info(self, *a, **kw):
            pass

        def warning(self, *a, **kw):
            pass

        def error(self, *a, **kw):
            pass

    context.boost_original_logger = retry_patterns_module.logger
    retry_patterns_module.logger = KwargRejectingLogger(context.boost_logged)

    def restore():
        retry_patterns_module.logger = context.boost_original_logger

    context.add_cleanup(restore)


@when("I call log_after_retry with a successful outcome")
def step_call_log_after_retry_success(context):
    """Call log_after_retry with outcome.failed=False to hit the success TypeError fallback."""
    success_state = SimpleNamespace(
        attempt_number=7,
        outcome=SimpleNamespace(failed=False),
        next_action=None,
    )
    log_after_retry(success_state)


@then("the fallback success message should be logged")
def step_verify_fallback_success_logged(context):
    """Verify the fallback message was recorded."""
    assert "Attempt 7 succeeded" in context.boost_logged, (
        f"Expected 'Attempt 7 succeeded' in {context.boost_logged}"
    )


# ---------------------------------------------------------------------------
# log_after_retry: else None branch (line 82)
# ---------------------------------------------------------------------------
@given("a logger that accepts all arguments is installed for coverage boost")
def step_install_accepting_logger(context):
    """Install a logger that records all calls without raising."""
    context.boost_debug_calls = []

    class AcceptingLogger:
        def __init__(self, calls):
            self._calls = calls

        def debug(self, message, *args, **kwargs):
            self._calls.append({"msg": message, "kwargs": kwargs})

        def info(self, *a, **kw):
            pass

        def warning(self, *a, **kw):
            pass

        def error(self, *a, **kw):
            pass

    context.boost_original_logger = retry_patterns_module.logger
    retry_patterns_module.logger = AcceptingLogger(context.boost_debug_calls)

    def restore():
        retry_patterns_module.logger = context.boost_original_logger

    context.add_cleanup(restore)


@given("I have a retry state where outcome.failed toggles between checks")
def step_create_toggling_outcome(context):
    """Create a retry state whose outcome.failed is True first, then False.

    Line 71 checks ``retry_state.outcome.failed`` to enter the failure branch.
    Line 81 checks it again as part of a ternary for the exception= kwarg.
    If the second check returns False, line 82 (``else None``) executes.
    We use a property that toggles after the first access.
    """

    class TogglingOutcome:
        def __init__(self):
            self._call_count = 0

        @property
        def failed(self):
            self._call_count += 1
            # Line 71: ``retry_state.outcome and retry_state.outcome.failed``
            #   - outcome truthiness doesn't call .failed
            #   - first .failed access → call_count=1, returns True (enters if block)
            # Line 81: ternary condition checks .failed again
            #   - second .failed access → call_count=2, returns False → else None
            return self._call_count <= 1

        def exception(self):
            return ValueError("should not be called")

    context.toggling_retry_state = SimpleNamespace(
        attempt_number=1,
        outcome=TogglingOutcome(),
        next_action=SimpleNamespace(sleep=0.1),
    )


@when("I call log_after_retry with the toggling outcome")
def step_call_log_after_retry_toggling(context):
    """Invoke log_after_retry with the toggling outcome."""
    log_after_retry(context.toggling_retry_state)


@then("the exception keyword should resolve to None")
def step_verify_exception_none(context):
    """Verify the logger received exception=None from the else branch."""
    # Find the 'Attempt failed, will retry' call
    failed_calls = [
        c for c in context.boost_debug_calls if c["msg"] == "Attempt failed, will retry"
    ]
    assert len(failed_calls) == 1, (
        f"Expected exactly 1 'Attempt failed' log call, got {len(failed_calls)}: "
        f"{context.boost_debug_calls}"
    )
    assert failed_calls[0]["kwargs"].get("exception") is None, (
        f"Expected exception=None, got {failed_calls[0]['kwargs'].get('exception')}"
    )
