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

import netaddr
from oslo_utils import uuidutils

from neutron.objects import port_forwarding
from neutron.objects import router
from neutron.tests import tools
from neutron.tests.unit.objects import test_base as obj_test_base
from neutron.tests.unit import testlib_api


class PortForwardingObjectTestCase(obj_test_base.BaseObjectIfaceTestCase):

    _test_class = port_forwarding.PortForwarding

    def setUp(self):
        super(PortForwardingObjectTestCase, self).setUp()
        # 'portforwardings' table will store the 'internal_ip_address' and
        # 'internal_port' as a single 'socket' column.
        # Port forwarding object accepts 'internal_ip_address' and
        # 'internal_port', but can not filter the records in db, so the
        # valid filters can not contain them.
        not_supported_filter_fields = ['internal_ip_address', 'internal_port']
        invalid_fields = set(
            self._test_class.synthetic_fields).union(
            set(not_supported_filter_fields))
        self.valid_field = [f for f in self._test_class.fields
                            if f not in invalid_fields][0]

        self.mock_load_fip = mock.patch.object(
            self._test_class, '_load_attr_from_fip', autospec=True,
            side_effect=self._mock_load_attr_from_fip).start()

        for obj_fields in self.obj_fields:
            obj_fields['floating_ip_address'] = tools.get_random_ip_address(4)
            obj_fields['router_id'] = uuidutils.generate_uuid()

    def _mock_load_attr_from_fip(self, fp_obj, attrname):
        def random_generate_fip_db(fip_id):
            fip_fields = self.get_random_db_fields(router.FloatingIP)
            fip_fields['id'] = fip_id
            fip_fields['floating_ip_address'] = tools.get_random_ip_address(4)
            return router.FloatingIP.db_model(**fip_fields)

        if not fp_obj.db_obj:
            fp_db_attrs = {
                'floatingip_id': fp_obj.floatingip_id,
                'external_port': fp_obj.external_port,
                'internal_neutron_port_id': fp_obj.internal_port_id,
                'protocol': fp_obj.protocol,
                'socket': fp_obj.internal_port,
                'floating_ip': random_generate_fip_db(fp_obj.floatingip_id)
            }
            fp_obj._captured_db_model = (
                port_forwarding.PortForwarding.db_model(**fp_db_attrs))

        if not fp_obj.db_obj.floating_ip:
            fp_obj.db_obj.floating_ip = random_generate_fip_db(
                fp_obj.floatingip_id)

        # From PortForwarding._load_attr_from_fip
        value = getattr(fp_obj.db_obj.floating_ip, attrname)
        setattr(self, attrname, value)
        fp_obj.obj_reset_changes([attrname])


class PortForwardingDbObjectTestCase(obj_test_base.BaseDbObjectTestCase,
                                     testlib_api.SqlTestCase):

    _test_class = port_forwarding.PortForwarding

    def setUp(self):
        super(PortForwardingDbObjectTestCase, self).setUp()
        self.update_obj_fields(
            {'floatingip_id':
                lambda: self._create_test_fip_id_for_port_forwarding(),
             'internal_port_id': lambda: self._create_test_port_id()})
        # 'portforwardings' table will store the 'internal_ip_address' and
        # 'internal_port' as a single 'socket' column.
        # Port forwarding object accepts 'internal_ip_address' and
        # 'internal_port', but can not filter the records in db, so the
        # valid filters can not contain them.
        not_supported_filter_fields = ['internal_ip_address', 'internal_port']
        invalid_fields = set(
            self._test_class.synthetic_fields).union(
            set(not_supported_filter_fields))
        self.valid_field = [f for f in self._test_class.fields
                            if f not in invalid_fields][0]
        self.valid_field_filter = {self.valid_field:
                                   self.obj_fields[-1][self.valid_field]}

    def _create_test_fip_id_for_port_forwarding(self):
        fake_fip = '172.23.3.0'
        ext_net_id = self._create_external_network_id()
        router_id = self._create_test_router_id()
        values = {
            'floating_ip_address': netaddr.IPAddress(fake_fip),
            'floating_network_id': ext_net_id,
            'floating_port_id': self._create_test_port_id(
                network_id=ext_net_id),
            'router_id': router_id,
        }
        fip_obj = router.FloatingIP(self.context, **values)
        fip_obj.create()
        return fip_obj.id

    def test_db_obj(self):
        # The reason for rewriting this test is:
        # 1. Currently, the existing test_db_obj test in
        #    obj_test_base.BaseDbObjectTestCase is not suitable for the case,
        #    for example, the db model is not the same with obj fields
        #    definition.
        # 2. For port forwarding, the db model will store and accept 'socket',
        #    but the obj fields just only support accepting the parameters
        #    generate 'socket', such as 'internal_ip_address' and
        #    'internal_port'.
        obj = self._make_object(self.obj_fields[0])
        self.assertIsNone(obj.db_obj)

        obj.create()
        self.assertIsNotNone(obj.db_obj)
        # Make sure the created obj socket field is correct.
        created_socket = obj.db_obj.socket.split(":")
        self.assertEqual(created_socket[0], str(obj.internal_ip_address))
        self.assertEqual(created_socket[1], str(obj.internal_port))

        fields_to_update = self.get_updatable_fields(self.obj_fields[1])
        if fields_to_update:
            old_fields = {}
            for key, val in fields_to_update.items():
                db_model_attr = (
                    obj.fields_need_translation.get(key, key))

                old_fields[db_model_attr] = obj.db_obj[
                    db_model_attr] if hasattr(
                    obj.db_obj, db_model_attr) else getattr(
                    obj, db_model_attr)
                setattr(obj, key, val)
            obj.update()
            self.assertIsNotNone(obj.db_obj)
            # Make sure the updated obj socket field is correct.
            updated_socket = obj.db_obj.socket.split(":")
            self.assertEqual(updated_socket[0],
                             str(self.obj_fields[1]['internal_ip_address']))
            self.assertEqual(updated_socket[1],
                             str(self.obj_fields[1]['internal_port']))
            # Then check all update fields had been updated.
            for k, v in obj.modify_fields_to_db(fields_to_update).items():
                self.assertEqual(v, obj.db_obj[k], '%s attribute differs' % k)

        obj.delete()
        self.assertIsNone(obj.db_obj)

    def test_get_objects_queries_constant(self):
        # NOTE(bzhao) Port Forwarding uses query FLoatingIP for injecting
        # floating_ip_address and router_id, not depends on relationship,
        # so it will cost extra SQL query each time for finding the
        # associated Floating IP by floatingip_id each time(or each
        # Port Forwarding Object). Rework this if this customized OVO
        # needs to be changed.
        pass
