# Copyright (c) 2017 OpenStack Foundation.
#
# 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_config import cfg
from oslo_utils import uuidutils

from neutron.objects import subnet as subnet_obj
from neutron_lib import context
from neutron_lib import exceptions as n_exc
from neutron_lib.plugins import directory

from vmware_nsx.tests.unit.dvs import test_plugin as dvs_tests
from vmware_nsx.tests.unit.nsx_v import test_plugin as v_tests
from vmware_nsx.tests.unit.nsx_v3 import test_plugin as t_tests

PLUGIN_NAME = 'vmware_nsx.plugin.NsxTVDPlugin'
_uuid = uuidutils.generate_uuid


class NsxTVDPluginTestCase(v_tests.NsxVPluginV2TestCase,
                           t_tests.NsxV3PluginTestCaseMixin,
                           dvs_tests.NeutronSimpleDvsTestCase):

    def setUp(self,
              plugin=PLUGIN_NAME,
              ext_mgr=None,
              service_plugins=None):

        # set the default plugin
        if self.plugin_type:
            cfg.CONF.set_override('default_plugin', self.plugin_type,
                                  group="nsx_tvd")

        # set the default availability zones
        cfg.CONF.set_override('nsx_v_default_availability_zones',
                              ['default'],
                              group="nsx_tvd")
        cfg.CONF.set_override('nsx_v3_default_availability_zones',
                              ['defaultv3'],
                              group="nsx_tvd")

        super(NsxTVDPluginTestCase, self).setUp(
            plugin=plugin,
            ext_mgr=ext_mgr,
            with_md_proxy=False)
        self._project_id = _uuid()
        self.core_plugin = directory.get_plugin()

        # create a context with this tenant
        self.context = context.get_admin_context()
        self.context.tenant_id = self.project_id

        # create a default user for this plugin
        self.core_plugin.create_project_plugin_map(self.context,
                {'project_plugin_map': {'plugin': self.plugin_type,
                                        'project': self.project_id}})
        self.sub_plugin = self.core_plugin.get_plugin_by_type(self.plugin_type)

    @property
    def project_id(self):
        return self._project_id

    @property
    def plugin_type(self):
        pass

    def _test_plugin_initialized(self):
        self.assertTrue(self.core_plugin.is_tvd_plugin())
        self.assertIsNotNone(self.sub_plugin)

    def _test_call_create(self, obj_name, calls_count=1, project_id=None,
                          is_bulk=False):
        method_name = single_name = 'create_%s' % obj_name
        if is_bulk:
            method_name = method_name + '_bulk'
        func_to_call = getattr(self.core_plugin, method_name)
        if not project_id:
            project_id = self.project_id
        with mock.patch.object(self.sub_plugin, method_name) as sub_func,\
            mock.patch.object(self.sub_plugin, single_name) as single_func:
            if is_bulk:
                func_to_call(self.context,
                             {obj_name + 's': [{obj_name:
                                                {'tenant_id': project_id}}]})
            else:
                func_to_call(self.context,
                             {obj_name: {'tenant_id': project_id}})
            self.assertEqual(calls_count,
                             sub_func.call_count or single_func.call_count)

    def _test_call_create_with_net_id(self, obj_name, field_name='network_id',
                                      calls_count=1, is_bulk=False):
        method_name = 'create_%s' % obj_name
        if is_bulk:
            method_name = method_name + '_bulk'
        func_to_call = getattr(self.core_plugin, method_name)
        net_id = _uuid()

        with mock.patch.object(self.sub_plugin, method_name) as sub_func,\
            mock.patch.object(self.core_plugin, '_get_network',
                              return_value={'tenant_id': self.project_id}):
            if is_bulk:
                func_to_call(self.context,
                             {obj_name + 's': [{obj_name:
                                                {'tenant_id': self.project_id,
                                                 field_name: net_id}}]})
            else:
                func_to_call(self.context,
                             {obj_name: {'tenant_id': self.project_id,
                                         field_name: net_id}})
            self.assertEqual(calls_count, sub_func.call_count)

    def _test_call_delete(self, obj_name):
        method_name = 'delete_%s' % obj_name
        func_to_call = getattr(self.core_plugin, method_name)
        obj_id = _uuid()
        with mock.patch.object(self.sub_plugin, method_name) as sub_func,\
            mock.patch.object(self.core_plugin, '_get_%s' % obj_name,
                              return_value={'tenant_id': self.project_id}):
            func_to_call(self.context, obj_id)
            sub_func.assert_called_once()

    def _test_call_delete_with_net(self, obj_name, field_name='network_id'):
        method_name = 'delete_%s' % obj_name
        func_to_call = getattr(self.core_plugin, method_name)
        obj_id = _uuid()
        net_id = _uuid()
        if obj_name == 'subnet':
            mock_name = '_get_subnet_object'
            ret_val = subnet_obj.Subnet(network_id=net_id)
        else:
            mock_name = '_get_%s' % obj_name
            ret_val = {field_name: net_id}
        with mock.patch.object(self.sub_plugin, method_name) as sub_func,\
            mock.patch.object(self.core_plugin, mock_name,
                              return_value=ret_val),\
            mock.patch.object(self.core_plugin, '_get_network',
                              return_value={'tenant_id': self.project_id}):
            func_to_call(self.context, obj_id)
            sub_func.assert_called_once()

    def _test_call_update(self, obj_name):
        method_name = 'update_%s' % obj_name
        func_to_call = getattr(self.core_plugin, method_name)
        obj_id = _uuid()
        with mock.patch.object(self.sub_plugin, method_name) as sub_func,\
            mock.patch.object(self.core_plugin, '_get_%s' % obj_name,
                              return_value={'tenant_id': self.project_id}):
            func_to_call(self.context, obj_id, {obj_name: {}})
            sub_func.assert_called_once()

    def _test_call_update_with_net(self, obj_name, field_name='network_id'):
        method_name = 'update_%s' % obj_name
        func_to_call = getattr(self.core_plugin, method_name)
        obj_id = _uuid()
        net_id = _uuid()
        if obj_name == 'subnet':
            mock_name = '_get_subnet_object'
            ret_val = subnet_obj.Subnet(network_id=net_id)
        else:
            mock_name = '_get_%s' % obj_name
            ret_val = {field_name: net_id}
        with mock.patch.object(self.sub_plugin, method_name) as sub_func,\
            mock.patch.object(self.core_plugin, mock_name,
                              return_value=ret_val),\
            mock.patch.object(self.core_plugin, '_get_network',
                              return_value={'tenant_id': self.project_id}):
            func_to_call(self.context, obj_id, {obj_name: {}})
            sub_func.assert_called_once()

    def _test_call_get(self, obj_name):
        method_name = 'get_%s' % obj_name
        func_to_call = getattr(self.core_plugin, method_name)
        obj_id = _uuid()
        with mock.patch.object(self.sub_plugin, method_name) as sub_func,\
            mock.patch.object(self.core_plugin, '_get_%s' % obj_name,
                              return_value={'tenant_id': self.project_id}):
            func_to_call(self.context, obj_id)
            sub_func.assert_called_once()

    def _test_call_get_with_net(self, obj_name, field_name='network_id'):
        method_name = 'get_%s' % obj_name
        func_to_call = getattr(self.core_plugin, method_name)
        obj_id = _uuid()
        net_id = _uuid()
        if obj_name == 'subnet':
            mock_name = '_get_subnet_object'
            ret_val = subnet_obj.Subnet(network_id=net_id)
        else:
            mock_name = '_get_%s' % obj_name
            ret_val = {field_name: net_id}
        with mock.patch.object(self.sub_plugin, method_name) as sub_func,\
            mock.patch.object(self.core_plugin, mock_name,
                              return_value=ret_val),\
            mock.patch.object(self.core_plugin, '_get_network',
                              return_value={'tenant_id': self.project_id}):
            func_to_call(self.context, obj_id)
            sub_func.assert_called_once()


