# Copyright 2015 Yahoo Inc.
#
# 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 timeutils
import pytz

from cinder import exception
from cinder import objects
from cinder.objects import fields
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_volume
from cinder.tests.unit import objects as test_objects

fake_consistencygroup = {
    'id': fake.CONSISTENCY_GROUP_ID,
    'user_id': fake.USER_ID,
    'project_id': fake.PROJECT_ID,
    'host': 'fake_host',
    'availability_zone': 'fake_az',
    'name': 'fake_name',
    'description': 'fake_description',
    'volume_type_id': fake.VOLUME_TYPE_ID,
    'status': fields.ConsistencyGroupStatus.CREATING,
    'cgsnapshot_id': fake.CGSNAPSHOT_ID,
    'source_cgid': None,
}

fake_cgsnapshot = {
    'id': fake.CGSNAPSHOT_ID,
    'user_id': fake.USER_ID,
    'project_id': fake.PROJECT_ID,
    'name': 'fake_name',
    'description': 'fake_description',
    'status': 'creating',
    'consistencygroup_id': fake.CONSISTENCY_GROUP_ID,
}

fake_group = {
    'id': fake.GROUP_ID,
    'user_id': fake.USER_ID,
    'project_id': fake.PROJECT_ID,
    'host': 'fake_host',
    'availability_zone': 'fake_az',
    'name': 'fake_name',
    'description': 'fake_description',
    'group_type_id': fake.GROUP_TYPE_ID,
    'status': fields.GroupStatus.CREATING,
}


