#    Copyright 2014 IBM Corp.
#    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.

"""Unit tests for cinder.db.api."""


import datetime

from oslo.config import cfg

from cinder import context
from cinder import db
from cinder import exception
from cinder.openstack.common import uuidutils
from cinder.quota import ReservableResource
from cinder import test


CONF = cfg.CONF


def _quota_reserve(context, project_id):
    """Create sample Quota, QuotaUsage and Reservation objects.

    There is no method db.quota_usage_create(), so we have to use
    db.quota_reserve() for creating QuotaUsage objects.

    Returns reservations uuids.

    """
    def get_sync(resource, usage):
        def sync(elevated, project_id, session):
            return {resource: usage}
        return sync
    quotas = {}
    resources = {}
    deltas = {}
    for i, resource in enumerate(('volumes', 'gigabytes')):
        quotas[resource] = db.quota_create(context, project_id,
                                           resource, i + 1)
        resources[resource] = ReservableResource(resource,
                                                 '_sync_%s' % resource)
        deltas[resource] = i + 1
    return db.quota_reserve(
        context, resources, quotas, deltas,
        datetime.datetime.utcnow(), datetime.datetime.utcnow(),
        datetime.timedelta(days=1), project_id
    )


class ModelsObjectComparatorMixin(object):
    def _dict_from_object(self, obj, ignored_keys):
        if ignored_keys is None:
            ignored_keys = []
        return dict([(k, v) for k, v in obj.iteritems()
                    if k not in ignored_keys])

    def _assertEqualObjects(self, obj1, obj2, ignored_keys=None):
        obj1 = self._dict_from_object(obj1, ignored_keys)
        obj2 = self._dict_from_object(obj2, ignored_keys)

        self.assertEqual(
            len(obj1), len(obj2),
            "Keys mismatch: %s" % str(set(obj1.keys()) ^ set(obj2.keys())))
        for key, value in obj1.iteritems():
            self.assertEqual(value, obj2[key])

    def _assertEqualListsOfObjects(self, objs1, objs2, ignored_keys=None):
        obj_to_dict = lambda o: self._dict_from_object(o, ignored_keys)
        sort_key = lambda d: [d[k] for k in sorted(d)]
        conv_and_sort = lambda obj: sorted(map(obj_to_dict, obj), key=sort_key)

        self.assertEqual(conv_and_sort(objs1), conv_and_sort(objs2))

    def _assertEqualListsOfPrimitivesAsSets(self, primitives1, primitives2):
        self.assertEqual(len(primitives1), len(primitives2))
        for primitive in primitives1:
            self.assertIn(primitive, primitives2)

        for primitive in primitives2:
            self.assertIn(primitive, primitives1)


class BaseTest(test.TestCase, ModelsObjectComparatorMixin):
    def setUp(self):
        super(BaseTest, self).setUp()
        self.ctxt = context.get_admin_context()