class TestPluginWithDefaultPlugin(NsxTVDPluginTestCase):
    """Test TVD plugin with the NSX-T (default) sub plugin"""

    @property
    def plugin_type(self):
        return 'nsx-t'

    def test_plugin_initialized(self):
        self._test_plugin_initialized()

        # no unsupported extensions for the nsx_t plugin
        self.assertCountEqual(
            ['router_type', 'router_size'],
            self.core_plugin._unsupported_fields[self.plugin_type]['router'])
        self.assertEqual(
            [],
            self.core_plugin._unsupported_fields[self.plugin_type]['port'])

    def test_create_network(self):
        self._test_call_create('network')

    def test_create_subnet(self):
        self._test_call_create_with_net_id('subnet')

    def test_create_port(self):
        self._test_call_create_with_net_id('port')

    def test_create_router(self):
        self._test_call_create('router')

    def test_create_floatingip(self):
        self._test_call_create_with_net_id(
            'floatingip', field_name='floating_network_id')

    def test_create_security_group(self):
        # plugin will be called twice because of the default sg
        self._test_call_create('security_group', calls_count=2)

    def test_create_security_group_rule(self):
        self._test_call_create('security_group_rule')

    def test_create_network_bulk(self):
        self._test_call_create('network', is_bulk=True)

    def test_create_subnet_bulk(self):
        self._test_call_create_with_net_id('subnet', is_bulk=True)

    def test_create_security_group_rule_bulk(self):
        self._test_call_create('security_group_rule', is_bulk=True)

    def test_delete_network(self):
        self._test_call_delete('network')

    def test_delete_subnet(self):
        self._test_call_delete_with_net('subnet')

    def test_delete_port(self):
        self._test_call_delete_with_net('port')

    def test_delete_router(self):
        self._test_call_delete('router')

    def test_delete_floatingip(self):
        self._test_call_delete_with_net(
            'floatingip', field_name='floating_network_id')

    def test_delete_security_group(self):
        self._test_call_delete('security_group')

    def test_update_network(self):
        self._test_call_update('network')

    def test_update_subnet(self):
        self._test_call_update_with_net('subnet')

    def test_update_port(self):
        self._test_call_update_with_net('port')

    def test_update_router(self):
        self._test_call_update('router')

    def test_update_floatingip(self):
        self._test_call_update_with_net(
            'floatingip', field_name='floating_network_id')

    def test_update_security_group(self):
        self._test_call_update('security_group')

    def test_unsupported_extensions(self):
        self.assertRaises(n_exc.InvalidInput,
            self.core_plugin.create_router,
            self.context,
            {'router': {'tenant_id': self.project_id,
                        'router_type': 'exclusive'}})

    def test_get_network(self):
        self._test_call_get('network')

    def test_get_subnet(self):
        self._test_call_get_with_net('subnet')

    def test_get_port(self):
        self._test_call_get_with_net('port')

    def test_get_router(self):
        self._test_call_get('router')

    def test_get_floatingip(self):
        self._test_call_get_with_net(
            'floatingip', field_name='floating_network_id')

    def test_get_security_group(self):
        self._test_call_get('security_group')

    def test_add_router_interface(self):
        rtr_id = _uuid()
        port_id = _uuid()
        net_id = _uuid()
        with mock.patch.object(self.sub_plugin,
                               'add_router_interface') as sub_func,\
            mock.patch.object(self.core_plugin, '_get_router',
                              return_value={'tenant_id': self.project_id}),\
            mock.patch.object(self.core_plugin, '_get_port',
                              return_value={'network_id': net_id}),\
            mock.patch.object(self.core_plugin, '_get_network',
                              return_value={'tenant_id': self.project_id}),\
            mock.patch.object(self.core_plugin, '_validate_interface_info',
                              return_value=(True, False)):
            self.core_plugin.add_router_interface(self.context, rtr_id,
                                                  {'port_id': port_id})
            sub_func.assert_called_once()

    def test_add_invalid_router_interface(self):
        # Test that the plugin prevents adding interface from one plugin
        # to a router of another plugin
        rtr_id = _uuid()
        port_id = _uuid()
        net_id = _uuid()
        another_tenant_id = _uuid()
        another_plugin = 'nsx-v' if self.plugin_type == 'nsx-t' else 'nsx-t'
        self.core_plugin.create_project_plugin_map(self.context,
                {'project_plugin_map': {'plugin': another_plugin,
                                        'project': another_tenant_id}})

        with mock.patch.object(self.core_plugin, '_get_router',
                               return_value={'tenant_id': self.project_id}),\
            mock.patch.object(self.core_plugin, '_get_port',
                              return_value={'network_id': net_id}),\
            mock.patch.object(self.core_plugin, '_get_network',
                              return_value={'tenant_id': another_tenant_id}),\
            mock.patch.object(self.core_plugin, '_validate_interface_info',
                              return_value=(True, False)):
            self.assertRaises(n_exc.InvalidInput,
                              self.core_plugin.add_router_interface,
                              self.context, rtr_id, {'port_id': port_id})

    def test_remove_router_interface(self):
        rtr_id = _uuid()
        with mock.patch.object(self.sub_plugin,
                               'remove_router_interface') as sub_func,\
            mock.patch.object(self.core_plugin, '_get_router',
                              return_value={'tenant_id': self.project_id}):
            self.core_plugin.remove_router_interface(self.context, rtr_id, {})
            sub_func.assert_called_once()

    def test_disassociate_floatingips(self):
        port_id = _uuid()
        net_id = _uuid()
        with mock.patch.object(self.sub_plugin,
                               'disassociate_floatingips') as sub_func,\
            mock.patch.object(self.core_plugin, '_get_port',
                              return_value={'network_id': net_id}),\
            mock.patch.object(self.core_plugin, '_get_network',
                              return_value={'tenant_id': self.project_id}):
            self.core_plugin.disassociate_floatingips(self.context, port_id)
            sub_func.assert_called_once()

    def test_new_user(self):
        project_id = _uuid()
        self._test_call_create('network', project_id=project_id)