class TestConsistencyGroup(test_objects.BaseObjectsTestCase):

    @mock.patch('cinder.db.sqlalchemy.api.consistencygroup_get',
                return_value=fake_consistencygroup)
    def test_get_by_id(self, consistencygroup_get):
        consistencygroup = objects.ConsistencyGroup.get_by_id(
            self.context, fake.CONSISTENCY_GROUP_ID)
        self._compare(self, fake_consistencygroup, consistencygroup)
        consistencygroup_get.assert_called_once_with(
            self.context, fake.CONSISTENCY_GROUP_ID)

    @mock.patch('cinder.db.sqlalchemy.api.model_query')
    def test_get_by_id_no_existing_id(self, model_query):
        model_query().filter_by().first.return_value = None
        self.assertRaises(exception.ConsistencyGroupNotFound,
                          objects.ConsistencyGroup.get_by_id, self.context,
                          123)

    @mock.patch('cinder.db.consistencygroup_create',
                return_value=fake_consistencygroup)
    def test_create(self, consistencygroup_create):
        fake_cg = fake_consistencygroup.copy()
        del fake_cg['id']
        consistencygroup = objects.ConsistencyGroup(context=self.context,
                                                    **fake_cg)
        consistencygroup.create()
        self._compare(self, fake_consistencygroup, consistencygroup)

    @mock.patch('cinder.db.group_create',
                return_value=fake_group)
    def test_create_from_group(self, group_create):
        fake_grp = fake_group.copy()
        del fake_grp['id']
        group = objects.Group(context=self.context,
                              **fake_grp)
        group.create()
        volumes_objs = [objects.Volume(context=self.context, id=i)
                        for i in [fake.VOLUME_ID, fake.VOLUME2_ID,
                                  fake.VOLUME3_ID]]
        volumes = objects.VolumeList(objects=volumes_objs)
        group.volumes = volumes
        consistencygroup = objects.ConsistencyGroup()
        consistencygroup.from_group(group)
        self.assertEqual(group.id, consistencygroup.id)
        self.assertEqual(group.name, consistencygroup.name)

    def test_create_with_id_except_exception(self, ):
        consistencygroup = objects.ConsistencyGroup(
            context=self.context, **{'id': fake.CONSISTENCY_GROUP_ID})
        self.assertRaises(exception.ObjectActionError, consistencygroup.create)

    @mock.patch('cinder.db.consistencygroup_update')
    def test_save(self, consistencygroup_update):
        consistencygroup = objects.ConsistencyGroup._from_db_object(
            self.context, objects.ConsistencyGroup(), fake_consistencygroup)
        consistencygroup.status = fields.ConsistencyGroupStatus.AVAILABLE
        consistencygroup.save()
        consistencygroup_update.assert_called_once_with(
            self.context,
            consistencygroup.id,
            {'status': fields.ConsistencyGroupStatus.AVAILABLE})

    def test_save_with_cgsnapshots(self):
        consistencygroup = objects.ConsistencyGroup._from_db_object(
            self.context, objects.ConsistencyGroup(), fake_consistencygroup)
        cgsnapshots_objs = [objects.CGSnapshot(context=self.context, id=i)
                            for i in [fake.CGSNAPSHOT_ID, fake.CGSNAPSHOT2_ID,
                                      fake.CGSNAPSHOT3_ID]]
        cgsnapshots = objects.CGSnapshotList(objects=cgsnapshots_objs)
        consistencygroup.name = 'foobar'
        consistencygroup.cgsnapshots = cgsnapshots
        self.assertEqual({'name': 'foobar',
                          'cgsnapshots': cgsnapshots},
                         consistencygroup.obj_get_changes())
        self.assertRaises(exception.ObjectActionError, consistencygroup.save)

    def test_save_with_volumes(self):
        consistencygroup = objects.ConsistencyGroup._from_db_object(
            self.context, objects.ConsistencyGroup(), fake_consistencygroup)
        volumes_objs = [objects.Volume(context=self.context, id=i)
                        for i in [fake.VOLUME_ID, fake.VOLUME2_ID,
                                  fake.VOLUME3_ID]]
        volumes = objects.VolumeList(objects=volumes_objs)
        consistencygroup.name = 'foobar'
        consistencygroup.volumes = volumes
        self.assertEqual({'name': 'foobar',
                          'volumes': volumes},
                         consistencygroup.obj_get_changes())
        self.assertRaises(exception.ObjectActionError, consistencygroup.save)

    @mock.patch('cinder.objects.cgsnapshot.CGSnapshotList.get_all_by_group')
    @mock.patch('cinder.objects.volume.VolumeList.get_all_by_group')
    def test_obj_load_attr(self, mock_vol_get_all_by_group,
                           mock_cgsnap_get_all_by_group):
        consistencygroup = objects.ConsistencyGroup._from_db_object(
            self.context, objects.ConsistencyGroup(), fake_consistencygroup)
        # Test cgsnapshots lazy-loaded field
        cgsnapshots_objs = [objects.CGSnapshot(context=self.context, id=i)
                            for i in [fake.CGSNAPSHOT_ID, fake.CGSNAPSHOT2_ID,
                                      fake.CGSNAPSHOT3_ID]]
        cgsnapshots = objects.CGSnapshotList(context=self.context,
                                             objects=cgsnapshots_objs)
        mock_cgsnap_get_all_by_group.return_value = cgsnapshots
        self.assertEqual(cgsnapshots, consistencygroup.cgsnapshots)
        mock_cgsnap_get_all_by_group.assert_called_once_with(
            self.context, consistencygroup.id)

        # Test volumes lazy-loaded field
        volume_objs = [objects.Volume(context=self.context, id=i)
                       for i in [fake.VOLUME_ID, fake.VOLUME2_ID,
                                 fake.VOLUME3_ID]]
        volumes = objects.VolumeList(context=self.context, objects=volume_objs)
        mock_vol_get_all_by_group.return_value = volumes
        self.assertEqual(volumes, consistencygroup.volumes)
        mock_vol_get_all_by_group.assert_called_once_with(self.context,
                                                          consistencygroup.id)

    @mock.patch('oslo_utils.timeutils.utcnow', return_value=timeutils.utcnow())
    @mock.patch('cinder.db.sqlalchemy.api.consistencygroup_destroy')
    def test_destroy(self, consistencygroup_destroy, utcnow_mock):
        consistencygroup_destroy.return_value = {
            'status': fields.ConsistencyGroupStatus.DELETED,
            'deleted': True,
            'deleted_at': utcnow_mock.return_value}
        consistencygroup = objects.ConsistencyGroup(
            context=self.context, id=fake.CONSISTENCY_GROUP_ID)
        consistencygroup.destroy()
        self.assertTrue(consistencygroup_destroy.called)
        admin_context = consistencygroup_destroy.call_args[0][0]
        self.assertTrue(admin_context.is_admin)
        self.assertTrue(consistencygroup.deleted)
        self.assertEqual(fields.ConsistencyGroupStatus.DELETED,
                         consistencygroup.status)
        self.assertEqual(utcnow_mock.return_value.replace(tzinfo=pytz.UTC),
                         consistencygroup.deleted_at)

    @mock.patch('cinder.db.sqlalchemy.api.consistencygroup_get')
    def test_refresh(self, consistencygroup_get):
        db_cg1 = fake_consistencygroup.copy()
        db_cg2 = db_cg1.copy()
        db_cg2['description'] = 'foobar'

        # On the second consistencygroup_get, return the ConsistencyGroup with
        # an updated description
        consistencygroup_get.side_effect = [db_cg1, db_cg2]
        cg = objects.ConsistencyGroup.get_by_id(self.context,
                                                fake.CONSISTENCY_GROUP_ID)
        self._compare(self, db_cg1, cg)

        # description was updated, so a ConsistencyGroup refresh should have a
        # new value for that field
        cg.refresh()
        self._compare(self, db_cg2, cg)
        consistencygroup_get.assert_has_calls([
            mock.call(
                self.context,
                fake.CONSISTENCY_GROUP_ID),
            mock.call.__bool__(),
            mock.call(
                self.context,
                fake.CONSISTENCY_GROUP_ID)])

    def test_from_db_object_with_all_expected_attributes(self):
        expected_attrs = ['volumes', 'cgsnapshots']
        db_volumes = [fake_volume.fake_db_volume(admin_metadata={},
                                                 volume_metadata={})]
        db_cgsnaps = [fake_cgsnapshot.copy()]
        db_cg = fake_consistencygroup.copy()
        db_cg['volumes'] = db_volumes
        db_cg['cgsnapshots'] = db_cgsnaps
        cg = objects.ConsistencyGroup._from_db_object(
            self.context, objects.ConsistencyGroup(), db_cg, expected_attrs)
        self.assertEqual(len(db_volumes), len(cg.volumes))
        self._compare(self, db_volumes[0], cg.volumes[0])
        self.assertEqual(len(db_cgsnaps), len(cg.cgsnapshots))
        self._compare(self, db_cgsnaps[0], cg.cgsnapshots[0])