class DBAPIServiceTestCase(BaseTest):

    """Unit tests for cinder.db.api.service_*."""

    def _get_base_values(self):
        return {
            'host': 'fake_host',
            'binary': 'fake_binary',
            'topic': 'fake_topic',
            'report_count': 3,
            'disabled': False
        }

    def _create_service(self, values):
        v = self._get_base_values()
        v.update(values)
        return db.service_create(self.ctxt, v)

    def test_service_create(self):
        service = self._create_service({})
        self.assertFalse(service['id'] is None)
        for key, value in self._get_base_values().iteritems():
            self.assertEqual(value, service[key])

    def test_service_destroy(self):
        service1 = self._create_service({})
        service2 = self._create_service({'host': 'fake_host2'})

        db.service_destroy(self.ctxt, service1['id'])
        self.assertRaises(exception.ServiceNotFound,
                          db.service_get, self.ctxt, service1['id'])
        self._assertEqualObjects(db.service_get(self.ctxt, service2['id']),
                                 service2)

    def test_service_update(self):
        service = self._create_service({})
        new_values = {
            'host': 'fake_host1',
            'binary': 'fake_binary1',
            'topic': 'fake_topic1',
            'report_count': 4,
            'disabled': True
        }
        db.service_update(self.ctxt, service['id'], new_values)
        updated_service = db.service_get(self.ctxt, service['id'])
        for key, value in new_values.iteritems():
            self.assertEqual(value, updated_service[key])

    def test_service_update_not_found_exception(self):
        self.assertRaises(exception.ServiceNotFound,
                          db.service_update, self.ctxt, 100500, {})

    def test_service_get(self):
        service1 = self._create_service({})
        service2 = self._create_service({'host': 'some_other_fake_host'})
        real_service1 = db.service_get(self.ctxt, service1['id'])
        self._assertEqualObjects(service1, real_service1)

    def test_service_get_not_found_exception(self):
        self.assertRaises(exception.ServiceNotFound,
                          db.service_get, self.ctxt, 100500)

    def test_service_get_by_host_and_topic(self):
        service1 = self._create_service({'host': 'host1', 'topic': 'topic1'})
        service2 = self._create_service({'host': 'host2', 'topic': 'topic2'})

        real_service1 = db.service_get_by_host_and_topic(self.ctxt,
                                                         host='host1',
                                                         topic='topic1')
        self._assertEqualObjects(service1, real_service1)

    def test_service_get_all(self):
        values = [
            {'host': 'host1', 'topic': 'topic1'},
            {'host': 'host2', 'topic': 'topic2'},
            {'disabled': True}
        ]
        services = [self._create_service(vals) for vals in values]
        disabled_services = [services[-1]]
        non_disabled_services = services[:-1]

        compares = [
            (services, db.service_get_all(self.ctxt)),
            (disabled_services, db.service_get_all(self.ctxt, True)),
            (non_disabled_services, db.service_get_all(self.ctxt, False))
        ]
        for comp in compares:
            self._assertEqualListsOfObjects(*comp)

    def test_service_get_all_by_topic(self):
        values = [
            {'host': 'host1', 'topic': 't1'},
            {'host': 'host2', 'topic': 't1'},
            {'disabled': True, 'topic': 't1'},
            {'host': 'host3', 'topic': 't2'}
        ]
        services = [self._create_service(vals) for vals in values]
        expected = services[:2]
        real = db.service_get_all_by_topic(self.ctxt, 't1')
        self._assertEqualListsOfObjects(expected, real)

    def test_service_get_all_by_host(self):
        values = [
            {'host': 'host1', 'topic': 't1'},
            {'host': 'host1', 'topic': 't1'},
            {'host': 'host2', 'topic': 't1'},
            {'host': 'host3', 'topic': 't2'}
        ]
        services = [self._create_service(vals) for vals in values]

        expected = services[:2]
        real = db.service_get_all_by_host(self.ctxt, 'host1')
        self._assertEqualListsOfObjects(expected, real)

    def test_service_get_by_args(self):
        values = [
            {'host': 'host1', 'binary': 'a'},
            {'host': 'host2', 'binary': 'b'}
        ]
        services = [self._create_service(vals) for vals in values]

        service1 = db.service_get_by_args(self.ctxt, 'host1', 'a')
        self._assertEqualObjects(services[0], service1)

        service2 = db.service_get_by_args(self.ctxt, 'host2', 'b')
        self._assertEqualObjects(services[1], service2)

    def test_service_get_by_args_not_found_exception(self):
        self.assertRaises(exception.HostBinaryNotFound,
                          db.service_get_by_args,
                          self.ctxt, 'non-exists-host', 'a')

    def test_service_get_all_volume_sorted(self):
        values = [
            ({'host': 'h1', 'binary': 'a', 'topic': CONF.volume_topic},
             100),
            ({'host': 'h2', 'binary': 'b', 'topic': CONF.volume_topic},
             200),
            ({'host': 'h3', 'binary': 'b', 'topic': CONF.volume_topic},
             300)]
        services = []
        for vals, size in values:
            services.append(self._create_service(vals))
            db.volume_create(self.ctxt, {'host': vals['host'], 'size': size})
        for service, size in db.service_get_all_volume_sorted(self.ctxt):
            self._assertEqualObjects(services.pop(0), service)
            self.assertEqual(values.pop(0)[1], size)


