#    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 copy import deepcopy
from unittest import mock

from oslo_db import exception as db_exc
from oslo_utils.fixture import uuidsentinel
from oslo_utils import timeutils

from nova import context
from nova.db.api import api as api_db_api
from nova.db.api import models as api_models
from nova import exception
import nova.objects.aggregate as aggregate_obj
from nova import test
from nova.tests.unit import matchers
from nova.tests.unit.objects.test_objects import compare_obj as base_compare


SUBS = {'metadata': 'metadetails'}


NOW = timeutils.utcnow().replace(microsecond=0)


def _get_fake_aggregate(db_id, in_api=True, result=True):
    agg_map = {
        'created_at': NOW,
        'updated_at': None,
        'deleted_at': None,
        'id': db_id,
        'uuid': getattr(uuidsentinel, str(db_id)),
        'name': 'name_' + str(db_id),
    }
    if not in_api:
        agg_map['deleted'] = False
    if result:
        agg_map['hosts'] = _get_fake_hosts(db_id)
        agg_map['metadetails'] = _get_fake_metadata(db_id)
    return agg_map


def _get_fake_hosts(db_id):
    return ['constant_host', 'unique_host_' + str(db_id)]


def _get_fake_metadata(db_id):
    return {'constant_key': 'constant_value',
            'unique_key': 'unique_value_' + str(db_id)}


@api_db_api.context_manager.writer
def _create_aggregate(context, values=_get_fake_aggregate(1, result=False),
                               metadata=_get_fake_metadata(1)):
    aggregate = api_models.Aggregate()
    aggregate.update(values)
    aggregate.save(context.session)

    if metadata:
        for key, value in metadata.items():
            aggregate_metadata = api_models.AggregateMetadata()
            aggregate_metadata.update({'key': key,
                                       'value': value,
                                       'aggregate_id': aggregate['id']})
            aggregate_metadata.save(context.session)

    return aggregate


@api_db_api.context_manager.writer
def _create_aggregate_with_hosts(context,
                                 values=_get_fake_aggregate(1, result=False),
                                 metadata=_get_fake_metadata(1),
                                 hosts=_get_fake_hosts(1)):
    aggregate = _create_aggregate(context, values, metadata)
    for host in hosts:
        host_model = api_models.AggregateHost()
        host_model.update({'host': host,
                           'aggregate_id': aggregate.id})
        host_model.save(context.session)

    return aggregate


@api_db_api.context_manager.reader
def _aggregate_host_get_all(context, aggregate_id):
    return context.session.query(api_models.AggregateHost).\
                       filter_by(aggregate_id=aggregate_id).all()


@api_db_api.context_manager.reader
def _aggregate_metadata_get_all(context, aggregate_id):
    results = context.session.query(api_models.AggregateMetadata).\
                                filter_by(aggregate_id=aggregate_id).all()
    metadata = {}
    for r in results:
        metadata[r['key']] = r['value']
    return metadata


