# -*- coding: utf-8 -*-

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

import mock
import os_client_config as occ
import yaml

import shade
from shade import meta
from shade.tests import fakes
from shade.tests.unit import base


class TestMemoryCache(base.TestCase):

    CACHING_CONFIG = {
        'cache':
        {
            'max_age': 90,
            'class': 'dogpile.cache.memory',
        },
        'clouds':
        {
            '_cache_test_':
            {
                'auth':
                {
                    'auth_url': 'http://198.51.100.1:35357/v2.0',
                    'username': '_test_user_',
                    'password': '_test_pass_',
                    'project_name': '_test_project_',
                },
                'region_name': '_test_region_',
            },
        },
    }

    def setUp(self):
        super(TestMemoryCache, self).setUp()

        # Isolate os-client-config from test environment
        config = tempfile.NamedTemporaryFile(delete=False)
        config.write(bytes(yaml.dump(self.CACHING_CONFIG).encode('utf-8')))
        config.close()
        vendor = tempfile.NamedTemporaryFile(delete=False)
        vendor.write(b'{}')
        vendor.close()

        self.cloud_config = occ.OpenStackConfig(config_files=[config.name],
                                                vendor_files=[vendor.name])
        self.cloud = shade.openstack_cloud(cloud='_cache_test_',
                                           config=self.cloud_config)

    def test_openstack_cloud(self):
        self.assertIsInstance(self.cloud, shade.OpenStackCloud)

    @mock.patch('shade.OpenStackCloud.keystone_client')
    def test_project_cache(self, keystone_mock):
        project = fakes.FakeProject('project_a')
        keystone_mock.projects.list.return_value = [project]
        self.assertEqual({'project_a': project}, self.cloud.project_cache)
        project_b = fakes.FakeProject('project_b')
        keystone_mock.projects.list.return_value = [project,
                                                    project_b]
        self.assertEqual(
            {'project_a': project}, self.cloud.project_cache)
        self.cloud.get_project_cache.invalidate(self.cloud)
        self.assertEqual(
            {'project_a': project,
             'project_b': project_b}, self.cloud.project_cache)

    @mock.patch('shade.OpenStackCloud.cinder_client')
    def test_list_volumes(self, cinder_mock):
        fake_volume = fakes.FakeVolume('volume1', 'available',
                                       'Volume 1 Display Name')
        fake_volume_dict = meta.obj_to_dict(fake_volume)
        cinder_mock.volumes.list.return_value = [fake_volume]
        self.assertEqual([fake_volume_dict], self.cloud.list_volumes())
        fake_volume2 = fakes.FakeVolume('volume2', 'available',
                                        'Volume 2 Display Name')
        fake_volume2_dict = meta.obj_to_dict(fake_volume2)
        cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2]
        self.assertEqual([fake_volume_dict], self.cloud.list_volumes())
        self.cloud.list_volumes.invalidate(self.cloud)
        self.assertEqual([fake_volume_dict, fake_volume2_dict],
                         self.cloud.list_volumes())

    @mock.patch('shade.OpenStackCloud.cinder_client')
    def test_list_volumes_creating_invalidates(self, cinder_mock):
        fake_volume = fakes.FakeVolume('volume1', 'creating',
                                       'Volume 1 Display Name')
        fake_volume_dict = meta.obj_to_dict(fake_volume)
        cinder_mock.volumes.list.return_value = [fake_volume]
        self.assertEqual([fake_volume_dict], self.cloud.list_volumes())
        fake_volume2 = fakes.FakeVolume('volume2', 'available',
                                        'Volume 2 Display Name')
        fake_volume2_dict = meta.obj_to_dict(fake_volume2)
        cinder_mock.volumes.list.return_value = [fake_volume, fake_volume2]
        self.assertEqual([fake_volume_dict, fake_volume2_dict],
                         self.cloud.list_volumes())

    @mock.patch.object(shade.OpenStackCloud, 'cinder_client')
    def test_create_volume_invalidates(self, cinder_mock):
        fake_volb4 = fakes.FakeVolume('volume1', 'available',
                                      'Volume 1 Display Name')
        fake_volb4_dict = meta.obj_to_dict(fake_volb4)
        cinder_mock.volumes.list.return_value = [fake_volb4]
        self.assertEqual([fake_volb4_dict], self.cloud.list_volumes())
        volume = dict(display_name='junk_vol',
                      size=1,
                      display_description='test junk volume')
        fake_vol = fakes.FakeVolume('12345', 'creating', '')
        fake_vol_dict = meta.obj_to_dict(fake_vol)
        cinder_mock.volumes.create.return_value = fake_vol
        cinder_mock.volumes.list.return_value = [fake_volb4, fake_vol]

        def creating_available():
            def now_available():
                fake_vol.status = 'available'
                fake_vol_dict['status'] = 'available'
                return mock.DEFAULT
            cinder_mock.volumes.list.side_effect = now_available
            return mock.DEFAULT
        cinder_mock.volumes.list.side_effect = creating_available
        self.cloud.create_volume(wait=True, timeout=None, **volume)
        self.assertTrue(cinder_mock.volumes.create.called)
        self.assertEqual(3, cinder_mock.volumes.list.call_count)
        # If cache was not invalidated, we would not see our own volume here
        # because the first volume was available and thus would already be
        # cached.
        self.assertEqual([fake_volb4_dict, fake_vol_dict],
                         self.cloud.list_volumes())

        # And now delete and check same thing since list is cached as all
        # available
        fake_vol.status = 'deleting'
        fake_vol_dict = meta.obj_to_dict(fake_vol)

        def deleting_gone():
            def now_gone():
                cinder_mock.volumes.list.return_value = [fake_volb4]
                return mock.DEFAULT
            cinder_mock.volumes.list.side_effect = now_gone
            return mock.DEFAULT
        cinder_mock.volumes.list.return_value = [fake_volb4, fake_vol]
        cinder_mock.volumes.list.side_effect = deleting_gone
        cinder_mock.volumes.delete.return_value = fake_vol_dict
        self.cloud.delete_volume('12345')
        self.assertEqual([fake_volb4_dict], self.cloud.list_volumes())

    @mock.patch.object(shade.OpenStackCloud, 'keystone_client')
    def test_get_user_cache(self, keystone_mock):
        fake_user = fakes.FakeUser('999', '', '')
        keystone_mock.users.list.return_value = [fake_user]
        self.assertEqual({'999': fake_user}, self.cloud.get_user_cache())

    @mock.patch.object(shade.OpenStackCloud, 'keystone_client')
    def test_modify_user_invalidates_cache(self, keystone_mock):
        fake_user = fakes.FakeUser('abc123', 'abc123@domain.test',
                                   'abc123 name')
        # first cache an empty list
        keystone_mock.users.list.return_value = []
        self.assertEqual({}, self.cloud.get_user_cache())
        # now add one
        keystone_mock.users.list.return_value = [fake_user]
        keystone_mock.users.create.return_value = fake_user
        created = self.cloud.create_user(name='abc123 name',
                                         email='abc123@domain.test')
        self.assertEqual({'id': 'abc123',
                          'name': 'abc123 name',
                          'email': 'abc123@domain.test'}, created)
        # Cache should have been invalidated
        self.assertEqual({'abc123': fake_user}, self.cloud.get_user_cache())
        # Update and check to see if it is updated
        fake_user2 = fakes.FakeUser('abc123', 'abc123 name',
                                    'abc123-changed@domain.test')
        keystone_mock.users.update.return_value = fake_user2
        keystone_mock.users.list.return_value = [fake_user2]
        self.cloud.update_user('abc123', email='abc123-changed@domain.test')
        keystone_mock.users.update.assert_called_with(
            user=fake_user2, email='abc123-changed@domain.test')
        self.assertEqual({'abc123': fake_user2}, self.cloud.get_user_cache())
        # Now delete and ensure it disappears
        keystone_mock.users.list.return_value = []
        self.cloud.delete_user('abc123')
        self.assertEqual({}, self.cloud.get_user_cache())
        self.assertTrue(keystone_mock.users.delete.was_called)

    @mock.patch.object(shade.OpenStackCloud, 'nova_client')
    def test_list_flavors(self, nova_mock):
        nova_mock.flavors.list.return_value = []
        self.assertEqual([], self.cloud.list_flavors())

        fake_flavor = fakes.FakeFlavor('555', 'vanilla')
        fake_flavor_dict = meta.obj_to_dict(fake_flavor)
        nova_mock.flavors.list.return_value = [fake_flavor]
        self.cloud.list_flavors.invalidate(self.cloud)
        self.assertEqual([fake_flavor_dict], self.cloud.list_flavors())

    @mock.patch.object(shade.OpenStackCloud, 'glance_client')
    def test_list_images(self, glance_mock):
        glance_mock.images.list.return_value = []
        self.assertEqual([], self.cloud.list_images())

        fake_image = fakes.FakeImage('22', '22 name', 'success')
        fake_image_dict = meta.obj_to_dict(fake_image)
        glance_mock.images.list.return_value = [fake_image]
        self.cloud.list_images.invalidate(self.cloud)
        self.assertEqual([fake_image_dict], self.cloud.list_images())

    @mock.patch.object(shade.OpenStackCloud, 'glance_client')
    def test_list_images_ignores_unsteady_status(self, glance_mock):
        steady_image = fakes.FakeImage('68', 'Jagr', 'active')
        steady_image_dict = meta.obj_to_dict(steady_image)
        for status in ('queued', 'saving', 'pending_delete'):
            active_image = fakes.FakeImage(self.getUniqueString(),
                                           self.getUniqueString(), status)
            glance_mock.images.list.return_value = [active_image]
            active_image_dict = meta.obj_to_dict(active_image)
            self.assertEqual([active_image_dict],
                             self.cloud.list_images())
            glance_mock.images.list.return_value = [active_image, steady_image]
            # Should expect steady_image to appear if active wasn't cached
            self.assertEqual([active_image_dict, steady_image_dict],
                             self.cloud.list_images())

    @mock.patch.object(shade.OpenStackCloud, 'glance_client')
    def test_list_images_caches_steady_status(self, glance_mock):
        steady_image = fakes.FakeImage('91', 'Federov', 'active')
        first_image = None
        for status in ('active', 'deleted', 'killed'):
            active_image = fakes.FakeImage(self.getUniqueString(),
                                           self.getUniqueString(), status)
            active_image_dict = meta.obj_to_dict(active_image)
            if not first_image:
                first_image = active_image_dict
            glance_mock.images.list.return_value = [active_image]
            self.assertEqual([first_image], self.cloud.list_images())
            glance_mock.images.list.return_value = [active_image, steady_image]
            # because we skipped the create_image code path, no invalidation
            # was done, so we _SHOULD_ expect steady state images to cache and
            # therefore we should _not_ expect to see the new one here
            self.assertEqual([first_image], self.cloud.list_images())

    def _call_create_image(self, name, container=None):
        imagefile = tempfile.NamedTemporaryFile(delete=False)
        imagefile.write(b'\0')
        imagefile.close()
        self.cloud.create_image(
            name, imagefile.name, container=container, wait=True)

    @mock.patch.object(shade.OpenStackCloud, 'glance_client')
    def test_create_image_put_v1(self, glance_mock):
        self.cloud.api_versions['image'] = '1'
        glance_mock.images.list.return_value = []
        self.assertEqual([], self.cloud.list_images())

        fake_image = fakes.FakeImage('42', '42 name', 'success')
        glance_mock.images.create.return_value = fake_image
        glance_mock.images.list.return_value = [fake_image]
        self._call_create_image('42 name')
        args = {'name': '42 name',
                'properties': {'owner_specified.shade.md5': mock.ANY,
                               'owner_specified.shade.sha256': mock.ANY}}
        glance_mock.images.create.assert_called_with(**args)
        glance_mock.images.update.assert_called_with(data=mock.ANY,
                                                     image=fake_image)
        fake_image_dict = meta.obj_to_dict(fake_image)
        self.assertEqual([fake_image_dict], self.cloud.list_images())

    @mock.patch.object(shade.OpenStackCloud, 'glance_client')
    def test_create_image_put_v2(self, glance_mock):
        self.cloud.api_versions['image'] = '2'
        self.cloud.image_api_use_tasks = False

        glance_mock.images.list.return_value = []
        self.assertEqual([], self.cloud.list_images())

        fake_image = fakes.FakeImage('42', '42 name', 'success')
        glance_mock.images.create.return_value = fake_image
        glance_mock.images.list.return_value = [fake_image]
        self._call_create_image('42 name')
        args = {'name': '42 name',
                'owner_specified.shade.md5': mock.ANY,
                'owner_specified.shade.sha256': mock.ANY}
        glance_mock.images.create.assert_called_with(**args)
        glance_mock.images.upload.assert_called_with(
            image_data=mock.ANY, image_id=fake_image.id, image_size=1)
        fake_image_dict = meta.obj_to_dict(fake_image)
        self.assertEqual([fake_image_dict], self.cloud.list_images())

    @mock.patch.object(shade.OpenStackCloud, 'glance_client')
    @mock.patch.object(shade.OpenStackCloud, 'swift_client')
    def test_create_image_task(self, swift_mock, glance_mock):
        self.cloud.api_versions['image'] = '2'
        self.cloud.image_api_use_tasks = True

        class Container(object):
            name = 'image_upload_v2_test_container'

        fake_container = Container()
        swift_mock.put_container.return_value = fake_container
        swift_mock.head_object.return_value = {}
        glance_mock.images.list.return_value = []
        self.assertEqual([], self.cloud.list_images())

        # V2's warlock objects just work like dicts
        class FakeImage(dict):
            status = 'CREATED'
            id = '99'
            name = '99 name'

        fake_image = FakeImage()
        fake_image.update({
            'id': '99',
            'name': '99 name',
            shade.IMAGE_MD5_KEY: '',
            shade.IMAGE_SHA256_KEY: '',
        })
        glance_mock.images.list.return_value = [fake_image]

        class FakeTask(dict):
            status = 'success'
            result = {'image_id': '99'}

        fake_task = FakeTask()
        fake_task.update({
            'id': '100',
            'status': 'success',
        })
        glance_mock.tasks.get.return_value = fake_task
        self._call_create_image(name='99 name',
                                container='image_upload_v2_test_container')
        args = {'headers': {'x-object-meta-x-shade-md5': mock.ANY,
                            'x-object-meta-x-shade-sha256': mock.ANY},
                'obj': '99 name',
                'container': 'image_upload_v2_test_container'}
        swift_mock.post_object.assert_called_with(**args)
        swift_mock.put_object.assert_called_with(
            contents=mock.ANY,
            obj='99 name',
            container='image_upload_v2_test_container')
        glance_mock.tasks.create.assert_called_with(type='import', input={
            'import_from': 'image_upload_v2_test_container/99 name',
            'image_properties': {'name': '99 name'}})
        args = {'owner_specified.shade.md5': mock.ANY,
                'owner_specified.shade.sha256': mock.ANY,
                'image_id': '99'}
        glance_mock.images.update.assert_called_with(**args)
        fake_image_dict = meta.obj_to_dict(fake_image)
        self.assertEqual([fake_image_dict], self.cloud.list_images())

    @mock.patch.object(shade.OpenStackCloud, 'glance_client')
    def test_cache_no_cloud_name(self, glance_mock):
        class FakeImage(dict):
            id = 1
            status = 'active'
            name = 'None Test Image'
        fi = FakeImage(id=FakeImage.id, status=FakeImage.status,
                       name=FakeImage.name)
        glance_mock.images.list.return_value = [fi]
        self.cloud.name = None
        self.assertEqual([fi], [dict(x) for x in self.cloud.list_images()])
        # Now test that the list was cached
        fi2 = FakeImage(id=2, status=FakeImage.status, name=FakeImage.name)
        fi2.id = 2
        glance_mock.images.list.return_value = [fi, fi2]
        self.assertEqual([fi], [dict(x) for x in self.cloud.list_images()])
        # Invalidation too
        self.cloud.list_images.invalidate(self.cloud)
        self.assertEqual(
            [fi, fi2], [dict(x) for x in self.cloud.list_images()])
