# Copyright (c) 2016 Midokura SARL
# 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_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib import context
from neutron_lib.plugins import constants
from neutron_lib.plugins import directory

from neutron.db import extraroute_db
from neutron.objects import router as l3_obj
from neutron.tests.unit import testlib_api


class _Plugin(extraroute_db.ExtraRoute_dbonly_mixin):
    pass


class TestExtraRouteDb(testlib_api.SqlTestCase):
    def setUp(self):
        super(TestExtraRouteDb, self).setUp()
        self._plugin = _Plugin()
        directory.add_plugin(constants.CORE, self._plugin)

    def test_update(self):
        ctx = context.get_admin_context()
        create_request = {
            'router': {
                'name': 'my router',
                'tenant_id': 'my tenant',
                'admin_state_up': True,
            }
        }
        router = self._plugin.create_router(ctx, create_request)
        self.assertCountEqual(router['routes'], [])
        router_id = router['id']
        routes = [
            {'destination': '10.0.0.0/24', 'nexthop': '1.1.1.4'},
            {'destination': '10.1.0.0/24', 'nexthop': '1.1.1.3'},
            {'destination': '10.2.0.0/24', 'nexthop': '1.1.1.2'},
        ]
        self._test_update_routes(ctx, router_id, router, routes)
        routes = [
            {'destination': '10.0.0.0/24', 'nexthop': '1.1.1.4'},
            {'destination': '10.2.0.0/24', 'nexthop': '1.1.1.2'},
            {'destination': '10.3.0.0/24', 'nexthop': '1.1.1.1'},
        ]
        self._test_update_routes(ctx, router_id, router, routes)

    def _test_update_routes(self, ctx, router_id, router, routes):
        router['routes'] = routes
        update_request = {
            'router': router,
        }
        with mock.patch.object(registry, "publish") as mock_cb:
            with mock.patch.object(self._plugin, '_validate_routes'):
                updated_router = self._plugin.update_router(ctx, router_id,
                                                            update_request)
            mock_cb.assert_any_call('router', events.PRECOMMIT_UPDATE,
                                    self._plugin, payload=mock.ANY)
        self.assertCountEqual(updated_router['routes'], routes)
        got_router = self._plugin.get_router(ctx, router_id)
        self.assertCountEqual(got_router['routes'], routes)

    def assertEqualRoutes(self, a, b):
        """Compare a list of routes without caring for the list order."""
        return self.assertSetEqual(
            set(frozenset(r.items()) for r in a),
            set(frozenset(r.items()) for r in b))

    def test_add_extra_routes(self):
        self.assertEqual(
            [],
            self._plugin._add_extra_routes([], []),
        )

        old = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"}]
        add = []
        self.assertEqual(old, self._plugin._add_extra_routes(old, add))

        old = []
        add = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"}]
        self.assertEqual(add, self._plugin._add_extra_routes(old, add))

        old = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"}]
        add = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"}]
        self.assertEqual(old, self._plugin._add_extra_routes(old, add))

        old = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"}]
        add = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.11"}]
        self.assertEqualRoutes(
            old + add, self._plugin._add_extra_routes(old, add))

    def test_remove_extra_routes(self):
        old = []
        remove = []
        self.assertEqual(old, self._plugin._remove_extra_routes(old, remove))

        old = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"}]
        remove = []
        self.assertEqual(old, self._plugin._remove_extra_routes(old, remove))

        old = []
        remove = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"}]
        self.assertEqual(old, self._plugin._remove_extra_routes(old, remove))

        old = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"}]
        remove = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.11"}]
        self.assertEqual(old, self._plugin._remove_extra_routes(old, remove))

        old = [{"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"}]
        remove = old
        self.assertEqual([], self._plugin._remove_extra_routes(old, remove))

        old = [
            {"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"},
            {"destination": "10.0.11.0/24", "nexthop": "10.0.0.11"},
        ]
        remove = old[1:]
        self.assertEqual(
            old[:1], self._plugin._remove_extra_routes(old, remove))

        old = [
            {"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"},
            {"destination": "10.0.10.0/24", "nexthop": "10.0.0.11"},
        ]
        remove = old[1:]
        self.assertEqual(
            old[:1], self._plugin._remove_extra_routes(old, remove))

        old = []
        remove = [
            {"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"},
            {"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"},
        ]
        self.assertEqual([], self._plugin._remove_extra_routes(old, remove))

        old = [
            {"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"},
        ]
        remove = [
            {"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"},
            {"destination": "10.0.10.0/24", "nexthop": "10.0.0.10"},
        ]
        self.assertEqual([], self._plugin._remove_extra_routes(old, remove))

    def test_update_routes_where_route_vanishes_while_on_delete(self):
        ctx = context.get_admin_context()
        create_request = {
            'router': {
                'name': 'my router',
                'tenant_id': 'my tenant',
                'admin_state_up': True,
            }
        }
        router = self._plugin.create_router(ctx, create_request)
        self.assertCountEqual(router['routes'], [])
        router_id = router['id']
        routes = [
            {'destination': '10.0.0.0/24', 'nexthop': '1.1.1.4'},
            {'destination': '10.1.0.0/24', 'nexthop': '1.1.1.3'},
            {'destination': '10.2.0.0/24', 'nexthop': '1.1.1.2'},
        ]
        self._test_update_routes(ctx, router_id, router, routes)
        routes = [
            {'destination': '10.0.0.0/24', 'nexthop': '1.1.1.4'},
            {'destination': '10.1.0.0/24', 'nexthop': '1.1.1.3'},
        ]

        def _remove_last_route(orig_func):
            def _wrapper(ctx, router_id):
                routes = orig_func(ctx, router_id)

                # forcefully delete route to 10.2.0.0/24
                ctx2 = context.get_admin_context()
                l3_obj.RouterRoute.get_object(
                    ctx2,
                    router_id=router_id,
                    destination="10.2.0.0/24",
                    nexthop="1.1.1.2").delete()
                return routes

            return _wrapper

        with mock.patch.object(self._plugin, '_get_extra_routes_by_router_id',
                wraps=_remove_last_route(
                    self._plugin._get_extra_routes_by_router_id)) \
                as mock_get_routes:
            self._test_update_routes(ctx, router_id, router, routes)
            mock_get_routes.assert_called_once()
