# Copyright (c) 2015 Red Hat Inc.
# 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 itertools
from unittest import mock

from neutron_lib import context
from neutron_lib.exceptions import qos as qos_exc
from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.services.qos import constants as qos_consts
from oslo_utils import uuidutils

from neutron.core_extensions import base as base_core
from neutron.core_extensions import qos as qos_core
from neutron.objects.qos import policy
from neutron.tests import base


def _get_test_dbdata(qos_policy_id, qos_network_policy_id=None):
    retval = {'id': None,
              'qos_policy_binding': {'policy_id': qos_policy_id,
                                     'network_id': 'fake_net_id'}}
    if qos_network_policy_id:
        retval['qos_network_policy_binding'] = {
            'policy_id': qos_network_policy_id}
    return retval


class QosCoreResourceExtensionTestCase(base.BaseTestCase):

    def setUp(self):
        super(QosCoreResourceExtensionTestCase, self).setUp()
        self.core_extension = qos_core.QosCoreResourceExtension()
        policy_p = mock.patch('neutron.objects.qos.policy.QosPolicy')
        self.policy_m = policy_p.start()
        self.context = context.get_admin_context()
        self.non_admin_context = context.Context('user_id', 'tenant_id')

    def test_process_fields_no_qos_policy_id(self):
        self.core_extension.process_fields(
            self.context, base_core.PORT, mock.ANY, {}, None)
        self.assertFalse(self.policy_m.called)

    def _mock_plugin_loaded(self, plugin_loaded):
        plugins = {}
        if plugin_loaded:
            plugins[plugin_constants.QOS] = None
        return mock.patch('neutron_lib.plugins.directory.get_plugins',
                          return_value=plugins)

    def test_process_fields_no_qos_plugin_loaded(self):
        with self._mock_plugin_loaded(False):
            self.core_extension.process_fields(
                self.context, base_core.PORT, mock.ANY,
                {qos_consts.QOS_POLICY_ID: None}, None)
            self.assertFalse(self.policy_m.called)

    def test_process_fields_port_new_policy(self):
        with self._mock_plugin_loaded(True):
            qos_policy_id = mock.Mock()
            actual_port = {'id': mock.Mock(),
                           qos_consts.QOS_POLICY_ID: qos_policy_id}
            qos_policy = mock.MagicMock()
            self.policy_m.get_policy_obj = mock.Mock(return_value=qos_policy)
            self.core_extension.process_fields(
                self.context, base_core.PORT, base_core.EVENT_UPDATE,
                {qos_consts.QOS_POLICY_ID: qos_policy_id},
                actual_port)

            qos_policy.attach_port.assert_called_once_with(actual_port['id'])

    def test_process_fields_port_updated_policy(self):
        with self._mock_plugin_loaded(True):
            qos_policy1_id = mock.Mock()
            qos_policy2_id = mock.Mock()
            port_id = mock.Mock()
            actual_port = {'id': port_id,
                           qos_consts.QOS_POLICY_ID: qos_policy1_id}
            old_qos_policy = mock.MagicMock()
            self.policy_m.get_port_policy = mock.Mock(
                return_value=old_qos_policy)
            new_qos_policy = mock.MagicMock()
            self.policy_m.get_policy_obj = mock.Mock(
                return_value=new_qos_policy)
            self.core_extension.process_fields(
                self.context, base_core.PORT, base_core.EVENT_UPDATE,
                {qos_consts.QOS_POLICY_ID: qos_policy2_id},
                actual_port)

            old_qos_policy.detach_port.assert_called_once_with(port_id)
            new_qos_policy.attach_port.assert_called_once_with(port_id)
            self.assertEqual(qos_policy2_id, actual_port['qos_policy_id'])

    def test_process_resource_port_updated_no_policy(self):
        with self._mock_plugin_loaded(True):
            port_id = mock.Mock()
            qos_policy_id = mock.Mock()
            actual_port = {'id': port_id,
                           qos_consts.QOS_POLICY_ID: qos_policy_id}
            old_qos_policy = mock.MagicMock()
            self.policy_m.get_port_policy = mock.Mock(
                return_value=old_qos_policy)
            new_qos_policy = mock.MagicMock()
            self.policy_m.get_object = mock.Mock(return_value=new_qos_policy)
            self.core_extension.process_fields(
                self.context, base_core.PORT, base_core.EVENT_UPDATE,
                {qos_consts.QOS_POLICY_ID: None},
                actual_port)

            old_qos_policy.detach_port.assert_called_once_with(port_id)
            self.assertIsNone(actual_port['qos_policy_id'])

    def _process_port_updated_policy(self, context, shared,
                                     policy_tenant_id):
        with self._mock_plugin_loaded(True):
            port_id = mock.sentinel.port_id
            qos_policy_id = mock.sentinel.policy_id
            actual_port = {'id': port_id,
                           qos_consts.QOS_POLICY_ID: qos_policy_id}
            old_qos_policy = mock.MagicMock()
            old_qos_policy.shared = shared
            old_qos_policy.tenant_id = policy_tenant_id
            self.policy_m.get_port_policy = mock.Mock(
                return_value=old_qos_policy)
            self.core_extension.process_fields(
                context, base_core.PORT, base_core.EVENT_UPDATE,
                {qos_consts.QOS_POLICY_ID: None},
                actual_port)

            old_qos_policy.detach_port.assert_called_once_with(port_id)

    def test_process_resource_port_updated_remove_own_policy(self):
        self._process_port_updated_policy(
            context=self.non_admin_context,
            shared=False,
            policy_tenant_id=self.non_admin_context.tenant_id)

    def test_process_resource_port_updated_admin_remove_provided_policy(self):
        self._process_port_updated_policy(
            context=self.context,
            shared=False,
            policy_tenant_id=self.non_admin_context.tenant_id)

    def test_process_resource_port_updated_remove_shared_policy(self):
        self._process_port_updated_policy(
            context=self.non_admin_context,
            shared=True,
            policy_tenant_id=self.context.tenant_id)

    def test_process_resource_port_updated_remove_provided_policy(self):
        self.policy_m.is_accessible.return_value = False
        self.assertRaises(qos_exc.PolicyRemoveAuthorizationError,
                          self._process_port_updated_policy,
                          context=self.non_admin_context,
                          shared=False,
                          policy_tenant_id=self.context.tenant_id)

    def test_process_resource_update_network_updated_no_policy(self):
        with self._mock_plugin_loaded(True):
            network_id = mock.Mock()
            qos_policy_id = mock.Mock()
            actual_network = {'id': network_id,
                              qos_consts.QOS_POLICY_ID: qos_policy_id}
            old_qos_policy = mock.MagicMock()
            self.policy_m.get_network_policy = mock.Mock(
                return_value=old_qos_policy)
            new_qos_policy = mock.MagicMock()
            self.policy_m.get_object = mock.Mock(return_value=new_qos_policy)
            self.core_extension.process_fields(
                self.context, base_core.NETWORK, base_core.EVENT_UPDATE,
                {qos_consts.QOS_POLICY_ID: None},
                actual_network)

            old_qos_policy.detach_network.assert_called_once_with(network_id)
            self.assertIsNone(actual_network['qos_policy_id'])

    def test_process_fields_update_network_new_policy(self):
        with self._mock_plugin_loaded(True):
            qos_policy_id = mock.Mock()
            actual_network = {'id': mock.Mock(),
                              qos_consts.QOS_POLICY_ID: qos_policy_id}
            qos_policy = mock.MagicMock()
            self.policy_m.get_policy_obj = mock.Mock(return_value=qos_policy)
            self.core_extension.process_fields(
                self.context, base_core.NETWORK, base_core.EVENT_UPDATE,
                {qos_consts.QOS_POLICY_ID: qos_policy_id}, actual_network)

            qos_policy.attach_network.assert_called_once_with(
                actual_network['id'])

    def test_process_fields_update_network_updated_policy(self):
        with self._mock_plugin_loaded(True):
            qos_policy_id = mock.Mock()
            network_id = mock.Mock()
            actual_network = {'id': network_id,
                              qos_consts.QOS_POLICY_ID: qos_policy_id}
            old_qos_policy = mock.MagicMock()
            self.policy_m.get_network_policy = mock.Mock(
                return_value=old_qos_policy)
            new_qos_policy = mock.MagicMock()
            self.policy_m.get_policy_obj = mock.Mock(
                return_value=new_qos_policy)
            self.core_extension.process_fields(
                self.context, base_core.NETWORK, base_core.EVENT_UPDATE,
                {qos_consts.QOS_POLICY_ID: qos_policy_id}, actual_network)

            old_qos_policy.detach_network.assert_called_once_with(network_id)
            new_qos_policy.attach_network.assert_called_once_with(network_id)

    def _process_network_updated_policy(self, context, shared,
                                        policy_tenant_id):
        with self._mock_plugin_loaded(True):
            qos_policy_id = mock.sentinel.policy_id
            network_id = mock.sentinel.net_id
            actual_network = {'id': network_id,
                              qos_consts.QOS_POLICY_ID: qos_policy_id}
            old_qos_policy = mock.MagicMock()
            old_qos_policy.shared = shared
            old_qos_policy.tenant_id = policy_tenant_id
            self.policy_m.get_network_policy.return_value = old_qos_policy
            self.core_extension.process_fields(
                context, base_core.NETWORK, base_core.EVENT_UPDATE,
                {qos_consts.QOS_POLICY_ID: None}, actual_network)

            old_qos_policy.detach_network.assert_called_once_with(network_id)

    def test_process_fields_update_network_updated_remove_shared_policy(self):
        self._process_network_updated_policy(
            context=self.non_admin_context,
            shared=True,
            policy_tenant_id=self.context.tenant_id)

    def test_process_fields_network_updated_remove_own_policy(self):
        self._process_network_updated_policy(
            context=self.non_admin_context,
            shared=True,
            policy_tenant_id=self.non_admin_context.tenant_id)

    def test_process_fields_update_network_admin_remove_provided_policy(self):
        self._process_network_updated_policy(
            context=self.context,
            shared=True,
            policy_tenant_id=self.non_admin_context.tenant_id)

    def test_process_fields_update_network_remove_provided_policy(self):
        self.policy_m.is_accessible.return_value = False
        self.assertRaises(qos_exc.PolicyRemoveAuthorizationError,
                          self._process_network_updated_policy,
                          context=self.non_admin_context,
                          shared=False,
                          policy_tenant_id=self.context.tenant_id)

    def test_process_fields_create_network(self):
        with self._mock_plugin_loaded(True):
            qos_policy_id = mock.Mock()
            network_id = mock.Mock()
            actual_network = {'id': network_id,
                              qos_consts.QOS_POLICY_ID: qos_policy_id}
            self.policy_m.get_network_policy = mock.Mock(
                return_value=qos_policy_id)
            qos_policy = mock.MagicMock()
            self.policy_m.get_policy_obj = mock.Mock(return_value=qos_policy)
            self.core_extension.process_fields(
                self.context, base_core.NETWORK, base_core.EVENT_CREATE,
                actual_network, actual_network)
            qos_policy.attach_network.assert_called_once_with(network_id)

    def test_process_fields_create_network_no_policy(self):
        with self._mock_plugin_loaded(True):
            project_id = mock.Mock()
            network_id = mock.Mock()
            actual_network = {'project_id': project_id,
                              'id': network_id,
                              qos_consts.QOS_POLICY_ID: None}
            qos_policy_id = mock.Mock()
            qos_policy = mock.MagicMock()
            with mock.patch.object(policy.QosPolicyDefault, "get_object",
                    return_value=qos_policy_id) as mock_get_default_policy_id:
                self.policy_m.get_policy_obj = mock.Mock(
                    return_value=qos_policy)
                self.core_extension.process_fields(
                    self.context, base_core.NETWORK, base_core.EVENT_CREATE,
                    actual_network, actual_network)
                qos_policy.attach_network.assert_called_once_with(network_id)
                mock_get_default_policy_id.assert_called_once_with(
                    self.context, project_id=project_id)

    def test_process_fields_create_network_no_default_policy(self):
        with self._mock_plugin_loaded(True):
            project_id = mock.Mock()
            network_id = mock.Mock()
            actual_network = {'project_id': project_id,
                              'id': network_id,
                              qos_consts.QOS_POLICY_ID: None}
            qos_policy = mock.MagicMock()
            with mock.patch.object(policy.QosPolicyDefault, "get_object",
                    return_value=None) as mock_get_default_policy_id:
                self.policy_m.get_object = mock.Mock(return_value=qos_policy)
                self.core_extension.process_fields(
                    self.context, base_core.NETWORK, base_core.EVENT_CREATE,
                    actual_network, actual_network)
                qos_policy.attach_network.assert_not_called()
                mock_get_default_policy_id.assert_called_once_with(
                    self.context, project_id=project_id)

    def test_extract_fields_plugin_not_loaded(self):
        with self._mock_plugin_loaded(False):
            fields = self.core_extension.extract_fields(None, None)
            self.assertEqual({}, fields)

    def _test_extract_fields_for_port(self, qos_policy_id,
                                      qos_network_policy_id=None):
        with self._mock_plugin_loaded(True):
            fields = self.core_extension.extract_fields(
                base_core.PORT, _get_test_dbdata(qos_policy_id,
                                                 qos_network_policy_id))
            expected = {
                qos_consts.QOS_POLICY_ID: qos_policy_id,
                qos_consts.QOS_NETWORK_POLICY_ID: qos_network_policy_id}
            self.assertEqual(expected, fields)

    def test_extract_fields_for_port(self):
        port_qos_policies = [None, uuidutils.generate_uuid()]
        network_qos_policies = [None, uuidutils.generate_uuid()]
        for port_qos, net_qos in itertools.product(port_qos_policies,
                                                   network_qos_policies):
            self._test_extract_fields_for_port(port_qos, net_qos)

    def _test_extract_fields_for_network(self, qos_policy_id):
        with self._mock_plugin_loaded(True):
            fields = self.core_extension.extract_fields(
                base_core.NETWORK, _get_test_dbdata(qos_policy_id))
            self.assertEqual({qos_consts.QOS_POLICY_ID: qos_policy_id}, fields)

    def test_extract_fields_no_network_policy(self):
        self._test_extract_fields_for_network(None)

    def test_extract_fields_network_policy_exists(self):
        qos_policy_id = mock.Mock()
        qos_policy = mock.Mock()
        qos_policy.id = qos_policy_id
        self._test_extract_fields_for_network(qos_policy_id)

    def test__create_network_policy(self):
        default_policy_id = uuidutils.generate_uuid()
        network_policy_id = uuidutils.generate_uuid()
        policy_mock = mock.MagicMock(qos_policy_id=default_policy_id)
        network_changes = mock.Mock()
        network = {'id': 'dummy_id', 'project_id': 'dummy_project',
                   qos_consts.QOS_POLICY_ID: None}
        with mock.patch.object(policy.QosPolicyDefault, 'get_object',
                               return_value=policy_mock),\
                mock.patch.object(policy.QosPolicy, 'get_object'):
            # Creating network with policy id
            network_changes.get.return_value = network_policy_id
            self.core_extension._create_network_policy(
                self.context, network, network_changes)
            self.assertEqual(network_policy_id,
                             network[qos_consts.QOS_POLICY_ID])
            # Creating network without policy id
            network_changes.get.return_value = None
            self.core_extension._create_network_policy(
                self.context, network, network_changes)
            self.assertEqual(default_policy_id,
                             network[qos_consts.QOS_POLICY_ID])
