from robot import result, running
from robot.libraries.DateTime import Time

from tests_engine import TestTag

# This listener wraps the RetryFailed listener to gracefully skip retrying for timed-out tests.
# Failure is silenced because the lack of `RetryFailed` isn't a problem if `retry_count` is one.
try:
    from RetryFailed import RetryFailed
except:
    pass

# The name isn't CamelCase typically used for Python classes only because it
# has to match the name of the file and snake_case is used for other listeners.
class retry_and_timeout_listener:
    ROBOT_LISTENER_API_VERSION = 3

    def __init__(self, retry_count):
        if int(retry_count) > 1:
            global_retries = int(retry_count) - 1  # Our `retry_count` includes the original test too.
            self.retry_failed = RetryFailed(global_retries, keep_retried_tests=True)

    # Redirects access to all callbacks not implemented in this listener to `RetryFailed` if it's used.
    def __getattr__(self, name):
        # This prevents infinite recursion if `self.retry_failed` wasn't initialized.
        # None is always returned cause `__getattr__` won't be called if it was initialized.
        if name == 'retry_failed':
            return None

        if self.retry_failed and hasattr(self.retry_failed, name):
            return getattr(self.retry_failed, name)

    def start_suite(self, suite: running.TestSuite, result: result.TestSuite):
        # Grab our RobotTestSuite and restore the original parent.
        (self.tests_provider, suite.parent) = suite.parent
        self.after_timeout = False

        # start_suite isn't currently used by RetryFailed but let's check for future versions.
        if self.retry_failed and hasattr(self.retry_failed, 'start_suite'):
            self.retry_failed.start_suite(suite, result)

    def end_test(self, test: running.TestCase, result: result.TestCase):
        timed_out = result.failed and result.timeout and result.timeout.timed_out()
        timeout_expected = self.tests_provider.timeout_expected_tag in test.tags
        if timeout_expected:
            # It passed if timeout occurred within the test timeout +3s.
            tolerance_seconds = 3
            over_timeout = result.elapsed_time.seconds - Time(test.timeout).seconds
            if timed_out and over_timeout < tolerance_seconds:
                result.status = result.PASS
            else:
                result.message = f"Expected timeout didn't occur in {test.timeout} (+{tolerance_seconds}s)"

        # Mark messages that failed after another test timed out except tests that timed out.
        # It isn't really probable that Renode restart will cause timeouts.
        if result.failed and self.after_timeout and not timed_out:
            result.message = result.message + '\n\n' + self.tests_provider.after_timeout_message_suffix

        if timed_out:
            # See `_create_timeout_handler` in `robot_tests_provider`, mostly restarts Renode.
            self.tests_provider.timeout_handler(test, result)
            self.after_timeout = True

            # Let's prevent retrying a test with unexpected timeout but let's still call
            # `RetryFailed.end_test` as `RetryFailed` might not properly work without it.
            if self.retry_failed and not timeout_expected:
                self.retry_failed.max_retries = self.retry_failed.retries

        if self.retry_failed:
            original_message = result.message
            original_status = result.status
            self.retry_failed.end_test(test, result)

            # A retried attempt keeps the SKIP status set by `RetryFailed` (a failed
            # attempt mustn't fail the suite) and is tagged so that everything parsing
            # the output can tell it apart from the final attempt. Its message stays
            # the unmodified failure message.
            if self.retry_failed.retries != 0:
                result.tags.add(TestTag.RETRIED_ATTEMPT)
                result.message = original_message
                return

            # On the final attempt restore the original status changed by `RetryFailed`
            # so that our output formatters print it as failed.
            result.status = original_status

            # Let's also restore the original message without suffixes added by `RetryFailed`
            # unless it contains the number of attempts required in our test reporting.
            if not self.tests_provider.retry_test_regex.search(result.message):
                result.message = original_message
