# Copyright (c) 2018 Fujitsu Limited
# 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.

from unittest import mock

from neutron.objects import ports as port_objects
from neutron.services.logapi.drivers import base as log_driver_base
from neutron.services.logapi.drivers import manager as driver_mgr
from neutron.tests import base
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib import constants as nl_const
from oslo_utils import uuidutils

from neutron_fwaas.services.logapi.common import log_db_api
from neutron_fwaas.services.logapi.common import port_callback

FAKE_DRIVER = None


class FakeDriver(log_driver_base.DriverBase):

    @staticmethod
    def create():
        return FakeDriver(
            name='fake_driver',
            vif_types=[],
            vnic_types=[],
            supported_logging_types=['firewall_group'],
            requires_rpc=True
        )


def fake_register():
    global FAKE_DRIVER
    if not FAKE_DRIVER:
        FAKE_DRIVER = FakeDriver.create()
    driver_mgr.register(resources.PORT, port_callback.NeutronPortCallBack)


class TestFirewallGroupRuleCallback(base.BaseTestCase):

    def setUp(self):
        super().setUp()
        self.driver_manager = driver_mgr.LoggingServiceDriverManager()
        self.port_callback = port_callback.NeutronPortCallBack(mock.Mock(),
                                                               mock.Mock())
        self.m_context = mock.Mock()

    def _create_port_object(self, name=None, device_owner=None,
                            status=nl_const.PORT_STATUS_ACTIVE):
        port_data = {
            'id': uuidutils.generate_uuid(),
            'project_id': 'fake_tenant_id',
            'status': status
        }
        if name:
            port_data['name'] = name
        if device_owner:
            port_data['device_owner'] = device_owner
        return port_objects.Port(**port_data)

    @mock.patch.object(port_callback.NeutronPortCallBack, 'handle_event')
    def test_handle_event(self, m_port_cb_handler):
        fake_register()
        self.driver_manager.register_driver(FAKE_DRIVER)
        payload = events.DBEventPayload(None)
        registry.publish(resources.PORT, events.AFTER_CREATE, mock.ANY,
                         payload)
        m_port_cb_handler.assert_called_once_with(
            resources.PORT, events.AFTER_CREATE, mock.ANY, payload=payload)

        m_port_cb_handler.reset_mock()
        registry.publish(
            resources.PORT, events.AFTER_UPDATE, mock.ANY, payload)
        m_port_cb_handler.assert_called_once_with(
            resources.PORT, events.AFTER_UPDATE, mock.ANY, payload=payload)

        m_port_cb_handler.reset_mock()
        registry.publish(
            'non_registered_resource', events.AFTER_CREATE, mock.ANY)
        m_port_cb_handler.assert_not_called()

        m_port_cb_handler.reset_mock()
        registry.publish(
            'non_registered_resource', events.AFTER_UPDATE, mock.ANY)
        m_port_cb_handler.assert_not_called()

    def test_trigger_logging(self):
        fake_log_obj = mock.Mock()
        self.port_callback.resource_push_api = mock.Mock()
        port = self._create_port_object(device_owner='fake_device_owner')

        # Test with log resource could be found from DB
        with mock.patch.object(log_db_api, 'get_logs_for_port',
                               return_value=[fake_log_obj]):
            self.port_callback.trigger_logging(self.m_context, port)
            self.port_callback.resource_push_api.assert_called()

        # Test with log resource could not be found from DB
        self.port_callback.resource_push_api.reset_mock()
        with mock.patch.object(log_db_api, 'get_logs_for_port',
                               return_value=[]):
            self.port_callback.trigger_logging(self.m_context, port)
            self.port_callback.resource_push_api.assert_not_called()

    def test_handle_event_with_router_port(self):
        with mock.patch.object(self.port_callback, 'trigger_logging'):
            # Test for router port enabling
            payload = self._fake_port_config(
                nl_const.DEVICE_OWNER_ROUTER_INTF, action='enable')
            self.port_callback.handle_event(mock.ANY,
                                            events.AFTER_UPDATE,
                                            mock.ANY,
                                            payload=payload)
            self.port_callback.trigger_logging.assert_called()

            # Test for router port disabling
            self.port_callback.trigger_logging.reset_mock()
            payload = self._fake_port_config(
                nl_const.DEVICE_OWNER_ROUTER_INTF, action='disable')
            self.port_callback.handle_event(mock.ANY,
                                            events.AFTER_UPDATE,
                                            mock.ANY,
                                            payload=payload)
            self.port_callback.trigger_logging.assert_called()

            # Test for router port status does not change
            self.port_callback.trigger_logging.reset_mock()
            payload = \
                self._fake_port_config(nl_const.DEVICE_OWNER_ROUTER_INTF)
            self.port_callback.handle_event(mock.ANY,
                                            events.AFTER_UPDATE,
                                            mock.ANY,
                                            payload=payload)
            self.port_callback.trigger_logging.assert_not_called()

    def test_handle_event_with_non_router_port(self):
        with mock.patch.object(self.port_callback, 'trigger_logging'):
            # Test for port enabling
            payload = self._fake_port_config('fake_port_type',
                                             action='enable')
            self.port_callback.handle_event(mock.ANY,
                                            events.AFTER_UPDATE,
                                            mock.ANY,
                                            payload=payload)
            self.port_callback.trigger_logging.assert_not_called()

            # Test for port disabling
            self.port_callback.trigger_logging.reset_mock()
            payload = self._fake_port_config('fake_port_type',
                                             action='disable')
            self.port_callback.handle_event(mock.ANY,
                                            events.AFTER_UPDATE,
                                            mock.ANY,
                                            payload=payload)
            self.port_callback.trigger_logging.assert_not_called()

    def _fake_port_config(self, device_owner, action=None):
        if action == 'enable':
            # Create original port with DOWN status
            original_port = self._create_port_object(
                device_owner=device_owner, status=nl_const.PORT_STATUS_DOWN)

            # Create port with ACTIVE status
            port = self._create_port_object(
                device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE)
        elif action == 'disable':
            # Create original port with ACTIVE status
            original_port = self._create_port_object(
                device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE)

            # Create port with DOWN status
            port = self._create_port_object(
                device_owner=device_owner, status=nl_const.PORT_STATUS_DOWN)
        else:
            # Create original port with ACTIVE status
            original_port = self._create_port_object(
                device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE)

            # Create port with ACTIVE status
            port = self._create_port_object(
                device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE)
        payload = events.DBEventPayload(self.m_context,
                                        states=[original_port, port])
        return payload