class AggregateObjectDbTestCase(test.TestCase):

    def setUp(self):
        super(AggregateObjectDbTestCase, self).setUp()
        self.context = context.RequestContext('fake-user', 'fake-project')

    def test_aggregate_get_from_db(self):
        result = _create_aggregate_with_hosts(self.context)
        expected = aggregate_obj._aggregate_get_from_db(self.context,
                                                        result['id'])
        self.assertEqual(_get_fake_hosts(1), expected.hosts)
        self.assertEqual(_get_fake_metadata(1), expected['metadetails'])

    def test_aggregate_get_from_db_by_uuid(self):
        result = _create_aggregate_with_hosts(self.context)
        expected = aggregate_obj._aggregate_get_from_db_by_uuid(
                self.context, result['uuid'])
        self.assertEqual(result.uuid, expected.uuid)
        self.assertEqual(_get_fake_hosts(1), expected.hosts)
        self.assertEqual(_get_fake_metadata(1), expected['metadetails'])

    def test_aggregate_get_from_db_raise_not_found(self):
        aggregate_id = 5
        self.assertRaises(exception.AggregateNotFound,
                          aggregate_obj._aggregate_get_from_db,
                          self.context, aggregate_id)

    def test_aggregate_get_all_from_db(self):
        for c in range(3):
            _create_aggregate(self.context,
                              values={'name': 'fake_aggregate_%d' % c})
        results = aggregate_obj._get_all_from_db(self.context)
        self.assertEqual(len(results), 3)

    def test_aggregate_get_by_host_from_db(self):
        _create_aggregate_with_hosts(self.context,
                                     values={'name': 'fake_aggregate_1'},
                                     hosts=['host.1.openstack.org'])
        _create_aggregate_with_hosts(self.context,
                                     values={'name': 'fake_aggregate_2'},
                                     hosts=['host.1.openstack.org'])
        _create_aggregate(self.context,
                          values={'name': 'no_host_aggregate'})
        rh1 = aggregate_obj._get_all_from_db(self.context)
        rh2 = aggregate_obj._get_by_host_from_db(self.context,
                                             'host.1.openstack.org')
        self.assertEqual(3, len(rh1))
        self.assertEqual(2, len(rh2))

    def test_aggregate_get_by_host_with_key_from_db(self):
        ah1 = _create_aggregate_with_hosts(self.context,
                                           values={'name': 'fake_aggregate_1'},
                                           metadata={'goodkey': 'good'},
                                           hosts=['host.1.openstack.org'])
        _create_aggregate_with_hosts(self.context,
                                     values={'name': 'fake_aggregate_2'},
                                     hosts=['host.1.openstack.org'])
        rh1 = aggregate_obj._get_by_host_from_db(self.context,
                                             'host.1.openstack.org',
                                             key='goodkey')
        self.assertEqual(1, len(rh1))
        self.assertEqual(ah1['id'], rh1[0]['id'])

    def test_aggregate_get_by_metadata_key_from_db(self):
        _create_aggregate(self.context,
                          values={'name': 'aggregate_1'},
                          metadata={'goodkey': 'good'})
        _create_aggregate(self.context,
                          values={'name': 'aggregate_2'},
                          metadata={'goodkey': 'bad'})
        _create_aggregate(self.context,
                          values={'name': 'aggregate_3'},
                          metadata={'badkey': 'good'})
        rl1 = aggregate_obj._get_by_metadata_from_db(self.context,
                                                     key='goodkey')
        self.assertEqual(2, len(rl1))

    def test_aggregate_create_in_db(self):
        fake_create_aggregate = {
            'name': 'fake-aggregate',
        }
        agg = aggregate_obj._aggregate_create_in_db(self.context,
                                                    fake_create_aggregate)
        result = aggregate_obj._aggregate_get_from_db(self.context,
                                                      agg.id)
        self.assertEqual(result.name, fake_create_aggregate['name'])

    def test_aggregate_create_in_db_with_metadata(self):
        fake_create_aggregate = {
            'name': 'fake-aggregate',
        }
        agg = aggregate_obj._aggregate_create_in_db(self.context,
                                                fake_create_aggregate,
                                                metadata={'goodkey': 'good'})
        result = aggregate_obj._aggregate_get_from_db(self.context,
                                                      agg.id)
        md = aggregate_obj._get_by_metadata_from_db(self.context,
                                                    key='goodkey')
        self.assertEqual(len(md), 1)
        self.assertEqual(md[0]['id'], agg.id)
        self.assertEqual(result.name, fake_create_aggregate['name'])

    def test_aggregate_create_raise_exist_exc(self):
        fake_create_aggregate = {
            'name': 'fake-aggregate',
        }
        aggregate_obj._aggregate_create_in_db(self.context,
                                              fake_create_aggregate)
        self.assertRaises(exception.AggregateNameExists,
                          aggregate_obj._aggregate_create_in_db,
                          self.context,
                          fake_create_aggregate,
                          metadata=None)

    def test_aggregate_delete(self):
        result = _create_aggregate(self.context, metadata=None)
        aggregate_obj._aggregate_delete_from_db(self.context, result['id'])
        self.assertRaises(exception.AggregateNotFound,
                          aggregate_obj._aggregate_get_from_db,
                          self.context, result['id'])

    def test_aggregate_delete_raise_not_found(self):
        # this does not exist!
        aggregate_id = 45
        self.assertRaises(exception.AggregateNotFound,
                          aggregate_obj._aggregate_delete_from_db,
                          self.context, aggregate_id)

    def test_aggregate_delete_with_metadata(self):
        result = _create_aggregate(self.context,
                            metadata={'availability_zone': 'fake_avail_zone'})
        aggregate_obj._aggregate_delete_from_db(self.context, result['id'])
        self.assertRaises(exception.AggregateNotFound,
                          aggregate_obj._aggregate_get_from_db,
                          self.context, result['id'])

    def test_aggregate_update(self):
        created = _create_aggregate(self.context,
                            metadata={'availability_zone': 'fake_avail_zone'})
        result = aggregate_obj._aggregate_get_from_db(self.context,
                                                      created['id'])
        self.assertEqual('fake_avail_zone', result['availability_zone'])
        new_values = deepcopy(_get_fake_aggregate(1, result=False))
        new_values['availability_zone'] = 'different_avail_zone'
        updated = aggregate_obj._aggregate_update_to_db(self.context,
                                                    result['id'], new_values)
        self.assertEqual('different_avail_zone', updated['availability_zone'])

    def test_aggregate_update_with_metadata(self):
        result = _create_aggregate(self.context, metadata=None)
        values = deepcopy(_get_fake_aggregate(1, result=False))
        values['metadata'] = deepcopy(_get_fake_metadata(1))
        values['availability_zone'] = 'different_avail_zone'
        expected_metadata = deepcopy(values['metadata'])
        expected_metadata['availability_zone'] = values['availability_zone']
        aggregate_obj._aggregate_update_to_db(self.context, result['id'],
                                              values)
        metadata = _aggregate_metadata_get_all(self.context, result['id'])
        updated = aggregate_obj._aggregate_get_from_db(self.context,
                                                       result['id'])
        self.assertThat(metadata,
                        matchers.DictMatches(expected_metadata))
        self.assertEqual('different_avail_zone', updated['availability_zone'])

    def test_aggregate_update_with_existing_metadata(self):
        result = _create_aggregate(self.context)
        values = deepcopy(_get_fake_aggregate(1, result=False))
        values['metadata'] = deepcopy(_get_fake_metadata(1))
        values['metadata']['fake_key1'] = 'foo'
        expected_metadata = deepcopy(values['metadata'])
        aggregate_obj._aggregate_update_to_db(self.context, result['id'],
                                              values)
        metadata = _aggregate_metadata_get_all(self.context, result['id'])
        self.assertThat(metadata, matchers.DictMatches(expected_metadata))

    def test_aggregate_update_zone_with_existing_metadata(self):
        result = _create_aggregate(self.context)
        new_zone = {'availability_zone': 'fake_avail_zone_2'}
        metadata = deepcopy(_get_fake_metadata(1))
        metadata.update(new_zone)
        aggregate_obj._aggregate_update_to_db(self.context, result['id'],
                                              new_zone)
        expected = _aggregate_metadata_get_all(self.context, result['id'])
        self.assertThat(metadata, matchers.DictMatches(expected))

    def test_aggregate_update_raise_not_found(self):
        # this does not exist!
        aggregate_id = 2
        new_values = deepcopy(_get_fake_aggregate(1, result=False))
        self.assertRaises(exception.AggregateNotFound,
                          aggregate_obj._aggregate_update_to_db,
                          self.context, aggregate_id, new_values)

    def test_aggregate_update_raise_name_exist(self):
        _create_aggregate(self.context, values={'name': 'test1'},
                         metadata={'availability_zone': 'fake_avail_zone'})
        _create_aggregate(self.context, values={'name': 'test2'},
                          metadata={'availability_zone': 'fake_avail_zone'})
        aggregate_id = 1
        new_values = {'name': 'test2'}
        self.assertRaises(exception.AggregateNameExists,
                          aggregate_obj._aggregate_update_to_db,
                          self.context, aggregate_id, new_values)

    def test_aggregate_host_add_to_db(self):
        result = _create_aggregate(self.context, metadata=None)
        host = _get_fake_hosts(1)[0]
        aggregate_obj._host_add_to_db(self.context, result['id'], host)
        expected = aggregate_obj._aggregate_get_from_db(self.context,
                                                        result['id'])
        self.assertEqual([_get_fake_hosts(1)[0]], expected.hosts)

    def test_aggregate_host_re_add_to_db(self):
        result = _create_aggregate_with_hosts(self.context,
                                              metadata=None)
        host = _get_fake_hosts(1)[0]
        aggregate_obj._host_delete_from_db(self.context, result['id'], host)
        aggregate_obj._host_add_to_db(self.context, result['id'], host)
        expected = _aggregate_host_get_all(self.context, result['id'])
        self.assertEqual(len(expected), 2)

    def test_aggregate_host_add_to_db_duplicate_works(self):
        r1 = _create_aggregate_with_hosts(self.context,
                                          metadata=None)
        r2 = _create_aggregate_with_hosts(self.context,
                          values={'name': 'fake_aggregate2'},
                          metadata={'availability_zone': 'fake_avail_zone2'})
        h1 = _aggregate_host_get_all(self.context, r1['id'])
        self.assertEqual(len(h1), 2)
        self.assertEqual(r1['id'], h1[0]['aggregate_id'])
        h2 = _aggregate_host_get_all(self.context, r2['id'])
        self.assertEqual(len(h2), 2)
        self.assertEqual(r2['id'], h2[0]['aggregate_id'])

    def test_aggregate_host_add_to_db_duplicate_raise_exist_exc(self):
        result = _create_aggregate_with_hosts(self.context,
                                              metadata=None)
        self.assertRaises(exception.AggregateHostExists,
                          aggregate_obj._host_add_to_db,
                          self.context, result['id'],
                          _get_fake_hosts(1)[0])

    def test_aggregate_host_add_to_db_raise_not_found(self):
        # this does not exist!
        aggregate_id = 1
        host = _get_fake_hosts(1)[0]
        self.assertRaises(exception.AggregateNotFound,
                          aggregate_obj._host_add_to_db,
                          self.context, aggregate_id, host)

    def test_aggregate_host_delete_from_db(self):
        result = _create_aggregate_with_hosts(self.context,
                                              metadata=None)
        aggregate_obj._host_delete_from_db(self.context, result['id'],
                                 _get_fake_hosts(1)[0])
        expected = _aggregate_host_get_all(self.context, result['id'])
        self.assertEqual(len(expected), 1)

    def test_aggregate_host_delete_from_db_raise_not_found(self):
        result = _create_aggregate(self.context)
        self.assertRaises(exception.AggregateHostNotFound,
                          aggregate_obj._host_delete_from_db,
                          self.context, result['id'],
                          _get_fake_hosts(1)[0])

    def test_aggregate_metadata_add(self):
        result = _create_aggregate(self.context, metadata=None)
        metadata = deepcopy(_get_fake_metadata(1))
        aggregate_obj._metadata_add_to_db(self.context, result['id'], metadata)
        expected = _aggregate_metadata_get_all(self.context, result['id'])
        self.assertThat(metadata, matchers.DictMatches(expected))

    def test_aggregate_metadata_add_empty_metadata(self):
        result = _create_aggregate(self.context, metadata=None)
        metadata = {}
        aggregate_obj._metadata_add_to_db(self.context, result['id'], metadata)
        expected = _aggregate_metadata_get_all(self.context, result['id'])
        self.assertThat(metadata, matchers.DictMatches(expected))

    def test_aggregate_metadata_add_and_update(self):
        result = _create_aggregate(self.context)
        metadata = deepcopy(_get_fake_metadata(1))
        key = list(metadata.keys())[0]
        new_metadata = {key: 'foo',
                        'fake_new_key': 'fake_new_value'}
        metadata.update(new_metadata)
        aggregate_obj._metadata_add_to_db(self.context,
                                          result['id'], new_metadata)
        expected = _aggregate_metadata_get_all(self.context, result['id'])
        self.assertThat(metadata, matchers.DictMatches(expected))

    def test_aggregate_metadata_add_retry(self):
        result = _create_aggregate(self.context, metadata=None)
        with mock.patch(
            'nova.db.api.models.AggregateMetadata.__table__.insert'
        ) as insert_mock:
            insert_mock.side_effect = db_exc.DBDuplicateEntry
            self.assertRaises(db_exc.DBDuplicateEntry,
                              aggregate_obj._metadata_add_to_db,
                              self.context,
                              result['id'],
                              {'fake_key2': 'fake_value2'},
                              max_retries=5)

    def test_aggregate_metadata_update(self):
        result = _create_aggregate(self.context)
        metadata = deepcopy(_get_fake_metadata(1))
        key = list(metadata.keys())[0]
        aggregate_obj._metadata_delete_from_db(self.context, result['id'], key)
        new_metadata = {key: 'foo'}
        aggregate_obj._metadata_add_to_db(self.context,
                                          result['id'], new_metadata)
        expected = _aggregate_metadata_get_all(self.context, result['id'])
        metadata[key] = 'foo'
        self.assertThat(metadata, matchers.DictMatches(expected))

    def test_aggregate_metadata_delete(self):
        result = _create_aggregate(self.context, metadata=None)
        metadata = deepcopy(_get_fake_metadata(1))
        aggregate_obj._metadata_add_to_db(self.context, result['id'], metadata)
        aggregate_obj._metadata_delete_from_db(self.context, result['id'],
                                           list(metadata.keys())[0])
        expected = _aggregate_metadata_get_all(self.context, result['id'])
        del metadata[list(metadata.keys())[0]]
        self.assertThat(metadata, matchers.DictMatches(expected))

    def test_aggregate_remove_availability_zone(self):
        result = _create_aggregate(self.context, metadata={'availability_zone':
            'fake_avail_zone'})
        aggregate_obj._metadata_delete_from_db(self.context,
                                           result['id'],
                                           'availability_zone')
        aggr = aggregate_obj._aggregate_get_from_db(self.context, result['id'])
        self.assertIsNone(aggr['availability_zone'])

    def test_aggregate_metadata_delete_raise_not_found(self):
        result = _create_aggregate(self.context)
        self.assertRaises(exception.AggregateMetadataNotFound,
                          aggregate_obj._metadata_delete_from_db,
                          self.context, result['id'], 'foo_key')


