#    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.

from unittest import mock

from oslo_utils import uuidutils
from sqlalchemy.orm import exc as orm_exc

from nova import exception
from nova import objects
from nova.objects import instance_mapping
from nova.tests.unit.objects import test_cell_mapping
from nova.tests.unit.objects import test_objects


def get_db_mapping(**updates):
    db_mapping = {
            'id': 1,
            'instance_uuid': uuidutils.generate_uuid(),
            'cell_id': None,
            'project_id': 'fake-project',
            'user_id': 'fake-user',
            'created_at': None,
            'updated_at': None,
            'queued_for_delete': False,
            }
    db_mapping["cell_mapping"] = test_cell_mapping.get_db_mapping(id=42)
    db_mapping['cell_id'] = db_mapping["cell_mapping"]["id"]
    db_mapping.update(updates)
    return db_mapping


class _TestInstanceMappingObject(object):
    def _check_cell_map_value(self, db_val, cell_obj):
        self.assertEqual(db_val, cell_obj.id)

    @mock.patch.object(instance_mapping.InstanceMapping,
            '_get_by_instance_uuid_from_db')
    def test_get_by_instance_uuid(self, uuid_from_db):
        db_mapping = get_db_mapping()
        uuid_from_db.return_value = db_mapping

        mapping_obj = objects.InstanceMapping().get_by_instance_uuid(
                self.context, db_mapping['instance_uuid'])
        uuid_from_db.assert_called_once_with(self.context,
                db_mapping['instance_uuid'])
        self.compare_obj(mapping_obj, db_mapping,
                         subs={'cell_mapping': 'cell_id'},
                         comparators={
                             'cell_mapping': self._check_cell_map_value})

    @mock.patch.object(instance_mapping.InstanceMapping,
            '_get_by_instance_uuid_from_db')
    def test_get_by_instance_uuid_cell_mapping_none(self, uuid_from_db):
        db_mapping = get_db_mapping(cell_mapping=None, cell_id=None)
        uuid_from_db.return_value = db_mapping

        mapping_obj = objects.InstanceMapping().get_by_instance_uuid(
                self.context, db_mapping['instance_uuid'])
        uuid_from_db.assert_called_once_with(self.context,
                db_mapping['instance_uuid'])
        self.compare_obj(mapping_obj, db_mapping,
                         subs={'cell_mapping': 'cell_id'})

    @mock.patch.object(instance_mapping.InstanceMapping, '_create_in_db')
    def test_create(self, create_in_db):
        db_mapping = get_db_mapping()
        uuid = db_mapping['instance_uuid']
        create_in_db.return_value = db_mapping
        mapping_obj = objects.InstanceMapping(self.context)
        mapping_obj.instance_uuid = uuid
        mapping_obj.cell_mapping = objects.CellMapping(self.context,
                id=db_mapping['cell_mapping']['id'])
        mapping_obj.project_id = db_mapping['project_id']
        mapping_obj.user_id = db_mapping['user_id']

        mapping_obj.create()
        create_in_db.assert_called_once_with(self.context,
                {'instance_uuid': uuid,
                 'queued_for_delete': False,
                 'cell_id': db_mapping['cell_mapping']['id'],
                 'project_id': db_mapping['project_id'],
                 'user_id': db_mapping['user_id']})
        self.compare_obj(mapping_obj, db_mapping,
                         subs={'cell_mapping': 'cell_id'},
                         comparators={
                             'cell_mapping': self._check_cell_map_value})

    @mock.patch.object(instance_mapping.InstanceMapping, '_create_in_db')
    def test_create_cell_mapping_none(self, create_in_db):
        db_mapping = get_db_mapping(cell_mapping=None, cell_id=None)
        uuid = db_mapping['instance_uuid']
        create_in_db.return_value = db_mapping
        mapping_obj = objects.InstanceMapping(self.context)
        mapping_obj.instance_uuid = uuid
        mapping_obj.cell_mapping = None
        mapping_obj.project_id = db_mapping['project_id']
        mapping_obj.user_id = db_mapping['user_id']

        mapping_obj.create()
        create_in_db.assert_called_once_with(self.context,
                {'instance_uuid': uuid,
                 'queued_for_delete': False,
                 'project_id': db_mapping['project_id'],
                 'user_id': db_mapping['user_id']})
        self.compare_obj(mapping_obj, db_mapping,
                         subs={'cell_mapping': 'cell_id'})
        self.assertIsNone(mapping_obj.cell_mapping)

    @mock.patch.object(instance_mapping.InstanceMapping, '_create_in_db')
    def test_create_cell_mapping_with_qfd_true(self, create_in_db):
        db_mapping = get_db_mapping(cell_mapping=None, cell_id=None)
        create_in_db.return_value = db_mapping
        mapping_obj = objects.InstanceMapping(self.context)
        mapping_obj.instance_uuid = db_mapping['instance_uuid']
        mapping_obj.cell_mapping = None
        mapping_obj.project_id = db_mapping['project_id']
        mapping_obj.user_id = db_mapping['user_id']
        mapping_obj.queued_for_delete = True

        mapping_obj.create()
        create_in_db.assert_called_once_with(self.context,
                {'instance_uuid': db_mapping['instance_uuid'],
                 'queued_for_delete': True,
                 'project_id': db_mapping['project_id'],
                 'user_id': db_mapping['user_id']})

    @mock.patch.object(instance_mapping.InstanceMapping, '_save_in_db')
    def test_save(self, save_in_db):
        db_mapping = get_db_mapping()
        uuid = db_mapping['instance_uuid']
        save_in_db.return_value = db_mapping
        mapping_obj = objects.InstanceMapping(self.context)
        mapping_obj.instance_uuid = uuid
        mapping_obj.cell_mapping = objects.CellMapping(self.context, id=42)

        mapping_obj.save()
        save_in_db.assert_called_once_with(self.context,
                db_mapping['instance_uuid'],
                {'cell_id': mapping_obj.cell_mapping.id,
                 'instance_uuid': uuid})
        self.compare_obj(mapping_obj, db_mapping,
                         subs={'cell_mapping': 'cell_id'},
                         comparators={
                             'cell_mapping': self._check_cell_map_value})

    @mock.patch.object(instance_mapping.InstanceMapping, '_save_in_db')
    def test_save_stale_data_error(self, save_in_db):
        save_in_db.side_effect = orm_exc.StaleDataError
        mapping_obj = objects.InstanceMapping(self.context)
        mapping_obj.instance_uuid = uuidutils.generate_uuid()

        self.assertRaises(exception.InstanceMappingNotFound, mapping_obj.save)

    @mock.patch.object(instance_mapping.InstanceMapping, '_destroy_in_db')
    def test_destroy(self, destroy_in_db):
        uuid = uuidutils.generate_uuid()
        mapping_obj = objects.InstanceMapping(self.context)
        mapping_obj.instance_uuid = uuid

        mapping_obj.destroy()
        destroy_in_db.assert_called_once_with(self.context, uuid)

    def test_cell_mapping_nullable(self):
        mapping_obj = objects.InstanceMapping(self.context)
        # Just ensure this doesn't raise an exception
        mapping_obj.cell_mapping = None

    def test_obj_make_compatible(self):
        uuid = uuidutils.generate_uuid()
        im_obj = instance_mapping.InstanceMapping(context=self.context)
        fake_im_obj = instance_mapping.InstanceMapping(context=self.context,
                                                       instance_uuid=uuid,
                                                       queued_for_delete=False,
                                                       user_id='fake-user')
        obj_primitive = fake_im_obj.obj_to_primitive('1.1')
        obj = im_obj.obj_from_primitive(obj_primitive)
        self.assertIn('queued_for_delete', obj)
        self.assertNotIn('user_id', obj)

        obj_primitive = fake_im_obj.obj_to_primitive('1.0')
        obj = im_obj.obj_from_primitive(obj_primitive)
        self.assertIn('instance_uuid', obj)
        self.assertEqual(uuid, obj.instance_uuid)
        self.assertNotIn('queued_for_delete', obj)

    @mock.patch('nova.objects.instance_mapping.LOG.error')
    def test_obj_load_attr(self, mock_log):
        im_obj = instance_mapping.InstanceMapping()
        # Access of unset user_id should have special handling
        self.assertRaises(exception.ObjectActionError, im_obj.obj_load_attr,
                          'user_id')
        msg = ('The unset user_id attribute of an unmigrated instance mapping '
               'should not be accessed.')
        mock_log.assert_called_once_with(msg)
        # Access of any other unset attribute should fall back to base class
        self.assertRaises(NotImplementedError, im_obj.obj_load_attr,
                          'project_id')