class DBAPIVolumeTestCase(BaseTest):

    """Unit tests for cinder.db.api.volume_*."""

    def test_volume_create(self):
        volume = db.volume_create(self.ctxt, {'host': 'host1'})
        self.assertTrue(uuidutils.is_uuid_like(volume['id']))
        self.assertEqual(volume.host, 'host1')

    def test_volume_allocate_iscsi_target_no_more_targets(self):
        self.assertRaises(exception.NoMoreTargets,
                          db.volume_allocate_iscsi_target,
                          self.ctxt, 42, 'host1')

    def test_volume_allocate_iscsi_target(self):
        host = 'host1'
        volume = db.volume_create(self.ctxt, {'host': host})
        db.iscsi_target_create_safe(self.ctxt, {'host': host,
                                                'target_num': 42})
        target_num = db.volume_allocate_iscsi_target(self.ctxt, volume['id'],
                                                     host)
        self.assertEqual(target_num, 42)

    def test_volume_attached_invalid_uuid(self):
        self.assertRaises(exception.InvalidUUID, db.volume_attached, self.ctxt,
                          42, 'invalid-uuid', None, '/tmp')

    def test_volume_attached_to_instance(self):
        volume = db.volume_create(self.ctxt, {'host': 'host1'})
        instance_uuid = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
        db.volume_attached(self.ctxt, volume['id'],
                           instance_uuid, None, '/tmp')
        volume = db.volume_get(self.ctxt, volume['id'])
        self.assertEqual(volume['status'], 'in-use')
        self.assertEqual(volume['mountpoint'], '/tmp')
        self.assertEqual(volume['attach_status'], 'attached')
        self.assertEqual(volume['instance_uuid'], instance_uuid)
        self.assertIsNone(volume['attached_host'])

    def test_volume_attached_to_host(self):
        volume = db.volume_create(self.ctxt, {'host': 'host1'})
        host_name = 'fake_host'
        db.volume_attached(self.ctxt, volume['id'],
                           None, host_name, '/tmp')
        volume = db.volume_get(self.ctxt, volume['id'])
        self.assertEqual(volume['status'], 'in-use')
        self.assertEqual(volume['mountpoint'], '/tmp')
        self.assertEqual(volume['attach_status'], 'attached')
        self.assertIsNone(volume['instance_uuid'])
        self.assertEqual(volume['attached_host'], host_name)

    def test_volume_data_get_for_host(self):
        for i in xrange(3):
            for j in xrange(3):
                db.volume_create(self.ctxt, {'host': 'h%d' % i, 'size': 100})
        for i in xrange(3):
            self.assertEqual((3, 300),
                             db.volume_data_get_for_host(
                                 self.ctxt, 'h%d' % i))

    def test_volume_data_get_for_project(self):
        for i in xrange(3):
            for j in xrange(3):
                db.volume_create(self.ctxt, {'project_id': 'p%d' % i,
                                             'size': 100,
                                             'host': 'h-%d-%d' % (i, j),
                                             })
        for i in xrange(3):
            self.assertEqual((3, 300),
                             db.volume_data_get_for_project(
                                 self.ctxt, 'p%d' % i))

    def test_volume_detached_from_instance(self):
        volume = db.volume_create(self.ctxt, {})
        db.volume_attached(self.ctxt, volume['id'],
                           'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
                           None, '/tmp')
        db.volume_detached(self.ctxt, volume['id'])
        volume = db.volume_get(self.ctxt, volume['id'])
        self.assertEqual('available', volume['status'])
        self.assertEqual('detached', volume['attach_status'])
        self.assertIsNone(volume['mountpoint'])
        self.assertIsNone(volume['instance_uuid'])
        self.assertIsNone(volume['attached_host'])

    def test_volume_detached_from_host(self):
        volume = db.volume_create(self.ctxt, {})
        db.volume_attached(self.ctxt, volume['id'],
                           None, 'fake_host', '/tmp')
        db.volume_detached(self.ctxt, volume['id'])
        volume = db.volume_get(self.ctxt, volume['id'])
        self.assertEqual('available', volume['status'])
        self.assertEqual('detached', volume['attach_status'])
        self.assertIsNone(volume['mountpoint'])
        self.assertIsNone(volume['instance_uuid'])
        self.assertIsNone(volume['attached_host'])

    def test_volume_get(self):
        volume = db.volume_create(self.ctxt, {})
        self._assertEqualObjects(volume, db.volume_get(self.ctxt,
                                                       volume['id']))

    def test_volume_destroy(self):
        volume = db.volume_create(self.ctxt, {})
        db.volume_destroy(self.ctxt, volume['id'])
        self.assertRaises(exception.VolumeNotFound, db.volume_get,
                          self.ctxt, volume['id'])

    def test_volume_get_all(self):
        volumes = [db.volume_create(self.ctxt,
                   {'host': 'h%d' % i, 'size': i})
                   for i in xrange(3)]
        self._assertEqualListsOfObjects(volumes, db.volume_get_all(
                                        self.ctxt, None, None, 'host', None))

    def test_volume_get_all_marker_passed(self):
        volumes = [
            db.volume_create(self.ctxt, {'id': 1}),
            db.volume_create(self.ctxt, {'id': 2}),
            db.volume_create(self.ctxt, {'id': 3}),
            db.volume_create(self.ctxt, {'id': 4}),
        ]

        self._assertEqualListsOfObjects(volumes[2:], db.volume_get_all(
                                        self.ctxt, 2, 2, 'id', None))

    def test_volume_get_all_by_host(self):
        volumes = []
        for i in xrange(3):
            volumes.append([db.volume_create(self.ctxt, {'host': 'h%d' % i})
                            for j in xrange(3)])
        for i in xrange(3):
            self._assertEqualListsOfObjects(volumes[i],
                                            db.volume_get_all_by_host(
                                            self.ctxt, 'h%d' % i))

    def test_volume_get_all_by_instance_uuid(self):
        instance_uuids = []
        volumes = []
        for i in xrange(3):
            instance_uuid = str(uuidutils.uuid.uuid1())
            instance_uuids.append(instance_uuid)
            volumes.append([db.volume_create(self.ctxt,
                            {'instance_uuid': instance_uuid})
                            for j in xrange(3)])
        for i in xrange(3):
            self._assertEqualListsOfObjects(volumes[i],
                                            db.volume_get_all_by_instance_uuid(
                                            self.ctxt, instance_uuids[i]))

    def test_volume_get_all_by_instance_uuid_empty(self):
        self.assertEqual([], db.volume_get_all_by_instance_uuid(self.ctxt,
                                                                'empty'))

    def test_volume_get_all_by_project(self):
        volumes = []
        for i in xrange(3):
            volumes.append([db.volume_create(self.ctxt, {
                'project_id': 'p%d' % i}) for j in xrange(3)])
        for i in xrange(3):
            self._assertEqualListsOfObjects(volumes[i],
                                            db.volume_get_all_by_project(
                                            self.ctxt, 'p%d' % i, None,
                                            None, 'host', None))

    def test_volume_get_by_name(self):
        db.volume_create(self.ctxt, {'display_name': 'vol1'})
        db.volume_create(self.ctxt, {'display_name': 'vol2'})
        db.volume_create(self.ctxt, {'display_name': 'vol3'})

        # no name filter
        volumes = db.volume_get_all(self.ctxt, None, None, 'created_at',
                                    'asc')
        self.assertEqual(len(volumes), 3)
        # filter on name
        volumes = db.volume_get_all(self.ctxt, None, None, 'created_at',
                                    'asc', {'display_name': 'vol2'})
        self.assertEqual(len(volumes), 1)
        self.assertEqual(volumes[0]['display_name'], 'vol2')
        # filter no match
        volumes = db.volume_get_all(self.ctxt, None, None, 'created_at',
                                    'asc', {'display_name': 'vol4'})
        self.assertEqual(len(volumes), 0)

    def test_volume_list_by_status(self):
        db.volume_create(self.ctxt, {'display_name': 'vol1',
                                     'status': 'available'})
        db.volume_create(self.ctxt, {'display_name': 'vol2',
                                     'status': 'available'})
        db.volume_create(self.ctxt, {'display_name': 'vol3',
                                     'status': 'in-use'})

        # no status filter
        volumes = db.volume_get_all(self.ctxt, None, None, 'created_at',
                                    'asc')
        self.assertEqual(len(volumes), 3)
        # single match
        volumes = db.volume_get_all(self.ctxt, None, None, 'created_at',
                                    'asc', {'status': 'in-use'})
        self.assertEqual(len(volumes), 1)
        self.assertEqual(volumes[0]['status'], 'in-use')
        # multiple match
        volumes = db.volume_get_all(self.ctxt, None, None, 'created_at',
                                    'asc', {'status': 'available'})
        self.assertEqual(len(volumes), 2)
        for volume in volumes:
            self.assertEqual(volume['status'], 'available')
        # multiple filters
        volumes = db.volume_get_all(self.ctxt, None, None, 'created_at',
                                    'asc', {'status': 'available',
                                            'display_name': 'vol1'})
        self.assertEqual(len(volumes), 1)
        self.assertEqual(volumes[0]['display_name'], 'vol1')
        self.assertEqual(volumes[0]['status'], 'available')
        # no match
        volumes = db.volume_get_all(self.ctxt, None, None, 'created_at',
                                    'asc', {'status': 'in-use',
                                            'display_name': 'vol1'})
        self.assertEqual(len(volumes), 0)

    def _assertEqualsVolumeOrderResult(self, correct_order, limit=None,
                                       sort_key='created_at', sort_dir='asc',
                                       filters=None, project_id=None,
                                       match_keys=['id', 'display_name',