# All Rights Reserved.
#
#    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 datetime

import mock
import six
from webob import exc

from nova.api.openstack.compute import migrations as migrations_v21
from nova import context
from nova import exception
from nova import objects
from nova.objects import base
from nova import test
from nova.tests.unit.api.openstack import fakes
from nova.tests import uuidsentinel as uuids


fake_migrations = [
    # in-progress live migration
    {
        'id': 1,
        'source_node': 'node1',
        'dest_node': 'node2',
        'source_compute': 'compute1',
        'dest_compute': 'compute2',
        'dest_host': '1.2.3.4',
        'status': 'running',
        'instance_uuid': uuids.instance1,
        'old_instance_type_id': 1,
        'new_instance_type_id': 2,
        'migration_type': 'live-migration',
        'hidden': False,
        'memory_total': 123456,
        'memory_processed': 12345,
        'memory_remaining': 111111,
        'disk_total': 234567,
        'disk_processed': 23456,
        'disk_remaining': 211111,
        'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
        'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
        'deleted_at': None,
        'deleted': False,
        'uuid': uuids.migration1,
    },
    # non in-progress live migration
    {
        'id': 2,
        'source_node': 'node1',
        'dest_node': 'node2',
        'source_compute': 'compute1',
        'dest_compute': 'compute2',
        'dest_host': '1.2.3.4',
        'status': 'error',
        'instance_uuid': uuids.instance1,
        'old_instance_type_id': 1,
        'new_instance_type_id': 2,
        'migration_type': 'live-migration',
        'hidden': False,
        'memory_total': 123456,
        'memory_processed': 12345,
        'memory_remaining': 111111,
        'disk_total': 234567,
        'disk_processed': 23456,
        'disk_remaining': 211111,
        'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
        'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
        'deleted_at': None,
        'deleted': False,
        'uuid': uuids.migration2,
    },
    # in-progress resize
    {
        'id': 4,
        'source_node': 'node10',
        'dest_node': 'node20',
        'source_compute': 'compute10',
        'dest_compute': 'compute20',
        'dest_host': '5.6.7.8',
        'status': 'migrating',
        'instance_uuid': uuids.instance2,
        'old_instance_type_id': 5,
        'new_instance_type_id': 6,
        'migration_type': 'resize',
        'hidden': False,
        'memory_total': 456789,
        'memory_processed': 56789,
        'memory_remaining': 45000,
        'disk_total': 96789,
        'disk_processed': 6789,
        'disk_remaining': 96000,
        'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
        'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
        'deleted_at': None,
        'deleted': False,
        'uuid': uuids.migration3,
    },
    # non in-progress resize
    {
        'id': 5,
        'source_node': 'node10',
        'dest_node': 'node20',
        'source_compute': 'compute10',
        'dest_compute': 'compute20',
        'dest_host': '5.6.7.8',
        'status': 'error',
        'instance_uuid': uuids.instance2,
        'old_instance_type_id': 5,
        'new_instance_type_id': 6,
        'migration_type': 'resize',
        'hidden': False,
        'memory_total': 456789,
        'memory_processed': 56789,
        'memory_remaining': 45000,
        'disk_total': 96789,
        'disk_processed': 6789,
        'disk_remaining': 96000,
        'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
        'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
        'deleted_at': None,
        'deleted': False,
        'uuid': uuids.migration4,
    }
]

migrations_obj = base.obj_make_list(
    'fake-context',
    objects.MigrationList(),
    objects.Migration,
    fake_migrations)


class FakeRequest(object):
    environ = {"nova.context": context.RequestContext('fake_user', 'fake',
                                                      is_admin=True)}
    GET = {}