class TestPluginWithNsxv(TestPluginWithDefaultPlugin):
    """Test TVD plugin with the NSX-V sub plugin"""

    @property
    def plugin_type(self):
        return 'nsx-v'

    def test_plugin_initialized(self):
        self._test_plugin_initialized()

        # no unsupported extensions for the nsx_v plugin
        self.assertEqual(
            [],
            self.core_plugin._unsupported_fields[self.plugin_type]['router'])
        self.assertEqual(
            [],
            self.core_plugin._unsupported_fields[self.plugin_type]['port'])

    def test_unsupported_extensions(self):
        self.skipTest('No unsupported extensions in this plugin')


class TestPluginWithDvs(TestPluginWithDefaultPlugin):
    """Test TVD plugin with the DVS sub plugin"""

    @property
    def plugin_type(self):
        return 'dvs'

    def test_plugin_initialized(self):
        self._test_plugin_initialized()

        # no unsupported extensions for the dvs plugin
        self.assertCountEqual(
            ['mac_learning_enabled', 'provider_security_groups'],
            self.core_plugin._unsupported_fields[self.plugin_type]['port'])

    def test_unsupported_extensions(self):
        net_id = _uuid()
        with mock.patch.object(self.core_plugin, '_get_network',
                               return_value={'tenant_id': self.project_id}):
            self.assertRaises(n_exc.InvalidInput,
                self.core_plugin.create_port,
                self.context,
                {'port': {'tenant_id': self.project_id,
                          'network_id': net_id,
                          'mac_learning_enabled': True}})
