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

from unittest import mock

from oslo_utils.fixture import uuidsentinel
from oslo_utils import timeutils

from nova import exception
from nova.objects import aggregate
from nova import test
from nova.tests.unit.objects import test_objects


NOW = timeutils.utcnow().replace(microsecond=0)
fake_aggregate = {
    'deleted': 0,
    'deleted_at': None,
    'created_at': NOW,
    'updated_at': None,
    'id': 123,
    'uuid': uuidsentinel.fake_aggregate,
    'name': 'fake-aggregate',
    'hosts': ['foo', 'bar'],
    'metadetails': {'this': 'that'},
    }

SUBS = {'metadata': 'metadetails'}


class _TestAggregateObject(object):

    @mock.patch('nova.objects.aggregate._aggregate_get_from_db')
    def test_get_by_id_from_api(self, mock_get_api):
        mock_get_api.return_value = fake_aggregate

        agg = aggregate.Aggregate.get_by_id(self.context, 123)
        self.compare_obj(agg, fake_aggregate, subs=SUBS)

        mock_get_api.assert_called_once_with(self.context, 123)

    @mock.patch('nova.objects.aggregate._aggregate_get_from_db_by_uuid')
    def test_get_by_uuid_from_api(self, get_by_uuid_api):
        get_by_uuid_api.return_value = fake_aggregate
        agg = aggregate.Aggregate.get_by_uuid(self.context,
                                              uuidsentinel.fake_aggregate)
        self.assertEqual(uuidsentinel.fake_aggregate, agg.uuid)
        self.assertEqual(fake_aggregate['id'], agg.id)

    @mock.patch('nova.objects.aggregate._aggregate_create_in_db')
    def test_create(self, api_create_mock):
        api_create_mock.return_value = fake_aggregate
        agg = aggregate.Aggregate(context=self.context)
        agg.name = 'foo'
        agg.metadata = {'one': 'two'}
        agg.uuid = uuidsentinel.fake_agg
        agg.create()
        api_create_mock.assert_called_once_with(
                self.context,
                {'name': 'foo', 'uuid': uuidsentinel.fake_agg},
                metadata={'one': 'two'})
        self.compare_obj(agg, fake_aggregate, subs=SUBS)

        api_create_mock.assert_called_once_with(self.context,
            {'name': 'foo', 'uuid': uuidsentinel.fake_agg},
            metadata={'one': 'two'})

    @mock.patch('nova.objects.aggregate._aggregate_create_in_db')
    def test_recreate_fails(self, api_create_mock):
        api_create_mock.return_value = fake_aggregate
        agg = aggregate.Aggregate(context=self.context)
        agg.name = 'foo'
        agg.metadata = {'one': 'two'}
        agg.uuid = uuidsentinel.fake_agg
        agg.create()
        self.assertRaises(exception.ObjectActionError, agg.create)

        api_create_mock.assert_called_once_with(self.context,
            {'name': 'foo', 'uuid': uuidsentinel.fake_agg},
            metadata={'one': 'two'})

    @mock.patch('nova.objects.aggregate._aggregate_delete_from_db')
    def test_destroy(self, api_delete_mock):
        agg = aggregate.Aggregate(context=self.context)
        agg.id = 123
        agg.destroy()
        api_delete_mock.assert_called_with(self.context, 123)

    @mock.patch('nova.compute.utils.notify_about_aggregate_action')
    @mock.patch('nova.objects.aggregate._aggregate_update_to_db')
    def test_save_to_api(self, api_update_mock, mock_notify):
        api_update_mock.return_value = fake_aggregate
        agg = aggregate.Aggregate(context=self.context)
        agg.id = 123
        agg.name = 'fake-api-aggregate'
        agg.save()
        self.compare_obj(agg, fake_aggregate, subs=SUBS)
        api_update_mock.assert_called_once_with(self.context,
                                                123,
                                                {'name': 'fake-api-aggregate'})

        api_update_mock.assert_called_once_with(self.context,
            123, {'name': 'fake-api-aggregate'})

        mock_notify.assert_has_calls([
            mock.call(context=self.context,
                      aggregate=test.MatchType(aggregate.Aggregate),
                      action='update_prop', phase='start'),
            mock.call(context=self.context,
                      aggregate=test.MatchType(aggregate.Aggregate),
                      action='update_prop', phase='end')])
        self.assertEqual(2, mock_notify.call_count)

    def test_save_and_create_no_hosts(self):
        agg = aggregate.Aggregate(context=self.context)
        agg.id = 123
        agg.hosts = ['foo', 'bar']
        self.assertRaises(exception.ObjectActionError,
                          agg.create)
        self.assertRaises(exception.ObjectActionError,
                          agg.save)

    @mock.patch('nova.objects.aggregate._metadata_delete_from_db')
    @mock.patch('nova.objects.aggregate._metadata_add_to_db')
    @mock.patch('nova.compute.utils.notify_about_aggregate_action')
    @mock.patch('oslo_versionedobjects.base.VersionedObject.'
                'obj_from_primitive')
    def test_update_metadata_api(self,
                                 mock_obj_from_primitive,
                                 mock_notify,
                                 mock_api_metadata_add,
                                 mock_api_metadata_delete):
        agg = aggregate.Aggregate()
        agg._context = self.context
        agg.id = 123
        agg.metadata = {'foo': 'bar'}
        agg.obj_reset_changes()
        mock_obj_from_primitive.return_value = agg

        agg.update_metadata({'todelete': None, 'toadd': 'myval'})
        self.assertEqual(2, len(self.notifier.notifications))
        msg = self.notifier.notifications[0]
        self.assertEqual('aggregate.updatemetadata.start', msg.event_type)
        self.assertEqual({'todelete': None, 'toadd': 'myval'},
                         msg.payload['meta_data'])
        msg = self.notifier.notifications[1]
        self.assertEqual('aggregate.updatemetadata.end', msg.event_type)
        mock_notify.assert_has_calls([
            mock.call(context=self.context, aggregate=agg,
                      action='update_metadata', phase='start'),
            mock.call(context=self.context, aggregate=agg,
                      action='update_metadata', phase='end')])
        self.assertEqual({'todelete': None, 'toadd': 'myval'},
                         msg.payload['meta_data'])
        self.assertEqual({'foo': 'bar', 'toadd': 'myval'}, agg.metadata)
        mock_api_metadata_delete.assert_called_once_with(self.context, 123,
                                                         'todelete')
        mock_api_metadata_add.assert_called_once_with(self.context, 123,
                                                      {'toadd': 'myval'})
        mock_api_metadata_delete.assert_called_once_with(self.context,
                                                         123,
                                                         'todelete')
        mock_api_metadata_add.assert_called_once_with(self.context,
                                                      123,
                                                      {'toadd': 'myval'})

    @mock.patch('nova.compute.utils.notify_about_aggregate_action')
    def test_update_metadata_api_case_sensitive(self, mock_notify):
        # Mock context.session.query().filter_by().filter().all()
        fake_context = mock.Mock()
        mock_query = mock.Mock()
        fake_context.session.query.return_value = mock_query
        mock_query = mock_query.filter_by.return_value.filter.return_value

        # Return a fake meta_ref that raises a KeyError
        fake_meta_ref = mock.Mock()
        fake_meta_ref.update.side_effect = KeyError
        mock_query.all.return_value = [fake_meta_ref]

        # Create an aggregate in the database
        agg = aggregate.Aggregate(
            context=self.context, name='agg', metadata={'Abc': 'bar'})
        agg.create()

        # Then replace its context with mock context
        agg._context = fake_context
        self.assertRaises(exception.AggregateMetadataKeyExists,
                          agg.update_metadata,
                          {'abC': 'barbar'})

    @mock.patch('nova.objects.aggregate._host_add_to_db')
    def test_add_host_api(self, mock_host_add_api):
        mock_host_add_api.return_value = {'host': 'bar'}
        agg = aggregate.Aggregate()
        agg.id = 123
        agg.hosts = ['foo']
        agg._context = self.context
        agg.add_host('bar')
        self.assertEqual(agg.hosts, ['foo', 'bar'])
        mock_host_add_api.assert_called_once_with(self.context, 123, 'bar')

    @mock.patch('nova.objects.aggregate._host_delete_from_db')
    def test_delete_host_api(self, mock_host_delete_api):
        agg = aggregate.Aggregate()
        agg.id = 123
        agg.hosts = ['foo', 'bar']
        agg._context = self.context
        agg.delete_host('foo')
        self.assertEqual(agg.hosts, ['bar'])
        mock_host_delete_api.assert_called_once_with(self.context, 123, 'foo')

    def test_availability_zone(self):
        agg = aggregate.Aggregate()
        agg.metadata = {'availability_zone': 'foo'}
        self.assertEqual('foo', agg.availability_zone)

    @mock.patch('nova.objects.aggregate._get_all_from_db')
    def test_get_all(self, mock_api_get_all):
        mock_api_get_all.return_value = [fake_aggregate]
        aggs = aggregate.AggregateList.get_all(self.context)
        self.assertEqual(1, len(aggs))
        self.compare_obj(aggs[0], fake_aggregate, subs=SUBS)

    @mock.patch('nova.objects.aggregate._get_by_host_from_db')
    def test_by_host(self, mock_api_get_by_host):
        mock_api_get_by_host.return_value = [fake_aggregate]
        aggs = aggregate.AggregateList.get_by_host(self.context, 'fake-host')
        self.assertEqual(1, len(aggs))
        self.compare_obj(aggs[0], fake_aggregate, subs=SUBS)

    @mock.patch('nova.objects.aggregate._get_by_metadata_from_db')
    def test_get_by_metadata_key(self, mock_api_get_by_metadata_key):
        mock_api_get_by_metadata_key.return_value = [fake_aggregate]
        aggs = aggregate.AggregateList.get_by_metadata_key(
            self.context, 'this')
        self.assertEqual(1, len(aggs))
        self.compare_obj(aggs[0], fake_aggregate, subs=SUBS)

    @mock.patch('nova.objects.aggregate._get_by_metadata_from_db')
    def test_get_by_metadata_key_and_hosts_no_match(self, get_by_metadata_key):
        get_by_metadata_key.return_value = [fake_aggregate]
        aggs = aggregate.AggregateList.get_by_metadata_key(
            self.context, 'this', hosts=['baz'])
        self.assertEqual(0, len(aggs))

    @mock.patch('nova.objects.aggregate._get_by_metadata_from_db')
    def test_get_by_metadata_key_and_hosts_match(self, get_by_metadata_key):
        get_by_metadata_key.return_value = [fake_aggregate]
        aggs = aggregate.AggregateList.get_by_metadata_key(
            self.context, 'this', hosts=['foo', 'bar'])
        self.assertEqual(1, len(aggs))
        self.compare_obj(aggs[0], fake_aggregate, subs=SUBS)

    @mock.patch('nova.objects.aggregate.'
                '_get_non_matching_by_metadata_keys_from_db')
    def test_get_non_matching_by_metadata_keys(
            self, get_non_matching_by_metadata_keys):
        get_non_matching_by_metadata_keys.return_value = [fake_aggregate]
        aggs = aggregate.AggregateList.get_non_matching_by_metadata_keys(
            self.context, ['abc'], 'th', value='that')
        self.assertEqual('that', aggs[0].metadata['this'])

    @mock.patch('nova.objects.aggregate.'
                '_get_non_matching_by_metadata_keys_from_db')
    def test_get_non_matching_by_metadata_keys_and_hosts_no_match(
            self, get_non_matching_by_metadata_keys):
        get_non_matching_by_metadata_keys.return_value = []
        aggs = aggregate.AggregateList.get_non_matching_by_metadata_keys(
            self.context, ['this'], 'th', value='that')
        self.assertEqual(0, len(aggs))


class TestAggregateObject(test_objects._LocalTest,
                          _TestAggregateObject):
    pass


class TestRemoteAggregateObject(test_objects._RemoteTest,
                                _TestAggregateObject):
    pass
