from __future__ import print_function
from __future__ import absolute_import
import mock
import os
import six
import sys
from six.moves import map
import unittest


PROGNAME = os.path.basename(sys.argv[0]) or 'koji'

"""
  Classes
"""


#
# Dummy Mock class
#
class _dummy_(object):
    def __enter__(self):
        return self

    def __exit__(self, *arg, **kwargs):
        pass

    def assert_called_once(self, *arg, **kwargs):
        pass

    def assert_called_once_with(self, *arg, **kwargs):
        pass


#
# CLI TestCase
#
class CliTestCase(unittest.TestCase):

    # public attribute
    progname = PROGNAME
    error_format = None
    STDOUT = sys.stdout
    STDERR = sys.stderr

    #
    # private methods
    #
    def __assert_callable(self, obj):
        if not callable(obj):
            raise ValueError('%s is not callable' %
                             getattr(obj, "__name__", 'function'))

    #
    # public methods
    #
    def format_error_message(self, error_message, progname=None):
        return self.error_format.format(message=error_message) \
            if self.error_format else error_message

    def print_message(self, *args, **kwargs):
        """Print message on sys.stdout

           This function will not be influenced when sys.stdout is mocked.
        """
        kwargs['file'] = self.STDOUT
        print(" ".join(map(str, args)), **kwargs)

    def assert_function_wrapper(self, callableObj, *args, **kwargs):
        """Wrapper func with anonymous funtion without argument"""
        self.__assert_callable(callableObj)
        return lambda: callableObj(*args, **kwargs)

    def assert_console_message(
            self, device, message, wipe=True, regex=False):

        # don't care condition
        if message is None:
            return

        output = device.getvalue()
        if not regex:
            self.assertMultiLineEqual(output, message)
        else:
            six.assertRegex(self, output, message)

        if wipe:
            device.seek(0)
            device.truncate(0)

    def assert_system_exit(self, callableObj, *args, **kwargs):
        """Test if SystemExit exception is issued

        Arguments:
            callableObj: the test function
            *args: Vaiable length arguments that will be passed to callableObj
            **kwargs: keyword arguments (see below)

        Keyword arguments: (reseverd for assert_system_exit)
            activate_session (string):
                Mock koji_cli.commands.activate_session and test if it is
                called.
                Default is on, use None to stop mocking.

            stdout:
            stderr: Arguments for messages comparison on stdout/stderr. These
                arguments allows different data types.

                ``None`` type will skip message comparison.
                ``string`` type will do multiple line comparison.
                ``dict`` type is an advanced type to allow regular expression
                 format. the format is::
                    {
                        'message': 'message string or regular expression',
                        'wipe': 'wipe the output device, default is True',
                        'regex': 'True if message format is regular expression'
                    }

            assert_func:
                Callable object with no arguments for customized tests.

            exit_code:
                The exit code when SystemExit is raised

            Important! all the other keyword arguments that are not listed
            above will be passed to callableObj
        """

        # check callableObj callable
        self.__assert_callable(callableObj)

        # these arguments are reseverd and used in assert_system_exit
        reserved = [
            'activate_session', 'stdout', 'stderr',
            'assert_func', 'exit_code'
        ]

        activate = kwargs.get(
            'activate_session', 'koji_cli.commands.activate_session')

        # stdout/stderr message comparison, None means don't care
        # message/error allows many different data types, None, string and dict
        message = {}
        for key in ['stdout', 'stderr']:
            data = kwargs.get(key, None)
            message[key] = {'message': None, 'wipe': True, 'regex': False}

            if data is None or isinstance(data, six.string_types):
                message[key]['message'] = data

            elif isinstance(data, dict):
                message[key] = {
                    'message': data.get('message', None),
                    'wipe': data.get('wipe', True),
                    'regex': data.get('regex', False),
                }

            else:
                raise ValueError('Invalid data type for %s' % key)

        assert_function = kwargs.get(
            'assert_func', lambda *args, **kwargs: True)

        exit_code = kwargs.get('exit_code', 2)

        # args for testee
        test_args = args

        # kwargs for testee, excludes those that are used in assert_system_exit
        test_kwargs = dict((k, v) for k, v in kwargs.items()
                           if k not in reserved)

        # check activate_session must be type of None or string
        if activate and not isinstance(activate, six.string_types):
            raise ValueError('activate_session is not a string')

        session_patch = mock.patch(activate) if activate else _dummy_()
        stdout_patch = mock.patch('sys.stdout', new_callable=six.StringIO)
        stderr_patch = mock.patch('sys.stderr', new_callable=six.StringIO)

        with session_patch as session:
            with stdout_patch as stdout:
                with stderr_patch as stderr:
                    with self.assertRaises(SystemExit) as ex:
                        callableObj(*test_args, **test_kwargs)
                    self.assertExitCode(ex, exit_code)
        session.assert_called_once()
        self.assert_console_message(stdout, **message['stdout'])
        self.assert_console_message(stderr, **message['stderr'])
        assert_function()

    @mock.patch('koji_cli.commands.activate_session')
    def assert_help(self, callableObj, message, activate_session_mock):
        # optarse uses gettext directly and it is driven by LANGUAGE
        # we need english to get comparable strings
        os.environ['LANGUAGE'] = 'C'
        self.assert_system_exit(
            callableObj,
            mock.MagicMock(),
            mock.MagicMock(),
            ['--help'],
            stdout=message,
            stderr='',
            activate_session=None,
            exit_code=0)
        activate_session_mock.assert_not_called()

    def assertExitCode(self, ex, code):
        if isinstance(ex.exception, int):
            self.assertEqual(ex.exception, code)
        else:
            self.assertEqual(ex.exception.code, code)


def get_builtin_open():
    if six.PY2:
        return '__builtin__.open'
    else:
        return 'builtins.open'
