"""This module defines entities to be by Content Library B2B Patching modules.

"""
import functools
import logging

from errors import (get_execution_error,
                    get_verification_error)

__author__ = 'VMware, Inc.'
__copyright__ = 'Copyright 2016 VMware, Inc. All rights reserved.'


logger = logging.getLogger(__name__)


class PatchVersion(object):
    """Content Library's internal representation of the patch version.

    A valid patch version has following form: x.y.z,
        where x, y, z are non-negative integers;
        x and y are typically the major and minor version of the product,
            while z is the Content Library's internal patch version.

    Examples:
        >>> dummy_message = ""
        >>> version_651 = PatchVersion("6.5.1", dummy_message)
        >>> version_652 = PatchVersion("6.5.2", dummy_message)
        >>> version_700 = PatchVersion("7.0.0", dummy_message)

    Method overrides help with comparison, sorting of PatchVersions
        >>> version_651 > version_652
        False
        >>> version_651 < version_652
        True
        >>> version_651 < None
        Traceback (most recent call last):
        ...
        TypeError: PatchVersion.__cmp__(x,y) requires y to be a 'PatchVersion', not a 'NoneType'
        >>> l = [version_651, version_700, version_652]
        >>> [v.version for v in sorted(l)]
        ['6.5.1', '6.5.2', '7.0.0']
        >>> [v.version for v in sorted(l, reverse=True)]
        ['7.0.0', '6.5.2', '6.5.1']
        >>> d = {version_651 : '6.5.1', version_652 : '6.5.2', version_700 : '7.0.0'}
        >>> d[version_652]
        '6.5.2'

    Due to overridden __hash__ method, following look up works.
        >>> another_version_652 = PatchVersion("6.5.2", dummy_message)
        >>> d[another_version_652]
        '6.5.2'

    """
    def __init__(self, version, summary):
        self.version = version
        self.summary = summary
        components = [int(i) for i in version.split(".")]
        self.components = tuple(components)

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __gt__(self, other):
        return self.__cmp__(other) > 0

    def __eq__(self, other):
        return self.__cmp__(other) is 0

    def __hash__(self):
        return hash(self.version)

    def __cmp__(self, other):
        if other is None or not isinstance(other, PatchVersion):
            raise TypeError("PatchVersion.__cmp__(x,y) requires y to be a 'PatchVersion', "
                            "not a 'NoneType'")
        for i in range(len(self.components)):
            if self.components[i] > other.components[i]:
                return 1
            elif self.components[i] < other.components[i]:
                return -1
        return 0


class PatchStepExecutor(object):
    """Encapsulates the business logic to be executed when patching to
    the given PatchVersion from the previous one.

    Args:
        version (PatchVersion): version to patch to
        executor (function): business-logic/ state changes to be performed when patching to version

    """
    def __init__(self, version, executor):
        self.version = version
        self.executor = executor

    def execute(self, patch_context):
        logger.info("Applying patch with version: %s", self.version.version)
        try:
            self.executor(patch_context)
        except Exception as e:
            more_info = "Version: %s, %s: %s" % (self.version.version, type(e), e)
            raise get_execution_error(more_info)


class PatchStepVerifier(object):
    """Encapsulates patch verifier.

    Args:
        version (PatchVersion): patch version which this verifier needs to verify
        verifier (function): method which verifies that patch with version is successfully applied.
            This will be invoked just after PatchStepExecutor.execute() is complete.

    """
    def __init__(self, version, verifier):
        self.version = version
        self.verifier = verifier

    def verify(self, patch_context):
        """Verifies the post-conditions after the successful run of the PatchStepExecutor for
        the given version.

        Raises:
            patch_errors.PermanentError: if the patch verification fails. The patch process
                is terminated at this point.

        """
        logger.info("Verifying patch with version: %s", self.version.version)
        try:
            self.verifier(patch_context)
        except Exception as e:
            more_info = "Version: %s, %s: %s" % (self.version.version, type(e), e)
            raise get_verification_error(more_info)


def _validate_patch_version(version):
    """Check if the provided patch version has a valid format.

    Args:
        version (str): version provided via @patch decorator

    Raises:
        ValueError: if the version is invalid

    """
    components = version.split('.')
    if len(components) != 3:
        raise ValueError("Invalid version format. Expected: major.minor.patch")
    for c in components:
        if not int(c) >= 0:
            raise ValueError("Invalid version format. "
                             "Need non-negative integer values for components")


def patch_info(version, summary):
    """Decorator to be applied to a method which performs the patching

    Args:
        version (str): a patch version . The patch method will be invoked when
            patching to this version.
        summary (LocalizableMessage): A brief summary of what this patch does.

    Returns:
        wrapped patch method with decorations

    Examples:
        >>> @patch_info("1.2.3", "this is localized string")
        ... def patch_logic(patch_context):
        ...     pass
        >>> patch_logic._patch_version, patch_logic._patch_summary
        ('1.2.3', 'this is localized string')

        >>> @patch_info("1.2.3", "extra", "args")
        ... def patch_logic(patch_context):
        ...     pass
        Traceback (most recent call last):
        ...
        TypeError: patch_info() takes exactly 2 arguments (3 given)

        >>> @patch_info()
        ... def patch_logic(patch_context):
        ...     pass
        Traceback (most recent call last):
        ...
        TypeError: patch_info() takes exactly 2 arguments (0 given)

        >>> @patch_info("a.b.c", "localized string")
        ... def patch_logic(patch_context):
        ...     pass
        Traceback (most recent call last):
        ...
        ValueError: invalid literal for int() with base 10: 'a'

        >>> @patch_info("6.5.-1", "localized string")
        ... def patch_logic(patch_context):
        ...     pass
        Traceback (most recent call last):
        ...
        ValueError: Invalid version format. Need non-negative integer values for components

        >>> @patch_info("6.5.1.0", "localized string")
        ... def patch_logic(patch_context):
        ...     pass
        Traceback (most recent call last):
        ...
        ValueError: Invalid version format. Expected: major.minor.patch

        >>> @patch_info("0.0.0", "localized string")
        ... def patch_logic(patch_context):
        ...     pass
        >>>

    """
    def decorator(patch_method):
        @functools.wraps(patch_method)
        def wrapper(*args, **kwargs):
            patch_method(*args, **kwargs)
        _validate_patch_version(version)
        wrapper._patch_version = version
        wrapper._patch_summary = summary
        return wrapper
    return decorator


def patch_test(patch_version):
    """Decorator which takes a patch version

    Any method which tests a successful patch needs to be decorated with @patch_test,
    with the patch version it is supposed to test.

    Args:
        patch_version (str): The version of the patch this method tests

    Returns:
        wrapped patch verifier method

    """
    def decorator(test_method):
        @functools.wraps(test_method)
        def wrapper(*args, **kwargs):
            test_method(*args, **kwargs)
        wrapper._patch_test_version = patch_version
        return wrapper
    return decorator


if __name__ == "__main__":
    import doctest
    doctest.testmod()