class TestInstanceMappingObject(test_objects._LocalTest,
                                _TestInstanceMappingObject):
    pass


class TestRemoteInstanceMappingObject(test_objects._RemoteTest,
                                      _TestInstanceMappingObject):
    pass


class _TestInstanceMappingListObject(object):
    def _check_cell_map_value(self, db_val, cell_obj):
        self.assertEqual(db_val, cell_obj.id)

    @mock.patch.object(instance_mapping.InstanceMappingList,
            '_get_by_project_id_from_db')
    def test_get_by_project_id(self, project_id_from_db):
        db_mapping = get_db_mapping()
        project_id_from_db.return_value = [db_mapping]

        mapping_obj = objects.InstanceMappingList().get_by_project_id(
                self.context, db_mapping['project_id'])
        project_id_from_db.assert_called_once_with(self.context,
                db_mapping['project_id'])
        self.compare_obj(mapping_obj.objects[0], db_mapping,
                         subs={'cell_mapping': 'cell_id'},
                         comparators={
                             'cell_mapping': self._check_cell_map_value})

    @mock.patch.object(instance_mapping.InstanceMappingList,
                                        '_destroy_bulk_in_db')
    def test_destroy_bulk(self, destroy_bulk_in_db):
        uuids_to_be_deleted = []
        for i in range(0, 5):
            uuid = uuidutils.generate_uuid()
            uuids_to_be_deleted.append(uuid)
        destroy_bulk_in_db.return_value = 5
        result = objects.InstanceMappingList.destroy_bulk(self.context,
                                            uuids_to_be_deleted)
        destroy_bulk_in_db.assert_called_once_with(self.context,
                                            uuids_to_be_deleted)
        self.assertEqual(5, result)


class TestInstanceMappingListObject(test_objects._LocalTest,
                                    _TestInstanceMappingListObject):
    pass


class TestRemoteInstanceMappingListObject(test_objects._RemoteTest,
                                          _TestInstanceMappingListObject):
    pass