class MigrationsTestCaseV21(test.NoDBTestCase):
    migrations = migrations_v21

    def _migrations_output(self):
        return self.controller._output(self.req, migrations_obj)

    def setUp(self):
        """Run before each test."""
        super(MigrationsTestCaseV21, self).setUp()
        self.controller = self.migrations.MigrationsController()
        self.req = fakes.HTTPRequest.blank('', use_admin_context=True)
        self.context = self.req.environ['nova.context']

    def test_index(self):
        migrations_in_progress = {'migrations': self._migrations_output()}

        for mig in migrations_in_progress['migrations']:
            self.assertIn('id', mig)
            self.assertNotIn('deleted', mig)
            self.assertNotIn('deleted_at', mig)
            self.assertNotIn('links', mig)

        filters = {'host': 'host1', 'status': 'migrating',
                   'instance_uuid': uuids.instance1,
                   'source_compute': 'host1', 'hidden': '0',
                   'migration_type': 'resize'}

        # python-novaclient actually supports sending this even though it's
        # not used in the DB API layer and is totally useless. This lets us,
        # however, test that additionalProperties=True allows it.
        unknown_filter = {'cell_name': 'ChildCell'}

        self.req.GET.update(filters)
        self.req.GET.update(unknown_filter)

        with mock.patch.object(self.controller.compute_api,
                               'get_migrations',
                               return_value=migrations_obj) as (
            mock_get_migrations
        ):
            response = self.controller.index(self.req)
            self.assertEqual(migrations_in_progress, response)
            # Only with the filters, and the unknown filter is stripped
            mock_get_migrations.assert_called_once_with(self.context, filters)

    def test_index_query_allow_negative_int_as_string(self):
        migrations = {'migrations': self._migrations_output()}
        filters = ['host', 'status', 'cell_name', 'instance_uuid',
                   'source_compute', 'hidden', 'migration_type']

        with mock.patch.object(self.controller.compute_api,
                               'get_migrations',
                               return_value=migrations_obj):
            for fl in filters:
                req = fakes.HTTPRequest.blank('/os-migrations',
                                              use_admin_context=True,
                                              query_string='%s=-1' % fl)
                response = self.controller.index(req)
                self.assertEqual(migrations, response)

    def test_index_query_duplicate_query_parameters(self):
        migrations = {'migrations': self._migrations_output()}
        params = {'host': 'host1', 'status': 'migrating',
                  'cell_name': 'ChildCell', 'instance_uuid': uuids.instance1,
                  'source_compute': 'host1', 'hidden': '0',
                  'migration_type': 'resize'}

        with mock.patch.object(self.controller.compute_api,
                               'get_migrations',
                               return_value=migrations_obj):
            for k, v in params.items():
                req = fakes.HTTPRequest.blank(
                    '/os-migrations', use_admin_context=True,
                    query_string='%s=%s&%s=%s' % (k, v, k, v))
                response = self.controller.index(req)
                self.assertEqual(migrations, response)


class MigrationsTestCaseV223(MigrationsTestCaseV21):
    wsgi_api_version = '2.23'

    def setUp(self):
        """Run before each test."""
        super(MigrationsTestCaseV223, self).setUp()
        self.req = fakes.HTTPRequest.blank(
            '', version=self.wsgi_api_version, use_admin_context=True)

    def test_index(self):
        migrations = {'migrations': self.controller._output(
                                        self.req, migrations_obj, True)}

        for i, mig in enumerate(migrations['migrations']):
            # first item is in-progress live migration
            if i == 0:
                self.assertIn('links', mig)
            else:
                self.assertNotIn('links', mig)

            self.assertIn('migration_type', mig)
            self.assertIn('id', mig)
            self.assertNotIn('deleted', mig)
            self.assertNotIn('deleted_at', mig)

        with mock.patch.object(self.controller.compute_api,
                               'get_migrations') as m_get:
            m_get.return_value = migrations_obj
            response = self.controller.index(self.req)
            self.assertEqual(migrations, response)
            self.assertIn('links', response['migrations'][0])
            self.assertIn('migration_type', response['migrations'][0])


