# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import functools
from types import MethodType
import uuid

from oslo_log import log as logging
import testtools

from tempest.lib import exceptions as lib_exc

LOG = logging.getLogger(__name__)

_SUPPORTED_BUG_TYPES = {
    'launchpad': 'https://launchpad.net/bugs/%s',
    'storyboard': 'https://storyboard.openstack.org/#!/story/%s',
}


def _validate_bug_and_bug_type(bug, bug_type):
    """Validates ``bug`` and ``bug_type`` values.

    :param bug: bug number causing the test to skip (launchpad or storyboard)
    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
    :raises: InvalidParam if ``bug`` is not a digit or ``bug_type`` is not
        a valid value
    """
    if not bug.isdigit():
        invalid_param = '%s must be a valid %s number' % (bug, bug_type)
        raise lib_exc.InvalidParam(invalid_param=invalid_param)
    if bug_type not in _SUPPORTED_BUG_TYPES:
        invalid_param = 'bug_type "%s" must be one of: %s' % (
            bug_type, ', '.join(_SUPPORTED_BUG_TYPES.keys()))
        raise lib_exc.InvalidParam(invalid_param=invalid_param)


def _get_bug_url(bug, bug_type='launchpad'):
    """Get the bug URL based on the ``bug_type`` and ``bug``

    :param bug: The launchpad/storyboard bug number causing the test
    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
    :returns: Bug URL corresponding to ``bug_type`` value
    """
    _validate_bug_and_bug_type(bug, bug_type)
    return _SUPPORTED_BUG_TYPES[bug_type] % bug


def skip_because(*args, **kwargs):
    """A decorator useful to skip tests hitting known bugs

    ``bug`` must be a number and ``condition`` must be true for the test to
    skip.

    :param bug: bug number causing the test to skip (launchpad or storyboard)
    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
    :param condition: optional condition to be True for the skip to have place
    :raises: testtools.TestCase.skipException if ``condition`` is True and
        ``bug`` is included
    """
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*func_args, **func_kwargs):
            condition = kwargs.get('condition', True)
            bug = kwargs.get('bug', None)
            if bug and condition:
                bug_type = kwargs.get('bug_type', 'launchpad')
                bug_url = _get_bug_url(bug, bug_type)
                raise testtools.TestCase.skipException(
                    "Skipped until bug: %s is resolved." % bug_url)
            return f(*func_args, **func_kwargs)
        return wrapper
    return decorator


def related_bug(bug, status_code=None, bug_type='launchpad'):
    """A decorator useful to know solutions from launchpad/storyboard reports

    :param bug: The launchpad/storyboard bug number causing the test bug
    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
    :param status_code: The status code related to the bug report
    """
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*func_args, **func_kwargs):
            try:
                return f(*func_args, **func_kwargs)
            except Exception as exc:
                exc_status_code = getattr(exc, 'status_code', None)
                if status_code is None or status_code == exc_status_code:
                    if bug:
                        LOG.error('Hints: This test was made for the bug_type '
                                  '%s. The failure could be related to '
                                  '%s', bug, _get_bug_url(bug, bug_type))
                raise exc
        return wrapper
    return decorator


def idempotent_id(id):
    """Stub for metadata decorator"""
    if not isinstance(id, str):
        raise TypeError('Test idempotent_id must be string not %s'
                        '' % type(id).__name__)
    uuid.UUID(id)

    def decorator(f):
        f = testtools.testcase.attr('id-%s' % id)(f)
        if f.__doc__:
            f.__doc__ = 'Test idempotent id: %s\n\n%s' % (id, f.__doc__)
        else:
            f.__doc__ = 'Test idempotent id: %s' % id
        return f
    return decorator


def attr(**kwargs):
    """A decorator which applies the testtools attr decorator

    This decorator applies the testtools.testcase.attr if it is in the list of
    attributes to testtools we want to apply.

    :param condition: Optional condition which if true will apply the attr. If
        a condition is specified which is false the attr will not be applied to
        the test function. If not specified, the attr is always applied.
    """

    def decorator(f):
        # Check to see if the attr should be conditional applied.
        if 'condition' in kwargs and not kwargs.get('condition'):
            return f
        if 'type' in kwargs and isinstance(kwargs['type'], str):
            f = testtools.testcase.attr(kwargs['type'])(f)
        elif 'type' in kwargs and isinstance(kwargs['type'], list):
            for attr in kwargs['type']:
                f = testtools.testcase.attr(attr)(f)
        return f

    return decorator


def unstable_test(*args, **kwargs):
    """A decorator useful to run tests hitting known bugs and skip it if fails

    This decorator can be used in cases like:

    * We have skipped tests with some bug and now bug is claimed to be fixed.
      Now we want to check the test stability so we use this decorator.
      The number of skipped cases with that bug can be counted to mark test
      stable again.
    * There is test which is failing often, but not always. If there is known
      bug related to it, and someone is working on fix, this decorator can be
      used instead of "skip_because". That will ensure that test is still run
      so new debug data can be collected from jobs' logs but it will not make
      life of other developers harder by forcing them to recheck jobs more
      often.

    ``bug`` must be a number for the test to skip.

    :param bug: bug number causing the test to skip (launchpad or storyboard)
    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
    :raises: testtools.TestCase.skipException if test actually fails,
        and ``bug`` is included
    """
    def decor(f):
        @functools.wraps(f)
        def inner(self, *func_args, **func_kwargs):
            try:
                return f(self, *func_args, **func_kwargs)
            except Exception as e:
                if "bug" in kwargs:
                    bug = kwargs['bug']
                    bug_type = kwargs.get('bug_type', 'launchpad')
                    bug_url = _get_bug_url(bug, bug_type)
                    msg = ("Marked as unstable and skipped because of bug: "
                           "%s, failure was: %s") % (bug_url, e)
                    raise testtools.TestCase.skipException(msg)
                else:
                    raise e
        return inner
    return decor


class cleanup_order:
    """Descriptor for base create function to cleanup based on caller.

    There are functions created as classmethod and the cleanup
    was managed by the class with addClassResourceCleanup,
    In case the function called from a class level (resource_setup) its ok
    But when it is called from testcase level there is no reason to delete the
    resource when class tears down.

    The testcase results will not reflect the resources cleanup because test
    may pass but the class cleanup fails. if the resources were created by
    testcase its better to let the testcase delete them and report failure
    part of the testcase
    """

    def __init__(self, func):
        self.func = func
        functools.update_wrapper(self, func)

    def __get__(self, instance, owner):
        if instance:
            # instance is the caller
            instance.cleanup = instance.addCleanup
            instance.__name__ = owner.__name__
            return MethodType(self.func, instance)
        elif owner:
            # class is the caller
            owner.cleanup = owner.addClassResourceCleanup
            return MethodType(self.func, owner)


def serial(cls):
    """A decorator to mark a test class for serial execution"""
    cls._serial = True
    LOG.debug('marked %s for serial execution', cls.__name__)
    return cls
