# -*- coding: utf-8 -*-
"""
    tests.unit.version_test
    ~~~~~~~~~~~~~~~~~~~~~~~

    These tests are copied from python's source `Lib/distutils/tests/test_version.py`
    Some new examples were added and some adjustments were made to run tests in python 2 and 3
"""
# pylint: disable=string-substitution-usage-error

from __future__ import absolute_import, print_function, unicode_literals

import datetime
import os
import sys
import warnings

import salt.modules.cmdmod
import salt.utils.platform
import salt.utils.versions
import salt.version
from salt.ext import six
from salt.utils.versions import LooseVersion, StrictVersion
from tests.support.helpers import slowTest
from tests.support.mock import patch
from tests.support.paths import CODE_DIR
from tests.support.unit import TestCase, skipIf

if six.PY2:
    cmp_method = "__cmp__"
else:
    cmp_method = "_cmp"


class VersionTestCase(TestCase):
    def test_prerelease(self):
        version = StrictVersion("1.2.3a1")
        self.assertEqual(version.version, (1, 2, 3))
        self.assertEqual(version.prerelease, ("a", 1))
        self.assertEqual(six.text_type(version), "1.2.3a1")

        version = StrictVersion("1.2.0")
        self.assertEqual(six.text_type(version), "1.2")

    def test_cmp_strict(self):
        versions = (
            ("1.5.1", "1.5.2b2", -1),
            ("161", "3.10a", ValueError),
            ("8.02", "8.02", 0),
            ("3.4j", "1996.07.12", ValueError),
            ("3.2.pl0", "3.1.1.6", ValueError),
            ("2g6", "11g", ValueError),
            ("0.9", "2.2", -1),
            ("1.2.1", "1.2", 1),
            ("1.1", "1.2.2", -1),
            ("1.2", "1.1", 1),
            ("1.2.1", "1.2.2", -1),
            ("1.2.2", "1.2", 1),
            ("1.2", "1.2.2", -1),
            ("0.4.0", "0.4", 0),
            ("1.13++", "5.5.kw", ValueError),
            # Added by us
            ("1.1.1a1", "1.1.1", -1),
        )

        for v1, v2, wanted in versions:
            try:
                res = getattr(StrictVersion(v1), cmp_method)(StrictVersion(v2))
            except ValueError:
                if wanted is ValueError:
                    continue
                else:
                    raise AssertionError(
                        ("cmp(%s, %s) " "shouldn't raise ValueError") % (v1, v2)
                    )
            self.assertEqual(
                res, wanted, "cmp(%s, %s) should be %s, got %s" % (v1, v2, wanted, res)
            )

    def test_cmp(self):
        versions = (
            ("1.5.1", "1.5.2b2", -1),
            ("161", "3.10a", 1),
            ("8.02", "8.02", 0),
            ("3.4j", "1996.07.12", -1),
            ("3.2.pl0", "3.1.1.6", 1),
            ("2g6", "11g", -1),
            ("0.960923", "2.2beta29", -1),
            ("1.13++", "5.5.kw", -1),
            # Added by us
            ("3.10.0-514.el7", "3.10.0-514.6.1.el7", 1),
            ("2.2.2", "2.12.1", -1),
        )

        for v1, v2, wanted in versions:
            res = getattr(LooseVersion(v1), cmp_method)(LooseVersion(v2))
            self.assertEqual(
                res, wanted, "cmp(%s, %s) should be %s, got %s" % (v1, v2, wanted, res)
            )

    @skipIf(not salt.utils.platform.is_linux(), "only need to run on linux")
    @slowTest
    def test_spelling_version_name(self):
        """
        check the spelling of the version name for the release
        names in the salt.utils.versions.warn_until call
        """
        query = "salt.utils.versions.warn_until("
        names = salt.version.SaltStackVersion.NAMES

        cmd = "grep -lr {} -A 1 {}".format(query, os.path.join(CODE_DIR, "salt"))

        grep_call = salt.modules.cmdmod.run_stdout(cmd=cmd).split(os.linesep)

        for line in grep_call:
            num_cmd = salt.modules.cmdmod.run_stdout(
                "grep -c {0} {1}".format(query, line)
            )
            ver_cmd = salt.modules.cmdmod.run_stdout(
                "grep {0} {1} -A 1".format(query, line)
            )
            if "pyc" in line:
                break

            match = 0
            for key in names:
                if key in ver_cmd:
                    match = match + (ver_cmd.count(key))
            if "utils/__init__.py" in line:
                # work around for utils/__init__.py because
                # it includes the warn_utils function
                match = match + 1
            self.assertEqual(
                match,
                int(num_cmd),
                msg="The file: {0} has an "
                "incorrect spelling for the release name in the warn_utils "
                "call: {1}. Expecting one of these release names: "
                "{2}".format(line, ver_cmd, names),
            )


