#       Licensed to the Apache Software Foundation (ASF) under one
#       or more contributor license agreements.  See the NOTICE file
#       distributed with this work for additional information
#       regarding copyright ownership.  The ASF licenses this file
#       to you 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 shutil
import tempfile
import textwrap
import os

from paste.deploy.converters import asint
import ming
from cryptography.hazmat.primitives.twofactor import InvalidToken
from mock import patch, Mock
import pytest
from tg import config

from allura import model as M
from allura.lib.exceptions import InvalidRecoveryCode, MultifactorRateLimitError
from allura.lib.multifactor import GoogleAuthenticatorFile, TotpService, MongodbTotpService
from allura.lib.multifactor import GoogleAuthenticatorPamFilesystemTotpService
from allura.lib.multifactor import RecoveryCodeService, MongodbRecoveryCodeService
from allura.lib.multifactor import GoogleAuthenticatorPamFilesystemRecoveryCodeService


class TestGoogleAuthenticatorFile:
    sample = textwrap.dedent('''\
        7CL3WL756ISQCU5HRVNAODC44Q
        " RATE_LIMIT 3 30
        " DISALLOW_REUSE
        " TOTP_AUTH
        43504045
        16951331
        16933944
        38009587
        49571579
        ''')
    # different key length
    sample2 = textwrap.dedent('''\
        LQQTTQUEW3VAGA6O5XICCWGBXUWXI737
        " TOTP_AUTH
        ''')

    def test_parse(self):
        gaf = GoogleAuthenticatorFile.load(self.sample)
        assert gaf.key == b'\xf8\x97\xbb/\xfd\xf2%\x01S\xa7\x8dZ\x07\x0c\\\xe4'
        assert gaf.options['RATE_LIMIT'] == '3 30'
        assert gaf.options['DISALLOW_REUSE'] is None
        assert gaf.options['TOTP_AUTH'] is None
        assert gaf.recovery_codes == [
            '43504045',
            '16951331',
            '16933944',
            '38009587',
            '49571579',
        ]

    def test_dump(self):
        gaf = GoogleAuthenticatorFile.load(self.sample)
        assert gaf.dump() == self.sample

    def test_dump2(self):
        gaf = GoogleAuthenticatorFile.load(self.sample2)
        assert gaf.dump() == self.sample2


class GenericTotpService(TotpService):
    def enforce_rate_limit(self, *args, **kwargs):
        pass


class TestTotpService:

    sample_key = b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a'
    sample_time = 1472502664
    # these generate code 283397

    def test_constructor(self):
        totp = TotpService().Totp(key=None)
        assert totp

    @patch('allura.lib.multifactor.time')
    def test_verify_types(self, time):
        time.return_value = self.sample_time
        srv = GenericTotpService()
        totp = srv.Totp(key=self.sample_key)
        srv.verify(totp, '283 397', None)
        srv.verify(totp, '283397', None)

    @patch('allura.lib.multifactor.time')
    def test_verify_window(self, time):
        time.return_value = self.sample_time
        srv = GenericTotpService()
        totp = srv.Totp(key=self.sample_key)
        srv.verify(totp, '283397', None)

        time.return_value = self.sample_time + 30
        srv.verify(totp, '283397', None)

        time.return_value = self.sample_time + 60
        with pytest.raises(InvalidToken):
            srv.verify(totp, '283397', None)

        time.return_value = self.sample_time - 30
        with pytest.raises(InvalidToken):
            srv.verify(totp, '283397', None)

    def test_get_qr_code(self):
        srv = TotpService()
        totp = srv.Totp(key=None)
        user = Mock(username='some-user-guy')
        config['site_name'] = 'Our Website'
        assert srv.get_qr_code(totp, user)


class TestAnyTotpServiceImplementation:

    __test__ = False

    sample_key = b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a'
    sample_time = 1472502664
    # these generate code 283397

    def mock_user(self):
        return M.User(username='some-user-guy')

    def test_none(self):
        srv = self.Service()
        user = self.mock_user()
        assert srv.get_secret_key(user) is None

    def test_set_get(self):
        srv = self.Service()
        user = self.mock_user()
        srv.set_secret_key(user, self.sample_key)
        assert self.sample_key == srv.get_secret_key(user)

    def test_delete(self):
        srv = self.Service()
        user = self.mock_user()
        srv.set_secret_key(user, self.sample_key)
        assert self.sample_key == srv.get_secret_key(user)
        srv.set_secret_key(user, None)
        assert srv.get_secret_key(user) is None

    @patch('allura.lib.multifactor.time')
    def test_rate_limiting(self, time):
        time.return_value = self.sample_time
        srv = self.Service()
        user = self.mock_user()
        totp = srv.Totp(key=self.sample_key)

        # 4th attempt (good or bad) will trip over the default limit of 3 in 30s
        with pytest.raises(InvalidToken):
            srv.verify(totp, '34dfvdasf', user)
        with pytest.raises(InvalidToken):
            srv.verify(totp, '234asdfsadf', user)
        srv.verify(totp, '283397', user)
        with pytest.raises(MultifactorRateLimitError):
            srv.verify(totp, '283397', user)


