"""Tests for threaded_test_wrapper.py."""

import contextlib
import logging
import sys
import threading
import time
import unittest
from io import StringIO
from unittest import mock

from tests.wrappers import threaded_test_wrapper
from tests.wrappers.threaded_test_wrapper import _ThreadedTestWrapper, run_in_thread_with_timeout

LOGGER = logging.getLogger(__name__)


class ThreadedTestWrapperSelfChecks(unittest.TestCase):
    """Tests for threaded_test_wrapper.py."""

    def start(self):
        """Each of the tests in this test case patches this method to run its own test."""
        raise NotImplementedError

    def test_propagation_of_failure_from_test_execution_thread(self):

        class SelfCheckExceptionHandling(Exception):
            pass

        caller_thread_id = threading.current_thread().ident

        @run_in_thread_with_timeout
        def my_errant_function(*_args, **_kwargs):
            if threading.current_thread().ident != caller_thread_id:
                raise SelfCheckExceptionHandling()

        # Suppress error output by redirecting to stringio_stderr
        stringio_stderr = StringIO()
        try:
            with mock.patch.object(_ThreadedTestWrapper, '_stderr',
                                   stringio_stderr), self.assertRaises(
                                       AssertionError) as exc_ctx:
                my_errant_function()

            self.assertIn('raise SelfCheckExceptionHandling()',
                          exc_ctx.exception.args[0])
            expected_tail = 'SelfCheckExceptionHandling\n'
            self.assertEqual(exc_ctx.exception.args[0][-len(expected_tail):],
                             expected_tail)

            self.assertIn('raise SelfCheckExceptionHandling()',
                          stringio_stderr.getvalue())
            self.assertEqual(stringio_stderr.getvalue()[-len(expected_tail):],
                             expected_tail)
        except Exception:
            try:
                print('This stderr was captured from our thread wrapper:\n',
                      stringio_stderr.getvalue(),
                      file=sys.stderr)
            except Exception:
                # Must not replace the original exception re-raised below.
                LOGGER.exception('Failed to dump captured stderr')

            raise

    def test_handling_of_test_execution_thread_timeout(self):
        # Suppress error output by redirecting to our stringio_stderr object
        stringio_stderr = StringIO()

        @run_in_thread_with_timeout
        def my_sleeper(*_args, **_kwargs):
            time.sleep(1.1)

        with contextlib.ExitStack() as stack:
            # Redirect _ThreadedTestWrapper error output to our StringIO
            # instance
            stack.enter_context(
                mock.patch.object(_ThreadedTestWrapper, '_stderr',
                                  stringio_stderr))
            # Patch DEFAULT_TEST_TIMEOUT to much smaller value than sleep in
            # my_start()
            stack.enter_context(
                mock.patch.object(threaded_test_wrapper, 'DEFAULT_TEST_TIMEOUT',
                                  0.01))
            # Redirect start() call from thread to our own my_start()
            exc_ctx = stack.enter_context(self.assertRaises(AssertionError))
            my_sleeper()

        self.assertEqual(len(stringio_stderr.getvalue()), 0)
        self.assertIn('The test timed out.', exc_ctx.exception.args[0])

    def test_integrity_of_args_and_return_value(self):
        args_bucket = []
        kwargs_bucket = []
        value_to_return = {}

        @run_in_thread_with_timeout
        def my_guinea_pig(*args, **kwargs):
            args_bucket.append(args)
            kwargs_bucket.append(kwargs)
            return value_to_return

        arg0 = {}
        arg1 = ()

        kwarg0 = []

        result = my_guinea_pig(arg0, arg1, kwarg0=kwarg0)

        self.assertIs(result, value_to_return)

        args_ut = args_bucket[0]
        self.assertEqual(len(args_ut), 2, repr(args_ut))
        self.assertIs(args_ut[0], arg0)
        self.assertIs(args_ut[1], arg1)

        kwargs_ut = kwargs_bucket[0]
        self.assertEqual(len(kwargs_ut), 1, repr(kwargs_ut))
        self.assertIn('kwarg0', kwargs_ut, repr(kwargs_ut))
        self.assertIs(kwargs_ut['kwarg0'], kwarg0)

    def test_skip_test_is_passed_through(self):

        @run_in_thread_with_timeout
        def my_test_skipper():
            raise unittest.SkipTest('I SKIP')

        with self.assertRaises(unittest.SkipTest) as ctx:
            my_test_skipper()

        self.assertEqual(ctx.exception.args[0], 'I SKIP')
