#    Copyright 2012 OpenStack Foundation
#
#    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 mock
import pymongo

import trove.common.context as context
import trove.common.utils as utils
import trove.guestagent.backup as backup
import trove.guestagent.datastore.experimental.mongodb.manager as manager
import trove.guestagent.datastore.experimental.mongodb.service as service
import trove.guestagent.db.models as models
import trove.guestagent.volume as volume
import trove.tests.unittests.trove_testtools as trove_testtools


class GuestAgentMongoDBManagerTest(trove_testtools.TestCase):

    @mock.patch.object(service.MongoDBApp, '_init_overrides_dir')
    def setUp(self, _):
        super(GuestAgentMongoDBManagerTest, self).setUp()
        self.context = context.TroveContext()
        self.manager = manager.Manager()

        self.execute_with_timeout_patch = mock.patch.object(
            utils, 'execute_with_timeout'
        )
        self.addCleanup(self.execute_with_timeout_patch.stop)
        self.execute_with_timeout_patch.start()

        self.pymongo_patch = mock.patch.object(
            pymongo, 'MongoClient'
        )
        self.addCleanup(self.pymongo_patch.stop)
        self.pymongo_patch.start()

        self.mount_point = '/var/lib/mongodb'

    def tearDown(self):
        super(GuestAgentMongoDBManagerTest, self).tearDown()

    def test_update_status(self):
        self.manager.app.status = mock.MagicMock()
        self.manager.update_status(self.context)
        self.manager.app.status.update.assert_any_call()

    def _prepare_method(self, packages=['packages'], databases=None,
                        memory_mb='2048', users=None, device_path=None,
                        mount_point=None, backup_info=None,
                        config_contents=None, root_password=None,
                        overrides=None, cluster_config=None,):
        """self.manager.app must be correctly mocked before calling."""

        self.manager.app.status = mock.Mock()

        self.manager.prepare(self.context, packages,
                             databases, memory_mb, users,
                             device_path=device_path,
                             mount_point=mount_point,
                             backup_info=backup_info,
                             config_contents=config_contents,
                             root_password=root_password,
                             overrides=overrides,
                             cluster_config=cluster_config)

        self.manager.app.status.begin_install.assert_any_call()
        self.manager.app.install_if_needed.assert_called_with(packages)
        self.manager.app.stop_db.assert_any_call()
        self.manager.app.clear_storage.assert_any_call()

        (self.manager.app.apply_initial_guestagent_configuration.
         assert_called_once_with(cluster_config, self.mount_point))

    @mock.patch.object(volume, 'VolumeDevice')
    @mock.patch('os.path.exists')
    def test_prepare_for_volume(self, exists, mocked_volume):
        device_path = '/dev/vdb'

        self.manager.app = mock.Mock()

        self._prepare_method(device_path=device_path)

        mocked_volume().unmount_device.assert_called_with(device_path)
        mocked_volume().format.assert_any_call()
        mocked_volume().migrate_data.assert_called_with(self.mount_point)
        mocked_volume().mount.assert_called_with(self.mount_point)

    def test_secure(self):
        self.manager.app = mock.Mock()

        mock_secure = mock.Mock()
        self.manager.app.secure = mock_secure

        self._prepare_method()

        mock_secure.assert_called_with()

    @mock.patch.object(backup, 'restore')
    @mock.patch.object(service.MongoDBAdmin, 'is_root_enabled')
    def test_prepare_from_backup(self, mocked_root_check, mocked_restore):
        self.manager.app = mock.Mock()

        backup_info = {'id': 'backup_id_123abc',
                       'location': 'fake-location',
                       'type': 'MongoDBDump',
                       'checksum': 'fake-checksum'}

        self._prepare_method(backup_info=backup_info)

        mocked_restore.assert_called_with(self.context, backup_info,
                                          '/var/lib/mongodb')
        mocked_root_check.assert_any_call()

    def test_prepare_with_databases(self):
        self.manager.app = mock.Mock()

        database = mock.Mock()
        mock_create_databases = mock.Mock()
        self.manager.create_database = mock_create_databases

        self._prepare_method(databases=[database])

        mock_create_databases.assert_called_with(self.context, [database])

    def test_prepare_with_users(self):
        self.manager.app = mock.Mock()

        user = mock.Mock()
        mock_create_users = mock.Mock()
        self.manager.create_user = mock_create_users

        self._prepare_method(users=[user])

        mock_create_users.assert_called_with(self.context, [user])

    @mock.patch.object(service.MongoDBAdmin, 'enable_root')
    def test_provide_root_password(self, mocked_enable_root):
        self.manager.app = mock.Mock()

        self._prepare_method(root_password='test_password')

        mocked_enable_root.assert_called_with('test_password')

    # This is used in the test_*_user tests below
    _serialized_user = {'_name': 'testdb.testuser', '_password': None,
                        '_roles': [{'db': 'testdb', 'role': 'testrole'}],
                        '_username': 'testuser', '_databases': [],
                        '_host': None,
                        '_database': {'_name': 'testdb',
                                      '_character_set': None,
                                      '_collate': None}}

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    def test_create_user(self, mocked_admin_user, mocked_client):
        user = self._serialized_user.copy()
        user['_password'] = 'testpassword'
        users = [user]

        client = mocked_client().__enter__()['testdb']

        self.manager.create_user(self.context, users)

        client.add_user.assert_called_with('testuser', password='testpassword',
                                           roles=[{'db': 'testdb',
                                                   'role': 'testrole'}])

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    def test_delete_user(self, mocked_admin_user, mocked_client):
        client = mocked_client().__enter__()['testdb']

        self.manager.delete_user(self.context, self._serialized_user)

        client.remove_user.assert_called_with('testuser')

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    def test_get_user(self, mocked_admin_user, mocked_client):
        mocked_find = mock.MagicMock(return_value={
            '_id': 'testdb.testuser',
            'user': 'testuser', 'db': 'testdb',
            'roles': [{'db': 'testdb', 'role': 'testrole'}]
        })
        client = mocked_client().__enter__().admin
        client.system.users.find_one = mocked_find

        result = self.manager.get_user(self.context, 'testdb.testuser', None)

        mocked_find.assert_called_with({'user': 'testuser', 'db': 'testdb'})
        self.assertEqual(self._serialized_user, result)

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    def test_list_users(self, mocked_admin_user, mocked_client):
        # roles are NOT returned by list_users
        user1 = self._serialized_user.copy()
        user2 = self._serialized_user.copy()
        user2['_name'] = 'testdb.otheruser'
        user2['_username'] = 'otheruser'
        user2['_roles'] = [{'db': 'testdb2', 'role': 'readWrite'}]
        user2['_databases'] = [{'_name': 'testdb2',
                                         '_character_set': None,
                                         '_collate': None}]

        mocked_find = mock.MagicMock(return_value=[
            {
                '_id': 'admin.os_admin',
                'user': 'os_admin', 'db': 'admin',
                'roles': [{'db': 'admin', 'role': 'root'}]
            },
            {
                '_id': 'testdb.testuser',
                'user': 'testuser', 'db': 'testdb',
                'roles': [{'db': 'testdb', 'role': 'testrole'}]
            },
            {
                '_id': 'testdb.otheruser',
                'user': 'otheruser', 'db': 'testdb',
                'roles': [{'db': 'testdb2', 'role': 'readWrite'}]
            }
        ])

        client = mocked_client().__enter__().admin
        client.system.users.find = mocked_find

        users, next_marker = self.manager.list_users(self.context)

        self.assertEqual(None, next_marker)
        self.assertEqual(sorted([user1, user2]), users)

    @mock.patch.object(service.MongoDBAdmin, 'create_user')
    @mock.patch.object(utils, 'generate_random_password',
                       return_value='password')
    def test_enable_root(self, mock_gen_rand_pwd, mock_create_user):
        root_user = {'_name': 'admin.root',
                     '_username': 'root',
                     '_database': {'_name': 'admin',
                                   '_character_set': None,
                                   '_collate': None},
                     '_password': 'password',
                     '_roles': [{'db': 'admin', 'role': 'root'}],
                     '_databases': [],
                     '_host': None}

        result = self.manager.enable_root(self.context)

        self.assertTrue(mock_create_user.called)
        self.assertEqual(root_user, result)

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    @mock.patch.object(service.MongoDBAdmin, '_get_user_record',
                       return_value=models.MongoDBUser('testdb.testuser'))
    def test_grant_access(self, mocked_get_user,
                          mocked_admin_user, mocked_client):
        client = mocked_client().__enter__()['testdb']

        self.manager.grant_access(self.context, 'testdb.testuser',
                                  None, ['db1', 'db2', 'db3'])

        client.add_user.assert_called_with('testuser', roles=[
            {'db': 'db1', 'role': 'readWrite'},
            {'db': 'db2', 'role': 'readWrite'},
            {'db': 'db3', 'role': 'readWrite'}
        ])

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    @mock.patch.object(service.MongoDBAdmin, '_get_user_record',
                       return_value=models.MongoDBUser('testdb.testuser'))
    def test_revoke_access(self, mocked_get_user,
                           mocked_admin_user, mocked_client):
        client = mocked_client().__enter__()['testdb']

        mocked_get_user.return_value.roles = [
            {'db': 'db1', 'role': 'readWrite'},
            {'db': 'db2', 'role': 'readWrite'},
            {'db': 'db3', 'role': 'readWrite'}
        ]

        self.manager.revoke_access(self.context, 'testdb.testuser',
                                   None, 'db2')

        client.add_user.assert_called_with('testuser', roles=[
            {'db': 'db1', 'role': 'readWrite'},
            {'db': 'db3', 'role': 'readWrite'}
        ])

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    @mock.patch.object(service.MongoDBAdmin, '_get_user_record',
                       return_value=models.MongoDBUser('testdb.testuser'))
    def test_list_access(self, mocked_get_user,
                         mocked_admin_user, mocked_client):
        mocked_get_user.return_value.roles = [
            {'db': 'db1', 'role': 'readWrite'},
            {'db': 'db2', 'role': 'readWrite'},
            {'db': 'db3', 'role': 'readWrite'}
        ]

        accessible_databases = self.manager.list_access(
            self.context, 'testdb.testuser', None
        )

        self.assertEqual(['db1', 'db2', 'db3'],
                         [db['_name'] for db in accessible_databases])

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    def test_create_databases(self, mocked_admin_user, mocked_client):
        schema = models.MongoDBSchema('testdb').serialize()
        db_client = mocked_client().__enter__()['testdb']

        self.manager.create_database(self.context, [schema])

        db_client['dummy'].insert.assert_called_with({'dummy': True})
        db_client.drop_collection.assert_called_with('dummy')

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    def test_list_databases(self,  # mocked_ignored_dbs,
                            mocked_admin_user, mocked_client):
        # This list contains the special 'admin', 'local' and 'config' dbs;
        # the special dbs should be skipped in the output.
        # Pagination is tested by starting at 'db1', so 'db0' should not
        # be in the output. The limit is set to 2, meaning the result
        # should be 'db1' and 'db2'. The next_marker should be 'db3'.
        mocked_list = mock.MagicMock(
            return_value=['admin', 'local', 'config',
                          'db0', 'db1', 'db2', 'db3'])
        mocked_client().__enter__().database_names = mocked_list

        marker = models.MongoDBSchema('db1').serialize()
        dbs, next_marker = self.manager.list_databases(
            self.context, limit=2, marker=marker, include_marker=True)

        mocked_list.assert_any_call()
        self.assertEqual([models.MongoDBSchema('db1').serialize(),
                          models.MongoDBSchema('db2').serialize()],
                         dbs)
        self.assertEqual(models.MongoDBSchema('db3').serialize(),
                         next_marker)

    @mock.patch.object(service, 'MongoDBClient')
    @mock.patch.object(service.MongoDBAdmin, '_admin_user')
    def test_delete_database(self, mocked_admin_user, mocked_client):
        schema = models.MongoDBSchema('testdb').serialize()

        self.manager.delete_database(self.context, schema)

        mocked_client().__enter__().drop_database.assert_called_with('testdb')
