# -*- coding: utf-8 -*-
'''
    :codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`


    ============================
    Unittest Compatibility Layer
    ============================

    Compatibility layer to use :mod:`unittest <python2:unittest>` under Python
    2.7 or `unittest2`_ under Python 2.6 without having to worry about which is
    in use.

    .. attention::

        Please refer to Python's :mod:`unittest <python2:unittest>`
        documentation as the ultimate source of information, this is just a
        compatibility layer.

    .. _`unittest2`: https://pypi.python.org/pypi/unittest2
'''
# pylint: disable=unused-import,blacklisted-module,deprecated-method

# Import python libs
from __future__ import absolute_import
import os
import sys
import logging
import salt.ext.six as six
try:
    import psutil
    HAS_PSUTIL = True
except ImportError:
    HAS_PSUTIL = False

log = logging.getLogger(__name__)

# Set SHOW_PROC to True to show
# process details when running in verbose mode
# i.e. [CPU:15.1%|MEM:48.3%|Z:0]
SHOW_PROC = 'NO_SHOW_PROC' not in os.environ

# support python < 2.7 via unittest2
if sys.version_info < (2, 7):
    try:
        # pylint: disable=import-error
        from unittest2 import (
            TestLoader as __TestLoader,
            TextTestRunner as __TextTestRunner,
            TestCase as __TestCase,
            expectedFailure,
            TestSuite as __TestSuite,
            skip,
            skipIf,
            TestResult as _TestResult,
            TextTestResult as __TextTestResult
        )
        from unittest2.case import _id
        # pylint: enable=import-error

        class NewStyleClassMixin(object):
            '''
            Simple new style class to make pylint shut up!

            And also to avoid errors like:

                'Cannot create a consistent method resolution order (MRO) for bases'
            '''

        class _TestLoader(__TestLoader, NewStyleClassMixin):
            pass

        class _TextTestRunner(__TextTestRunner, NewStyleClassMixin):
            pass

        class _TestCase(__TestCase, NewStyleClassMixin):
            pass

        class _TestSuite(__TestSuite, NewStyleClassMixin):
            pass

        class TestResult(_TestResult, NewStyleClassMixin):
            pass

        class _TextTestResult(__TextTestResult, NewStyleClassMixin):
            pass

    except ImportError:
        raise SystemExit('You need to install unittest2 to run the salt tests')
else:
    from unittest import (
        TestLoader as _TestLoader,
        TextTestRunner as _TextTestRunner,
        TestCase as _TestCase,
        expectedFailure,
        TestSuite as _TestSuite,
        skip,
        skipIf,
        TestResult,
        TextTestResult as _TextTestResult
    )
    from unittest.case import _id


class TestSuite(_TestSuite):

    def _handleClassSetUp(self, test, result):
        previousClass = getattr(result, '_previousTestClass', None)
        currentClass = test.__class__
        if currentClass == previousClass or getattr(currentClass, 'setUpClass', None) is None:
            return super(TestSuite, self)._handleClassSetUp(test, result)

        # Store a reference to all class attributes before running the setUpClass method
        initial_class_attributes = dir(test.__class__)
        ret = super(TestSuite, self)._handleClassSetUp(test, result)
        # Store the difference in in a variable in order to check later if they were deleted
        test.__class__._prerun_class_attributes = [
                attr for attr in dir(test.__class__) if attr not in initial_class_attributes]
        return ret

    def _tearDownPreviousClass(self, test, result):
        # Run any tearDownClass code defined
        super(TestSuite, self)._tearDownPreviousClass(test, result)
        previousClass = getattr(result, '_previousTestClass', None)
        currentClass = test.__class__
        if currentClass == previousClass:
            return
        # See if the previous class attributes have been cleaned
        if previousClass and getattr(previousClass, 'tearDownClass', None):
            prerun_class_attributes = getattr(previousClass, '_prerun_class_attributes', None)
            if prerun_class_attributes is not None:
                previousClass._prerun_class_attributes = None
                del previousClass._prerun_class_attributes
                for attr in prerun_class_attributes:
                    if hasattr(previousClass, attr):
                        attr_value = getattr(previousClass, attr, None)
                        if attr_value is None:
                            continue
                        if isinstance(attr_value, (bool,) + six.string_types + six.integer_types):
                            setattr(previousClass, attr, None)
                            continue
                        log.warning('Deleting extra class attribute after test run: %s.%s(%s). '
                                    'Please consider using \'del self.%s\' on the test class '
                                    '\'tearDownClass()\' method', previousClass.__name__, attr,
                                    str(getattr(previousClass, attr))[:100], attr)
                        delattr(previousClass, attr)