class VersionFuncsTestCase(TestCase):
    def test_compare(self):
        ret = salt.utils.versions.compare("1.0", "==", "1.0")
        self.assertTrue(ret)

        ret = salt.utils.versions.compare("1.0", "!=", "1.0")
        self.assertFalse(ret)

        with patch.object(salt.utils.versions, "log") as log_mock:
            ret = salt.utils.versions.compare(
                "1.0", "HAH I AM NOT A COMP OPERATOR! I AM YOUR FATHER!", "1.0"
            )
            self.assertTrue(log_mock.error.called)

    def test_kwargs_warn_until(self):
        # Test invalid version arg
        self.assertRaises(RuntimeError, salt.utils.versions.kwargs_warn_until, {}, [])

    def test_warn_until_warning_raised(self):
        # We *always* want *all* warnings thrown on this module
        warnings.filterwarnings("always", "", DeprecationWarning, __name__)

        def raise_warning(_version_info_=(0, 16, 0)):
            salt.utils.versions.warn_until(
                (0, 17), "Deprecation Message!", _version_info_=_version_info_
            )

        def raise_named_version_warning(_version_info_=(0, 16, 0)):
            salt.utils.versions.warn_until(
                "Hydrogen", "Deprecation Message!", _version_info_=_version_info_
            )

        # raise_warning should show warning until version info is >= (0, 17)
        with warnings.catch_warnings(record=True) as recorded_warnings:
            raise_warning()
            self.assertEqual(
                "Deprecation Message!", six.text_type(recorded_warnings[0].message)
            )

        # raise_warning should show warning until version info is >= (0, 17)
        with warnings.catch_warnings(record=True) as recorded_warnings:
            raise_named_version_warning()
            self.assertEqual(
                "Deprecation Message!", six.text_type(recorded_warnings[0].message)
            )

        # the deprecation warning is not issued because we passed
        # _dont_call_warning
        with warnings.catch_warnings(record=True) as recorded_warnings:
            salt.utils.versions.warn_until(
                (0, 17), "Foo", _dont_call_warnings=True, _version_info_=(0, 16)
            )
            self.assertEqual(0, len(recorded_warnings))

        # Let's set version info to (0, 17), a RuntimeError should be raised
        with self.assertRaisesRegex(
            RuntimeError,
            r"The warning triggered on filename \'(.*)test_versions.py\', "
            r"line number ([\d]+), is supposed to be shown until version "
            r"0.17.0 is released. Current version is now 0.17.0. "
            r"Please remove the warning.",
        ):
            raise_warning(_version_info_=(0, 17, 0))

        # Let's set version info to (0, 17), a RuntimeError should be raised
        with self.assertRaisesRegex(
            RuntimeError,
            r"The warning triggered on filename \'(.*)test_versions.py\', "
            r"line number ([\d]+), is supposed to be shown until version "
            r"(.*) is released. Current version is now "
            r"([\d.]+). Please remove the warning.",
        ):
            raise_named_version_warning(
                _version_info_=(
                    getattr(sys, "maxint", None) or getattr(sys, "maxsize"),
                    16,
                    0,
                )
            )

        # Even though we're calling warn_until, we pass _dont_call_warnings
        # because we're only after the RuntimeError
        with self.assertRaisesRegex(
            RuntimeError,
            r"The warning triggered on filename \'(.*)test_versions.py\', "
            r"line number ([\d]+), is supposed to be shown until version "
            r"0.17.0 is released. Current version is now "
            r"(.*). Please remove the warning.",
        ):
            salt.utils.versions.warn_until((0, 17), "Foo", _dont_call_warnings=True)

        with self.assertRaisesRegex(
            RuntimeError,
            r"The warning triggered on filename \'(.*)test_versions.py\', "
            r"line number ([\d]+), is supposed to be shown until version "
            r"(.*) is released. Current version is now "
            r"(.*). Please remove the warning.",
        ):
            salt.utils.versions.warn_until(
                "Hydrogen",
                "Foo",
                _dont_call_warnings=True,
                _version_info_=(
                    getattr(sys, "maxint", None) or getattr(sys, "maxsize"),
                    16,
                    0,
                ),
            )

        # version on the deprecation message gets properly formatted
        with warnings.catch_warnings(record=True) as recorded_warnings:
            vrs = salt.version.SaltStackVersion.from_name("Helium")
            salt.utils.versions.warn_until(
                "Helium",
                "Deprecation Message until {version}!",
                _version_info_=(vrs.major - 1, 0),
            )
            self.assertEqual(
                "Deprecation Message until {0}!".format(vrs.formatted_version),
                six.text_type(recorded_warnings[0].message),
            )

    def test_kwargs_warn_until_warning_raised(self):
        # We *always* want *all* warnings thrown on this module
        warnings.filterwarnings("always", "", DeprecationWarning, __name__)

        def raise_warning(**kwargs):
            _version_info_ = kwargs.pop("_version_info_", (0, 16, 0))
            salt.utils.versions.kwargs_warn_until(
                kwargs, (0, 17), _version_info_=_version_info_
            )

        # raise_warning({...}) should show warning until version info is >= (0, 17)
        with warnings.catch_warnings(record=True) as recorded_warnings:
            raise_warning(foo=42)  # with a kwarg
            self.assertEqual(
                "The following parameter(s) have been deprecated and "
                "will be removed in '0.17.0': 'foo'.",
                six.text_type(recorded_warnings[0].message),
            )
        # With no **kwargs, should not show warning until version info is >= (0, 17)
        with warnings.catch_warnings(record=True) as recorded_warnings:
            salt.utils.versions.kwargs_warn_until(
                {}, (0, 17), _version_info_=(0, 16, 0)  # no kwargs
            )
            self.assertEqual(0, len(recorded_warnings))

        # Let's set version info to (0, 17), a RuntimeError should be raised
        # regardless of whether or not we pass any **kwargs.
        with self.assertRaisesRegex(
            RuntimeError,
            r"The warning triggered on filename \'(.*)test_versions.py\', "
            r"line number ([\d]+), is supposed to be shown until version "
            r"0.17.0 is released. Current version is now 0.17.0. "
            r"Please remove the warning.",
        ):
            raise_warning(_version_info_=(0, 17))  # no kwargs

        with self.assertRaisesRegex(
            RuntimeError,
            r"The warning triggered on filename \'(.*)test_versions.py\', "
            r"line number ([\d]+), is supposed to be shown until version "
            r"0.17.0 is released. Current version is now 0.17.0. "
            r"Please remove the warning.",
        ):
            raise_warning(bar="baz", qux="quux", _version_info_=(0, 17))  # some kwargs

    def test_warn_until_date_warning_raised(self):
        # We *always* want *all* warnings thrown on this module
        warnings.filterwarnings("always", "", DeprecationWarning, __name__)

        _current_date = datetime.date(2000, 1, 1)

        # Test warning with datetime.date instance
        with warnings.catch_warnings(record=True) as recorded_warnings:
            salt.utils.versions.warn_until_date(
                datetime.date(2000, 1, 2),
                "Deprecation Message!",
                _current_date=_current_date,
            )
            self.assertEqual(
                "Deprecation Message!", six.text_type(recorded_warnings[0].message)
            )

        # Test warning with datetime.datetime instance
        with warnings.catch_warnings(record=True) as recorded_warnings:
            salt.utils.versions.warn_until_date(
                datetime.datetime(2000, 1, 2),
                "Deprecation Message!",
                _current_date=_current_date,
            )
            self.assertEqual(
                "Deprecation Message!", six.text_type(recorded_warnings[0].message)
            )

        # Test warning with date as a string
        with warnings.catch_warnings(record=True) as recorded_warnings:
            salt.utils.versions.warn_until_date(
                "20000102", "Deprecation Message!", _current_date=_current_date
            )
            self.assertEqual(
                "Deprecation Message!", six.text_type(recorded_warnings[0].message)
            )

        # the deprecation warning is not issued because we passed
        # _dont_call_warning
        with warnings.catch_warnings(record=True) as recorded_warnings:
            salt.utils.versions.warn_until_date(
                "20000102",
                "Deprecation Message!",
                _dont_call_warnings=True,
                _current_date=_current_date,
            )
            self.assertEqual(0, len(recorded_warnings))

        # Let's test for RuntimeError raise
        with self.assertRaisesRegex(
            RuntimeError,
            r"Deprecation Message! This warning\(now exception\) triggered on "
            r"filename \'(.*)test_versions.py\', line number ([\d]+), is "
            r"supposed to be shown until ([\d-]+). Today is ([\d-]+). "
            r"Please remove the warning.",
        ):
            salt.utils.versions.warn_until_date("20000101", "Deprecation Message!")

        # Even though we're calling warn_until_date, we pass _dont_call_warnings
        # because we're only after the RuntimeError
        with self.assertRaisesRegex(
            RuntimeError,
            r"Deprecation Message! This warning\(now exception\) triggered on "
            r"filename \'(.*)test_versions.py\', line number ([\d]+), is "
            r"supposed to be shown until ([\d-]+). Today is ([\d-]+). "
            r"Please remove the warning.",
        ):
            salt.utils.versions.warn_until_date(
                "20000101",
                "Deprecation Message!",
                _dont_call_warnings=True,
                _current_date=_current_date,
            )

    def test_warn_until_date_bad_strptime_format(self):
        # We *always* want *all* warnings thrown on this module
        warnings.filterwarnings("always", "", DeprecationWarning, __name__)

        # Let's test for RuntimeError raise
        with self.assertRaisesRegex(
            ValueError, "time data '0022' does not match format '%Y%m%d'"
        ):
            salt.utils.versions.warn_until_date("0022", "Deprecation Message!")
