# -*- coding: utf-8 -*-
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
# more details.
#
# You should have received a copy of the GNU General Public License version 3
# along with this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# (c) 2016 Valentin Samir
"""Tests module for utils"""
from django.test import TestCase, RequestFactory
from django.db import connection

import six
import warnings
import datetime

from cas_server import utils


class CheckPasswordCase(TestCase):
    """Tests for the utils function `utils.check_password`"""

    def setUp(self):
        """Generate random bytes string that will be used ass passwords"""
        self.password1 = utils.gen_saml_id()
        self.password2 = utils.gen_saml_id()
        if not isinstance(self.password1, bytes):  # pragma: no cover executed only in python3
            self.password1 = self.password1.encode("utf8")
            self.password2 = self.password2.encode("utf8")

    def test_setup(self):
        """check that generated password are bytes"""
        self.assertIsInstance(self.password1, bytes)
        self.assertIsInstance(self.password2, bytes)

    def test_plain(self):
        """test the plain auth method"""
        self.assertTrue(utils.check_password("plain", self.password1, self.password1, "utf8"))
        self.assertFalse(utils.check_password("plain", self.password1, self.password2, "utf8"))

    def test_plain_unicode(self):
        """test the plain auth method with unicode input"""
        self.assertTrue(
            utils.check_password(
                "plain",
                self.password1.decode("utf8"),
                self.password1.decode("utf8"),
                "utf8"
            )
        )
        self.assertFalse(
            utils.check_password(
                "plain",
                self.password1.decode("utf8"),
                self.password2.decode("utf8"),
                "utf8"
            )
        )

    def test_crypt(self):
        """test the crypt auth method"""
        salts = ["$6$UVVAQvrMyXMF3FF3", "aa"]
        hashed_password1 = []
        for salt in salts:
            if six.PY3:
                hashed_password1.append(
                    utils.crypt.crypt(
                        self.password1.decode("utf8"),
                        salt
                    ).encode("utf8")
                )
            else:
                hashed_password1.append(utils.crypt.crypt(self.password1, salt))

        for hp1 in hashed_password1:
            self.assertTrue(utils.check_password("crypt", self.password1, hp1, "utf8"))
            self.assertFalse(utils.check_password("crypt", self.password2, hp1, "utf8"))

        with self.assertRaises(ValueError):
            utils.check_password("crypt", self.password1, b"$truc$s$dsdsd", "utf8")

    def test_ldap_password_valid(self):
        """test the ldap auth method with all the schemes"""
        salt = b"UVVAQvrMyXMF3FF3"
        schemes_salt = [b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}"]
        schemes_nosalt = [b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"]
        hashed_password1 = []
        for scheme in schemes_salt:
            hashed_password1.append(
                utils.LdapHashUserPassword.hash(scheme, self.password1, salt, charset="utf8")
            )
        for scheme in schemes_nosalt:
            hashed_password1.append(
                utils.LdapHashUserPassword.hash(scheme, self.password1, charset="utf8")
            )
        hashed_password1.append(
            utils.LdapHashUserPassword.hash(
                b"{CRYPT}",
                self.password1,
                b"$6$UVVAQvrMyXMF3FF3",
                charset="utf8"
            )
        )
        for hp1 in hashed_password1:
            self.assertIsInstance(hp1, bytes)
            self.assertTrue(utils.check_password("ldap", self.password1, hp1, "utf8"))
            self.assertFalse(utils.check_password("ldap", self.password2, hp1, "utf8"))

    def test_ldap_password_fail(self):
        """test the ldap auth method with malformed hash or bad schemes"""
        salt = b"UVVAQvrMyXMF3FF3"
        schemes_salt = [b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}"]
        schemes_nosalt = [b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"]

        # first try to hash with bad parameters
        with self.assertRaises(utils.LdapHashUserPassword.BadScheme):
            utils.LdapHashUserPassword.hash(b"TOTO", self.password1)
        for scheme in schemes_nosalt:
            with self.assertRaises(utils.LdapHashUserPassword.BadScheme):
                utils.LdapHashUserPassword.hash(scheme, self.password1, salt)
        for scheme in schemes_salt:
            with self.assertRaises(utils.LdapHashUserPassword.BadScheme):
                utils.LdapHashUserPassword.hash(scheme, self.password1)
        with self.assertRaises(utils.LdapHashUserPassword.BadSalt):
            utils.LdapHashUserPassword.hash(b'{CRYPT}', self.password1, b"$truc$toto")

        # then try to check hash with bad hashes
        with self.assertRaises(utils.LdapHashUserPassword.BadHash):
            utils.check_password("ldap", self.password1, b"TOTOssdsdsd", "utf8")
        for scheme in schemes_salt:
            # bad length
            with self.assertRaises(utils.LdapHashUserPassword.BadHash):
                utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw==", "utf8")
            # bad base64
            with self.assertRaises(utils.LdapHashUserPassword.BadHash):
                utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw", "utf8")

    def test_hex(self):
        """test all the hex_HASH method: the hashed password is a simple hash of the password"""
        hashes = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"]
        hashed_password1 = []
        for hash_scheme in hashes:
            hashed_password1.append(
                (
                    "hex_%s" % hash_scheme,
                    getattr(utils.hashlib, hash_scheme)(self.password1).hexdigest()
                )
            )
        for (method, hp1) in hashed_password1:
            self.assertTrue(utils.check_password(method, self.password1, hp1, "utf8"))
            self.assertFalse(utils.check_password(method, self.password2, hp1, "utf8"))

    def test_bad_method(self):
        """try to check password with a bad method, should raise a ValueError"""
        with self.assertRaises(ValueError):
            utils.check_password("test", self.password1, b"$truc$s$dsdsd", "utf8")


class UtilsTestCase(TestCase):
    """tests for some little utils functions"""
    def test_import_attr(self):
        """
            test the import_attr function. Feeded with a dotted path string, it should
            import the dotted module and return that last componend of the dotted path
            (function, class or variable)
        """
        with self.assertRaises(ImportError):
            utils.import_attr('toto.titi.tutu')
        with self.assertRaises(AttributeError):
            utils.import_attr('cas_server.utils.toto')
        with self.assertRaises(ValueError):
            utils.import_attr('toto')
        self.assertEqual(
            utils.import_attr('cas_server.default_app_config'),
            'cas_server.apps.CasAppConfig'
        )
        self.assertEqual(utils.import_attr(utils), utils)

    def test_update_url(self):
        """
            test the update_url function. Given an url with possible GET parameter and a dict
            the function build a url with GET parameters updated by the dictionnary
        """
        url1 = utils.update_url(u"https://www.example.com?toto=1", {u"tata": u"2"})
        url2 = utils.update_url(b"https://www.example.com?toto=1", {b"tata": b"2"})
        self.assertEqual(url1, u"https://www.example.com?tata=2&toto=1")
        self.assertEqual(url2, u"https://www.example.com?tata=2&toto=1")

        url3 = utils.update_url(u"https://www.example.com?toto=1", {u"toto": u"2"})
        self.assertEqual(url3, u"https://www.example.com?toto=2")

    def test_crypt_salt_is_valid(self):
        """test the function crypt_salt_is_valid who test if a crypt salt is valid"""
        self.assertFalse(utils.crypt_salt_is_valid(""))  # len 0
        self.assertFalse(utils.crypt_salt_is_valid("a"))  # len 1
        self.assertFalse(utils.crypt_salt_is_valid("$$"))  # start with $ followed by $
        self.assertFalse(utils.crypt_salt_is_valid("$toto"))  # start with $ but no secondary $
        self.assertFalse(utils.crypt_salt_is_valid("$toto$toto"))  # algorithm toto not known

    def test_get_current_url(self):
        """test the function get_current_url"""
        factory = RequestFactory()
        request = factory.get('/truc/muche?test=1')
        self.assertEqual(utils.get_current_url(request), 'http://testserver/truc/muche?test=1')
        self.assertEqual(
            utils.get_current_url(request, ignore_params={'test'}),
            'http://testserver/truc/muche'
        )

    def test_get_tuple(self):
        """test the function get_tuple"""
        test_tuple = (1, 2, 3)
        for index, value in enumerate(test_tuple):
            self.assertEqual(utils.get_tuple(test_tuple, index), value)
        self.assertEqual(utils.get_tuple(test_tuple, 3), None)
        self.assertEqual(utils.get_tuple(test_tuple, 3, 'toto'), 'toto')
        self.assertEqual(utils.get_tuple(None, 3), None)

    def test_last_version(self):
        """
            test the function last_version. An internet connection is needed, if you do not have
            one, this test will fail and you should ignore it.
        """
        try:
            # first check if pypi is available
            utils.requests.get("https://pypi.python.org/simple/django-cas-server/")
        except utils.requests.exceptions.RequestException:
            warnings.warn(
                (
                    "Pypi seems not available, perhaps you do not have internet access. "
                    "Consequently, the test cas_server.tests.test_utils.UtilsTestCase.test_last_"
                    "version is ignored"
                ),
                RuntimeWarning
            )
        else:
            version = utils.last_version()
            self.assertIsInstance(version, six.text_type)
            self.assertEqual(len(version.split('.')), 3)

            # version is cached 24h so calling it a second time should return the save value
            self.assertEqual(version, utils.last_version())

    def test_dictfetchall(self):
        """test the function dictfetchall"""
        with connection.cursor() as curs:
            curs.execute("SELECT * FROM django_migrations")
            results = utils.dictfetchall(curs)
            self.assertIsInstance(results, list)
            self.assertTrue(len(results) > 0)
            for result in results:
                self.assertIsInstance(result, dict)
                self.assertIn('applied', result)
                self.assertIsInstance(result['applied'], datetime.datetime)

    def test_regexpr_validator(self):
        """test the function regexpr_validator"""
        utils.regexpr_validator("^a$")
        with self.assertRaises(utils.ValidationError):
            utils.regexpr_validator("[")