class TestConsistencyGroupList(test_objects.BaseObjectsTestCase):
    @mock.patch('cinder.db.consistencygroup_get_all',
                return_value=[fake_consistencygroup])
    def test_get_all(self, consistencygroup_get_all):
        consistencygroups = objects.ConsistencyGroupList.get_all(self.context)
        self.assertEqual(1, len(consistencygroups))
        TestConsistencyGroup._compare(self, fake_consistencygroup,
                                      consistencygroups[0])

    @mock.patch('cinder.db.consistencygroup_get_all_by_project',
                return_value=[fake_consistencygroup])
    def test_get_all_by_project(self, consistencygroup_get_all_by_project):
        consistencygroups = objects.ConsistencyGroupList.get_all_by_project(
            self.context, self.project_id)
        self.assertEqual(1, len(consistencygroups))
        TestConsistencyGroup._compare(self, fake_consistencygroup,
                                      consistencygroups[0])

    @mock.patch('cinder.db.consistencygroup_get_all',
                return_value=[fake_consistencygroup])
    def test_get_all_with_pagination(self, consistencygroup_get_all):
        consistencygroups = objects.ConsistencyGroupList.get_all(
            self.context, filters={'id': 'fake'}, marker=None, limit=1,
            offset=None, sort_keys='id', sort_dirs='asc')
        self.assertEqual(1, len(consistencygroups))
        consistencygroup_get_all.assert_called_once_with(
            self.context, filters={'id': 'fake'}, marker=None, limit=1,
            offset=None, sort_keys='id', sort_dirs='asc')
        TestConsistencyGroup._compare(self, fake_consistencygroup,
                                      consistencygroups[0])

    @mock.patch('cinder.db.consistencygroup_get_all_by_project',
                return_value=[fake_consistencygroup])
    def test_get_all_by_project_with_pagination(
            self, consistencygroup_get_all_by_project):
        consistencygroups = objects.ConsistencyGroupList.get_all_by_project(
            self.context, self.project_id, filters={'id': 'fake'}, marker=None,
            limit=1, offset=None, sort_keys='id', sort_dirs='asc')
        self.assertEqual(1, len(consistencygroups))
        consistencygroup_get_all_by_project.assert_called_once_with(
            self.context, self.project_id, filters={'id': 'fake'}, marker=None,
            limit=1, offset=None, sort_keys='id', sort_dirs='asc')
        TestConsistencyGroup._compare(self, fake_consistencygroup,
                                      consistencygroups[0])

    @mock.patch('cinder.db.consistencygroup_include_in_cluster')
    def test_include_in_cluster(self, include_mock):
        filters = {'host': mock.sentinel.host,
                   'cluster_name': mock.sentinel.cluster_name}
        cluster = 'new_cluster'
        objects.ConsistencyGroupList.include_in_cluster(self.context, cluster,
                                                        **filters)
        include_mock.assert_called_once_with(self.context, cluster, True,
                                             **filters)

    @mock.patch('cinder.db.consistencygroup_include_in_cluster')
    def test_include_in_cluster_specify_partial(self, include_mock):
        filters = {'host': mock.sentinel.host,
                   'cluster_name': mock.sentinel.cluster_name}
        cluster = 'new_cluster'
        objects.ConsistencyGroupList.include_in_cluster(
            self.context, cluster, mock.sentinel.partial_rename, **filters)
        include_mock.assert_called_once_with(
            self.context, cluster, mock.sentinel.partial_rename, **filters)