def create_aggregate(context, db_id):
    fake_aggregate = _get_fake_aggregate(db_id, in_api=False, result=False)
    aggregate_obj._aggregate_create_in_db(context, fake_aggregate,
                                          metadata=_get_fake_metadata(db_id))
    for host in _get_fake_hosts(db_id):
        aggregate_obj._host_add_to_db(context, fake_aggregate['id'], host)


def compare_obj(test, result, source):
    source['deleted'] = False

    def updated_at_comparator(result, source):
        return True

    return base_compare(test, result, source, subs=SUBS,
                        comparators={'updated_at': updated_at_comparator})


class AggregateObjectTestCase(test.TestCase):
    def setUp(self):
        super(AggregateObjectTestCase, self).setUp()
        self.context = context.RequestContext('fake-user', 'fake-project')
        self._seed_data()

    def _seed_data(self):
        for i in range(1, 10):
            create_aggregate(self.context, i)

    def test_create(self):
        new_agg = aggregate_obj.Aggregate(self.context)
        new_agg.name = 'new-aggregate'
        new_agg.create()
        result = aggregate_obj.Aggregate.get_by_id(self.context, new_agg.id)
        self.assertEqual(new_agg.name, result.name)

    def test_get_by_id(self):
        for i in range(1, 10):
            agg = aggregate_obj.Aggregate.get_by_id(self.context, i)
            compare_obj(self, agg, _get_fake_aggregate(i))

    def test_save(self):
        for i in range(1, 10):
            agg = aggregate_obj.Aggregate.get_by_id(self.context, i)
            fake_agg = _get_fake_aggregate(i)
            fake_agg['name'] = 'new-name' + str(i)
            agg.name = 'new-name' + str(i)
            agg.save()
            result = aggregate_obj.Aggregate.get_by_id(self.context, i)
            compare_obj(self, agg, fake_agg)
            compare_obj(self, result, fake_agg)

    def test_update_metadata(self):
        for i in range(1, 10):
            agg = aggregate_obj.Aggregate.get_by_id(self.context, i)
            fake_agg = _get_fake_aggregate(i)
            fake_agg['metadetails'] = {'constant_key': 'constant_value'}
            agg.update_metadata({'unique_key': None})
            agg.save()
            result = aggregate_obj.Aggregate.get_by_id(self.context, i)
            compare_obj(self, agg, fake_agg)
            compare_obj(self, result, fake_agg)

    def test_destroy(self):
        for i in range(1, 10):
            agg = aggregate_obj.Aggregate.get_by_id(self.context, i)
            agg.destroy()
        aggs = aggregate_obj.AggregateList.get_all(self.context)
        self.assertEqual(len(aggs), 0)

    def test_add_host(self):
        for i in range(1, 10):
            agg = aggregate_obj.Aggregate.get_by_id(self.context, i)
            fake_agg = _get_fake_aggregate(i)
            fake_agg['hosts'].append('barbar')
            agg.add_host('barbar')
            agg.save()
            result = aggregate_obj.Aggregate.get_by_id(self.context, i)
            compare_obj(self, agg, fake_agg)
            compare_obj(self, result, fake_agg)

    def test_delete_host(self):
        for i in range(1, 10):
            agg = aggregate_obj.Aggregate.get_by_id(self.context, i)
            fake_agg = _get_fake_aggregate(i)
            fake_agg['hosts'].remove('constant_host')
            agg.delete_host('constant_host')
            result = aggregate_obj.Aggregate.get_by_id(self.context, i)
            compare_obj(self, agg, fake_agg)
            compare_obj(self, result, fake_agg)

    def test_get_by_metadata(self):
        agg = aggregate_obj.Aggregate.get_by_id(self.context, 1)
        agg.update_metadata({'foo': 'bar'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 2)
        agg.update_metadata({'foo': 'baz',
                             'fu': 'bar'})

        aggs = aggregate_obj.AggregateList.get_by_metadata(
            self.context, key='foo', value='bar')
        self.assertEqual(1, len(aggs))
        self.assertEqual(1, aggs[0].id)

        aggs = aggregate_obj.AggregateList.get_by_metadata(
            self.context, value='bar')
        self.assertEqual(2, len(aggs))
        self.assertEqual(set([1, 2]), set([a.id for a in aggs]))

    def test_get_by_metadata_from_db_assertion(self):
        self.assertRaises(AssertionError,
                          aggregate_obj._get_by_metadata_from_db,
                          self.context)

    def test_get_non_matching_by_metadata_keys(self):
        """Test aggregates that are not matching with metadata."""

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 1)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 2)
        agg.update_metadata({'trait:HW_CPU_X86_SGX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 3)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 4)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'just_for_marking'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 5)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'just_for_marking'})

        aggs = aggregate_obj.AggregateList.get_non_matching_by_metadata_keys(
            self.context, ['trait:HW_CPU_X86_MMX'], 'trait:',
            value='required')

        self.assertEqual(2, len(aggs))
        self.assertCountEqual([2, 3], [a.id for a in aggs])

    def test_matching_aggregates_multiple_keys(self):
        """All matching aggregates for multiple keys."""

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 1)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 2)
        agg.update_metadata({'trait:HW_CPU_X86_SGX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 3)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 4)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'just_for_marking'})

        aggs = aggregate_obj.AggregateList.get_non_matching_by_metadata_keys(
            self.context, ['trait:HW_CPU_X86_MMX', 'trait:HW_CPU_X86_SGX'],
            'trait:', value='required')

        self.assertEqual(0, len(aggs))

    def test_get_non_matching_aggregates_multiple_keys(self):
        """Return non matching aggregates for multiple keys."""

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 1)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 2)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'required',
                             'trait:HW_CPU_API_DXVA': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 3)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 4)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'just_for_marking'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 5)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'just_for_marking',
                             'trait:HW_CPU_X86_SSE': 'required'})

        aggs = aggregate_obj.AggregateList.get_non_matching_by_metadata_keys(
            self.context, ['trait:HW_CPU_X86_MMX', 'trait:HW_CPU_X86_SGX'],
            'trait:', value='required')

        self.assertEqual(2, len(aggs))
        self.assertCountEqual([2, 5], [a.id for a in aggs])

    def test_get_non_matching_by_metadata_keys_empty_keys(self):
        """Test aggregates non matching by metadata with empty keys."""

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 1)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 2)
        agg.update_metadata({'trait:HW_CPU_X86_SGX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 3)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'required'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 4)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'just_for_marking'})

        agg = aggregate_obj.Aggregate.get_by_id(self.context, 5)
        agg.update_metadata({'trait:HW_CPU_X86_MMX': 'required',
                             'trait:HW_CPU_X86_SGX': 'just_for_marking',
                             'trait:HW_CPU_X86_SSE': 'required'})

        aggs = aggregate_obj.AggregateList.get_non_matching_by_metadata_keys(
            self.context, [], 'trait:', value='required')

        self.assertEqual(5, len(aggs))
        self.assertCountEqual([1, 2, 3, 4, 5], [a.id for a in aggs])

    def test_get_non_matching_by_metadata_keys_empty_key_prefix(self):
        """Test aggregates non matching by metadata with empty key_prefix."""

        self.assertRaises(
                ValueError,
                aggregate_obj.AggregateList.get_non_matching_by_metadata_keys,
                self.context, ['trait:HW_CPU_X86_MMX'], '',
                value='required')
