"""Acceptance test utils."""

import functools
import logging
import time
import traceback

import pika._utils

LOGGER = logging.getLogger(__name__)


def safe_close(closeable, *args, **kwargs):
    """
    Close `closeable` from cleanup, logging rather than raising on failure.

    Teardown registered via `addCleanup()` reports its exceptions as test errors, so a failing
    close() masks the result of the test that registered it. Best-effort closing keeps the test's
    own outcome, passing or failing, as the thing that gets reported.

    Callers that can distinguish an expected teardown failure from a real one should catch that
    narrow type themselves instead of reaching for this.

    :param closeable: object whose `close()` is to be called.
    :param args: positional args to pass to `close()`.
    :param kwargs: keyword args to pass to `close()`.
    """
    try:
        closeable.close(*args, **kwargs)
    except Exception:
        LOGGER.exception('Best-effort cleanup close() failed for %r', closeable)


def retry_assertion(timeout_sec, retry_interval_sec=0.1):
    """
    Creates a decorator that retries the decorated function or method only upon `AssertionError`
    exception at the given retry interval not to exceed the overall given timeout.

    :param float timeout_sec: overall timeout in seconds
    :param float retry_interval_sec: amount of time to sleep
        between retries in seconds.

    :returns: decorator that implements the following behavior

    1. This decorator guarantees to call the decorated function or method at
    least once.
    2. It passes through all exceptions besides `AssertionError`, preserving the
    original exception and its traceback.
    3. If no exception, it returns the return value from the decorated function/method.
    4. It sleeps `time.sleep(retry_interval_sec)` between retries.
    5. It checks for expiry of the overall timeout before sleeping.
    6. If the overall timeout is exceeded, it re-raises the latest `AssertionError`,
    preserving its original traceback
    """

    def retry_assertion_decorator(func):
        """Decorator."""

        @functools.wraps(func)
        def retry_assertion_wrap(*args, **kwargs):
            """The wrapper."""
            num_attempts = 0
            start_time = pika._utils.time_now()

            while True:
                num_attempts += 1

                try:
                    result = func(*args, **kwargs)
                except AssertionError:

                    now = pika._utils.time_now()
                    # Compensate for time adjustment
                    start_time = min(start_time, now)

                    if (now - start_time) > timeout_sec:
                        LOGGER.exception(
                            'Exceeded retry timeout of %s sec in %s attempts '
                            "with func %r. Caller's stack:\n%s", timeout_sec,
                            num_attempts, func,
                            ''.join(traceback.format_stack()))
                        raise

                    LOGGER.debug('Attempt %s failed; retrying %r in %s sec.',
                                 num_attempts, func, retry_interval_sec)

                    time.sleep(retry_interval_sec)
                else:
                    LOGGER.debug('%r succeeded at attempt %s', func,
                                 num_attempts)
                    return result

        return retry_assertion_wrap

    return retry_assertion_decorator