class MigrationsTestCaseV259(MigrationsTestCaseV223):
    wsgi_api_version = '2.59'

    def test_index(self):
        migrations = {'migrations': self.controller._output(
                                        self.req, migrations_obj, True, True)}

        for i, mig in enumerate(migrations['migrations']):
            # first item is in-progress live migration
            if i == 0:
                self.assertIn('links', mig)
            else:
                self.assertNotIn('links', mig)

            self.assertIn('migration_type', mig)
            self.assertIn('id', mig)
            self.assertIn('uuid', mig)
            self.assertNotIn('deleted', mig)
            self.assertNotIn('deleted_at', mig)

        with mock.patch.object(self.controller.compute_api,
                               'get_migrations_sorted') as m_get:
            m_get.return_value = migrations_obj
            response = self.controller.index(self.req)
            self.assertEqual(migrations, response)
            self.assertIn('links', response['migrations'][0])
            self.assertIn('migration_type', response['migrations'][0])

    @mock.patch('nova.compute.api.API.get_migrations_sorted')
    def test_index_with_invalid_marker(self, mock_migrations_get):
        """Tests detail paging with an invalid marker (not found)."""
        mock_migrations_get.side_effect = exception.MarkerNotFound(
            marker=uuids.invalid_marker)
        req = fakes.HTTPRequest.blank(
            '/os-migrations?marker=%s' % uuids.invalid_marker,
            version=self.wsgi_api_version, use_admin_context=True)
        e = self.assertRaises(exc.HTTPBadRequest,
                              self.controller.index, req)
        self.assertEqual(
            "Marker %s could not be found." % uuids.invalid_marker,
            six.text_type(e))

    def test_index_with_invalid_limit(self):
        """Tests detail paging with an invalid limit."""
        req = fakes.HTTPRequest.blank(
            '/os-migrations?limit=x', version=self.wsgi_api_version,
            use_admin_context=True)
        self.assertRaises(exception.ValidationError,
                          self.controller.index, req)
        req = fakes.HTTPRequest.blank(
            '/os-migrations?limit=-1', version=self.wsgi_api_version,
            use_admin_context=True)
        self.assertRaises(exception.ValidationError,
                          self.controller.index, req)

    def test_index_with_invalid_changes_since(self):
        """Tests detail paging with an invalid changes-since value."""
        req = fakes.HTTPRequest.blank(
            '/os-migrations?changes-since=wrong_time',
            version=self.wsgi_api_version, use_admin_context=True)
        self.assertRaises(exception.ValidationError,
                          self.controller.index, req)

    def test_index_with_unknown_query_param(self):
        """Tests detail paging with an unknown query parameter."""
        req = fakes.HTTPRequest.blank(
            '/os-migrations?foo=bar',
            version=self.wsgi_api_version, use_admin_context=True)
        ex = self.assertRaises(exception.ValidationError,
                               self.controller.index, req)
        self.assertIn('Additional properties are not allowed',
                      six.text_type(ex))

    @mock.patch('nova.compute.api.API.get_migrations',
                return_value=objects.MigrationList())
    def test_index_with_changes_since_old_microversion(self, get_migrations):
        """Tests that the changes-since query parameteris ignored before
        microversion 2.59.
        """
        # Also use a valid filter (instance_uuid) to make sure only
        # changes-since is removed.
        req = fakes.HTTPRequest.blank(
            '/os-migrations?changes-since=2018-01-10T16:59:24.138939&'
            'instance_uuid=%s' % uuids.instance_uuid,
            version='2.58', use_admin_context=True)
        result = self.controller.index(req)
        self.assertEqual({'migrations': []}, result)
        get_migrations.assert_called_once_with(
            req.environ['nova.context'],
            {'instance_uuid': uuids.instance_uuid})


class MigrationsPolicyEnforcement(test.NoDBTestCase):
    def setUp(self):
        super(MigrationsPolicyEnforcement, self).setUp()
        self.controller = migrations_v21.MigrationsController()
        self.req = fakes.HTTPRequest.blank('')

    def test_list_policy_failed(self):
        rule_name = "os_compute_api:os-migrations:index"
        self.policy.set_rules({rule_name: "project_id:non_fake"})
        exc = self.assertRaises(
            exception.PolicyNotAuthorized,
            self.controller.index, self.req)
        self.assertEqual(
            "Policy doesn't allow %s to be performed." % rule_name,
            exc.format_message())


class MigrationsPolicyEnforcementV223(MigrationsPolicyEnforcement):
    wsgi_api_version = '2.23'

    def setUp(self):
        super(MigrationsPolicyEnforcementV223, self).setUp()
        self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version)


class MigrationsPolicyEnforcementV259(MigrationsPolicyEnforcementV223):
    wsgi_api_version = '2.59'