class TestLoader(_TestLoader):
    # We're just subclassing to make sure tha tour TestSuite class is the one used
    suiteClass = TestSuite


class TestCase(_TestCase):

    # pylint: disable=expected-an-indented-block-comment,too-many-leading-hastag-for-block-comment
##   Commented out because it may be causing tests to hang
##   at the end of the run
#
#    _cwd = os.getcwd()
#    _chdir_counter = 0

#    @classmethod
#    def tearDownClass(cls):
#        '''
#        Overriden method for tearing down all classes in salttesting
#
#        This hard-resets the environment between test classes
#        '''
#        # Compare where we are now compared to where we were when we began this family of tests
#        if not cls._cwd == os.getcwd() and cls._chdir_counter > 0:
#            os.chdir(cls._cwd)
#            print('\nWARNING: A misbehaving test has modified the working directory!\nThe test suite has reset the working directory '
#                    'on tearDown() to {0}\n'.format(cls._cwd))
#            cls._chdir_counter += 1
    # pylint: enable=expected-an-indented-block-comment,too-many-leading-hastag-for-block-comment

    def run(self, result=None):
        self._prerun_instance_attributes = dir(self)
        self.maxDiff = None
        outcome = super(TestCase, self).run(result=result)
        for attr in dir(self):
            if attr == '_prerun_instance_attributes':
                continue
            if attr in getattr(self.__class__, '_prerun_class_attributes', ()):
                continue
            if attr not in self._prerun_instance_attributes:
                attr_value = getattr(self, attr, None)
                if attr_value is None:
                    continue
                if isinstance(attr_value, (bool,) + six.string_types + six.integer_types):
                    setattr(self, attr, None)
                    continue
                log.warning('Deleting extra class attribute after test run: %s.%s(%s). '
                            'Please consider using \'del self.%s\' on the test case '
                            '\'tearDown()\' method', self.__class__.__name__, attr,
                            getattr(self, attr), attr)
                delattr(self, attr)
        self._prerun_instance_attributes = None
        del self._prerun_instance_attributes
        return outcome

    def shortDescription(self):
        desc = _TestCase.shortDescription(self)
        if HAS_PSUTIL and SHOW_PROC:
            show_zombie_processes = 'SHOW_PROC_ZOMBIES' in os.environ
            proc_info = '[CPU:{0}%|MEM:{1}%'.format(psutil.cpu_percent(),
                                                    psutil.virtual_memory().percent)
            if show_zombie_processes:
                found_zombies = 0
                try:
                    for proc in psutil.process_iter():
                        if proc.status == psutil.STATUS_ZOMBIE:
                            found_zombies += 1
                except Exception:
                    pass
                proc_info += '|Z:{0}'.format(found_zombies)
            proc_info += '] {short_desc}'.format(short_desc=desc if desc else '')
            return proc_info
        else:
            return _TestCase.shortDescription(self)

    def assertEquals(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('assertEquals', 'assertEqual')
        )
        # return _TestCase.assertEquals(self, *args, **kwargs)

    def assertNotEquals(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('assertNotEquals', 'assertNotEqual')
        )
        # return _TestCase.assertNotEquals(self, *args, **kwargs)

    def assert_(self, *args, **kwargs):
        # The unittest2 library uses this deprecated method, we can't raise
        # the exception.
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('assert_', 'assertTrue')
        )
        # return _TestCase.assert_(self, *args, **kwargs)

    def assertAlmostEquals(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('assertAlmostEquals', 'assertAlmostEqual')
        )
        # return _TestCase.assertAlmostEquals(self, *args, **kwargs)

    def assertNotAlmostEquals(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('assertNotAlmostEquals', 'assertNotAlmostEqual')
        )
        # return _TestCase.assertNotAlmostEquals(self, *args, **kwargs)

    def failUnlessEqual(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('failUnlessEqual', 'assertEqual')
        )
        # return _TestCase.failUnlessEqual(self, *args, **kwargs)

    def failIfEqual(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('failIfEqual', 'assertNotEqual')
        )
        # return _TestCase.failIfEqual(self, *args, **kwargs)

    def failUnless(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('failUnless', 'assertTrue')
        )
        # return _TestCase.failUnless(self, *args, **kwargs)

    def failIf(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('failIf', 'assertFalse')
        )
        # return _TestCase.failIf(self, *args, **kwargs)

    def failUnlessRaises(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('failUnlessRaises', 'assertRaises')
        )
        # return _TestCase.failUnlessRaises(self, *args, **kwargs)

    def failUnlessAlmostEqual(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('failUnlessAlmostEqual', 'assertAlmostEqual')
        )
        # return _TestCase.failUnlessAlmostEqual(self, *args, **kwargs)

    def failIfAlmostEqual(self, *args, **kwargs):
        raise DeprecationWarning(
            'The {0}() function is deprecated. Please start using {1}() '
            'instead.'.format('failIfAlmostEqual', 'assertNotAlmostEqual')
        )
        # return _TestCase.failIfAlmostEqual(self, *args, **kwargs)

    @staticmethod
    def assert_called_once(mock):
        '''
        mock.assert_called_once only exists in PY3 in 3.6 and newer
        '''
        try:
            mock.assert_called_once()
        except AttributeError:
            log.warning('assert_called_once invoked, but not available')

    if six.PY2:
        def assertRegexpMatches(self, *args, **kwds):
            raise DeprecationWarning(
                'The {0}() function will be deprecated in python 3. Please start '
                'using {1}() instead.'.format(
                    'assertRegexpMatches',
                    'assertRegex'
                )
            )

        def assertRegex(self, text, regex, msg=None):
            # In python 2, alias to the future python 3 function
            return _TestCase.assertRegexpMatches(self, text, regex, msg=msg)

        def assertNotRegexpMatches(self, *args, **kwds):
            raise DeprecationWarning(
                'The {0}() function will be deprecated in python 3. Please start '
                'using {1}() instead.'.format(
                    'assertNotRegexpMatches',
                    'assertNotRegex'
                )
            )

        def assertNotRegex(self, text, regex, msg=None):
            # In python 2, alias to the future python 3 function
            return _TestCase.assertNotRegexpMatches(self, text, regex, msg=msg)

        def assertRaisesRegexp(self, *args, **kwds):
            raise DeprecationWarning(
                'The {0}() function will be deprecated in python 3. Please start '
                'using {1}() instead.'.format(
                    'assertRaisesRegexp',
                    'assertRaisesRegex'
                )
            )

        def assertRaisesRegex(self, exception, regexp, *args, **kwds):
            # In python 2, alias to the future python 3 function
            return _TestCase.assertRaisesRegexp(self, exception, regexp, *args, **kwds)
    else:
        def assertRegexpMatches(self, *args, **kwds):
            raise DeprecationWarning(
                'The {0}() function is deprecated. Please start using {1}() '
                'instead.'.format(
                    'assertRegexpMatches',
                    'assertRegex'
                )
            )

        def assertNotRegexpMatches(self, *args, **kwds):
            raise DeprecationWarning(
                'The {0}() function is deprecated. Please start using {1}() '
                'instead.'.format(
                    'assertNotRegexpMatches',
                    'assertNotRegex'
                )
            )

        def assertRaisesRegexp(self, *args, **kwds):
            raise DeprecationWarning(
                'The {0}() function is deprecated. Please start using {1}() '
                'instead.'.format(
                    'assertRaisesRegexp',
                    'assertRaisesRegex'
                )
            )


class TextTestResult(_TextTestResult):
    '''
    Custom TestResult class whith logs the start and the end of a test
    '''

    def startTest(self, test):
        log.debug('>>>>> START >>>>> {0}'.format(test.id()))
        return super(TextTestResult, self).startTest(test)

    def stopTest(self, test):
        log.debug('<<<<< END <<<<<<< {0}'.format(test.id()))
        return super(TextTestResult, self).stopTest(test)


class TextTestRunner(_TextTestRunner):
    '''
    Custom Text tests runner to log the start and the end of a test case
    '''
    resultclass = TextTestResult


__all__ = [
    'TestLoader',
    'TextTestRunner',
    'TestCase',
    'expectedFailure',
    'TestSuite',
    'skipIf',
    'TestResult'
]