class TestMongodbTotpService(TestAnyTotpServiceImplementation):

    __test__ = True
    Service = MongodbTotpService

    def setup_method(self, method):
        config = {
            'ming.main.uri': 'mim://host/allura_test',
        }
        ming.configure(**config)


class TestGoogleAuthenticatorPamFilesystemMixin:

    def setup_method(self, method):
        self.totp_basedir = tempfile.mkdtemp(prefix='totp-test', dir=os.getenv('TMPDIR', '/tmp'))
        config['auth.multifactor.totp.filesystem.basedir'] = self.totp_basedir

    def teardown_method(self, method):
        if os.path.exists(self.totp_basedir):
            shutil.rmtree(self.totp_basedir)


class TestGoogleAuthenticatorPamFilesystemTotpService(TestAnyTotpServiceImplementation,
                                                      TestGoogleAuthenticatorPamFilesystemMixin):

    __test__ = True
    Service = GoogleAuthenticatorPamFilesystemTotpService

    def test_rate_limiting(self):
        # make a regular .google-authenticator file first, so rate limit info has somewhere to go
        self.Service().set_secret_key(self.mock_user(), self.sample_key)
        # then run test
        super().test_rate_limiting()


class TestRecoveryCodeService:

    def test_generate_one_code(self):
        code = RecoveryCodeService().generate_one_code()
        assert code
        another_code = RecoveryCodeService().generate_one_code()
        assert code != another_code

    def test_regenerate_codes(self):
        class DummyRecoveryService(RecoveryCodeService):
            def replace_codes(self, user, codes):
                self.saved_user = user
                self.saved_codes = codes
        recovery = DummyRecoveryService()
        user = Mock(username='some-user-guy')

        recovery.regenerate_codes(user)

        assert recovery.saved_user == user
        assert len(recovery.saved_codes) == asint(config.get('auth.multifactor.recovery_code.count', 10))


class TestAnyRecoveryCodeServiceImplementation:

    __test__ = False

    def mock_user(self):
        return M.User(username='some-user-guy')

    def test_get_codes_none(self):
        recovery = self.Service()
        user = self.mock_user()
        assert recovery.get_codes(user) == []

    def test_regen_get_codes(self):
        recovery = self.Service()
        user = self.mock_user()
        recovery.regenerate_codes(user)
        assert recovery.get_codes(user)

    def test_replace_codes(self):
        recovery = self.Service()
        user = self.mock_user()
        codes = [
            '12345',
            '67890'
        ]
        recovery.replace_codes(user, codes)
        assert recovery.get_codes(user) == codes

    def test_verify_fail(self):
        recovery = self.Service()
        user = self.mock_user()
        with pytest.raises(InvalidRecoveryCode):
            recovery.verify_and_remove_code(user, '11111')
        with pytest.raises(InvalidRecoveryCode):
            recovery.verify_and_remove_code(user, '')

    def test_verify_and_remove_code(self):
        recovery = self.Service()
        user = self.mock_user()
        codes = [
            '12345',
            '67890'
        ]
        recovery.replace_codes(user, codes)
        result = recovery.verify_and_remove_code(user, '12345')
        assert result is True
        assert recovery.get_codes(user) == ['67890']

    def test_rate_limiting(self):
        recovery = self.Service()
        user = self.mock_user()
        codes = [
            '11111',
            '22222',
        ]
        recovery.replace_codes(user, codes)

        # 4th attempt (good or bad) will trip over the default limit of 3 in 30s
        with pytest.raises(InvalidRecoveryCode):
            recovery.verify_and_remove_code(user, '13485u0233')
        with pytest.raises(InvalidRecoveryCode):
            recovery.verify_and_remove_code(user, '34123rdxafs')
        recovery.verify_and_remove_code(user, '11111')
        with pytest.raises(MultifactorRateLimitError):
            recovery.verify_and_remove_code(user, '22222')


class TestMongodbRecoveryCodeService(TestAnyRecoveryCodeServiceImplementation):

    __test__ = True

    Service = MongodbRecoveryCodeService

    def setup_method(self, method):
        config = {
            'ming.main.uri': 'mim://host/allura_test',
        }
        ming.configure(**config)


class TestGoogleAuthenticatorPamFilesystemRecoveryCodeService(TestAnyRecoveryCodeServiceImplementation,
                                                              TestGoogleAuthenticatorPamFilesystemMixin):

    __test__ = True

    Service = GoogleAuthenticatorPamFilesystemRecoveryCodeService

    def setup_method(self, method):
        super().setup_method(method)

        # make a regular .google-authenticator file first, so recovery keys have somewhere to go
        GoogleAuthenticatorPamFilesystemTotpService().set_secret_key(self.mock_user(),
                                                                     b'\x00K\xda\xbfv\xc2B\xaa\x1a\xbe\xa5\x96b\xb2\xa0Z:\xc9\xcf\x8a')

    def test_get_codes_none_when_no_file(self):
        # this deletes the file
        GoogleAuthenticatorPamFilesystemTotpService().set_secret_key(self.mock_user(), None)

        super().test_get_codes_none()

    def test_replace_codes_when_no_file(self):
        # this deletes the file
        GoogleAuthenticatorPamFilesystemTotpService().set_secret_key(self.mock_user(), None)

        # then it errors because no .google-authenticator file
        with pytest.raises(IOError):
            super().test_replace_codes()
