# Copyright (C) 2016 VA Linux Systems Japan K.K.
# Copyright (C) 2016 Fumihiko Kakuma <kakuma at valinux co jp>
# 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 collections
import threading
import time

import netaddr

from os_ken.tests.integrated.common import docker_base as ctn_base
from tempest.common import utils
from tempest import config

from neutron_tempest_plugin.api import base
from neutron_tempest_plugin import config as nconfig
from neutron_tempest_plugin.services.bgp import bgp_client

CONF = config.CONF

Scope = collections.namedtuple('Scope', 'name')
Pool = collections.namedtuple('Pool', 'name, prefixlen, prefixes')
Net = collections.namedtuple('Net', 'name, net, mask, cidr, router')
SubNet = collections.namedtuple('SubNet', 'name, cidr, mask')
Router = collections.namedtuple('Router', 'name, gw')
AS = collections.namedtuple('AS', 'asn, router_id, adv_net')
CHECKTIME = 180
CHECKTIME_INFO = 60
CHECKTIME_INT = 1
BRIDGE_TYPE = ctn_base.BRIDGE_TYPE_DOCKER


def _setup_client_args(auth_provider):
    """Set up ServiceClient arguments using config settings. """
    service = CONF.network.catalog_type or 'network'
    region = CONF.network.region or 'regionOne'
    endpoint_type = CONF.network.endpoint_type
    build_interval = CONF.network.build_interval
    build_timeout = CONF.network.build_timeout

    # The disable_ssl appears in identity
    disable_ssl_certificate_validation = (
        CONF.identity.disable_ssl_certificate_validation)
    ca_certs = CONF.identity.ca_certificates_file

    # Trace in debug section
    trace_requests = CONF.debug.trace_requests

    return [auth_provider, service, region, endpoint_type,
            build_interval, build_timeout,
            disable_ssl_certificate_validation, ca_certs,
            trace_requests]


class BgpSpeakerScenarioTestJSONBase(base.BaseAdminNetworkTest):

    def setUp(self):
        self.addCleanup(self.net_resource_cleanup)
        super(BgpSpeakerScenarioTestJSONBase, self).setUp()

    @classmethod
    def _setup_bgp_non_admin_client(cls):
        mgr = cls.get_client_manager()
        auth_provider = mgr.auth_provider
        client_args = _setup_client_args(auth_provider)
        cls.bgp_client = bgp_client.BgpSpeakerClientJSON(*client_args)

    @classmethod
    def _setup_bgp_admin_client(cls):
        mgr = cls.get_client_manager(credential_type='admin')
        auth_provider = mgr.auth_provider
        client_args = _setup_client_args(auth_provider)
        cls.bgp_adm_client = bgp_client.BgpSpeakerClientJSON(*client_args)

    @classmethod
    def resource_setup(cls):
        super(BgpSpeakerScenarioTestJSONBase, cls).resource_setup()
        if not utils.is_extension_enabled('bgp', 'network'):
            msg = "BGP Speaker extension is not enabled."
            raise cls.skipException(msg)

        cls.images = []
        cls.containers = []
        cls.r_ass = []
        cls.r_as_ip = []
        cls.bridges = []
        cls.admin_routerports = []
        cls.admin_floatingips = []
        cls.admin_routers = []
        cls.admin_router_ip = []
        cls.resource_setup_container()
        cls._setup_bgp_admin_client()
        cls._setup_bgp_non_admin_client()
        cls.lock = threading.Lock()

    @classmethod
    def resource_cleanup(cls):
        for ctn in cls.containers:
            try:
                ctn.stop()
            except ctn_base.CommandError:
                pass
            ctn.remove()
        for br in cls.bridges:
            br.delete()
        super(BgpSpeakerScenarioTestJSONBase, cls).resource_cleanup()

    @classmethod
    def get_subnet(self, start='10.10.1.0', end='10.10.255.0', step=256):
        subnet_gen = netaddr.iter_iprange(start, end, step=step)
        i = 1
        while True:
            with self.lock:
                try:
                    yield (i, str(next(subnet_gen)))
                except StopIteration:
                    subnet_gen = netaddr.iter_iprange(start, end, step=step)
                    yield (i, str(next(subnet_gen)))
                i += 1

    def net_resource_cleanup(self):
        for floatingip in self.admin_floatingips:
            self._try_delete_resource(self.admin_client.delete_floatingip,
                                      floatingip['id'])
        for routerport in self.admin_routerports:
            self._try_delete_resource(
                self.admin_client.remove_router_interface_with_subnet_id,
                routerport['router_id'], routerport['subnet_id'])
        for router in self.admin_routers:
            self._try_delete_resource(self.admin_client.delete_router,
                                      router['id'])

    def create_bgp_speaker(self, auto_delete=True, **args):
        data = {'bgp_speaker': args}
        bgp_speaker = self.bgp_adm_client.create_bgp_speaker(data)
        bgp_speaker_id = bgp_speaker['bgp_speaker']['id']
        if auto_delete:
            self.addCleanup(self.bgp_adm_client.delete_bgp_speaker,
                            bgp_speaker_id)
        if nconfig.CONF.neutron_plugin_options.bgp_schedule_speakers_to_agents:
            self.add_bgp_speaker_to_dragent(self.get_dragent_id(),
                                            bgp_speaker_id)
        return bgp_speaker['bgp_speaker']

    def delete_bgp_speaker(self, id):
        return self.bgp_adm_client.delete_bgp_speaker(id)

    def create_bgp_peer(self, auto_delete=True, **args):
        bgp_peer = self.bgp_adm_client.create_bgp_peer({'bgp_peer': args})
        bgp_peer_id = bgp_peer['bgp_peer']['id']
        if auto_delete:
            self.addCleanup(self.bgp_adm_client.delete_bgp_peer,
                            bgp_peer_id)
        return bgp_peer['bgp_peer']

    def delete_bgp_peer(self, id):
        return self.bgp_adm_client.delete_bgp_peer(id)

    def get_dragent_id(self):
        agents = self.admin_client.list_agents(
            agent_type="BGP dynamic routing agent")
        self.assertTrue(agents['agents'][0]['alive'])
        return agents['agents'][0]['id']

    def add_bgp_speaker_to_dragent(self, agent_id, speaker_id):
        self.bgp_adm_client.add_bgp_speaker_to_dragent(agent_id, speaker_id)

    # tnets[[net1, subnet1, router1], [net2, subnet2, router2], ...]
    def create_bgp_network(self, ip_version, scope,
                           exnet, expool, exsubnet,
                           tpool, tnets):
        addr_scope = self.create_address_scope(scope.name,
                                               ip_version=ip_version)
        # external network
        ext_net = self.create_shared_network(**{'router:external': True})
        ext_net_id = ext_net['id']
        ext_subnetpool = self.create_subnetpool(
            expool.name,
            is_admin=True,
            default_prefixlen=expool.prefixlen,
            address_scope_id=addr_scope['id'],
            prefixes=expool.prefixes)
        self.create_subnet(
            {'id': ext_net_id},
            cidr=netaddr.IPNetwork(exsubnet.cidr),
            mask_bits=exsubnet.mask,
            ip_version=ip_version,
            client=self.admin_client,
            subnetpool_id=ext_subnetpool['id'],
            reserve_cidr=False)
        # tenant network
        tenant_subnetpool = self.create_subnetpool(
            tpool.name,
            default_prefixlen=tpool.prefixlen,
            address_scope_id=addr_scope['id'],
            prefixes=tpool.prefixes)
        for tnet, tsubnet, router in tnets:
            tenant_net = self.create_network()
            tenant_subnet = self.create_subnet(
                {'id': tenant_net['id']},
                cidr=netaddr.IPNetwork(tsubnet.cidr),
                mask_bits=tsubnet.mask,
                ip_version=ip_version,
                subnetpool_id=tenant_subnetpool['id'],
                reserve_cidr=False)
            # router
            ext_gw_info = {'network_id': ext_net_id}
            router_cr = self.admin_client.create_router(
                router.name,
                external_gateway_info=ext_gw_info)['router']
            self.admin_routers.append(router_cr)
            self.admin_client.add_router_interface_with_subnet_id(
                router_cr['id'],
                tenant_subnet['id'])
            self.admin_routerports.append({'router_id': router_cr['id'],
                                           'subnet_id': tenant_subnet['id']})
            router = self.admin_client.show_router(router_cr['id'])['router']
            fixed_ips = router['external_gateway_info']['external_fixed_ips']
            self.admin_router_ip.append(fixed_ips[0]['ip_address'])
        return ext_net_id

    def create_and_add_peers_to_speaker(self, ext_net_id,
                                        speaker_info, peer_infos,
                                        auto_delete=True):
        speaker = self.create_bgp_speaker(auto_delete=auto_delete,
                                          **speaker_info)
        speaker_id = speaker['id']
        self.bgp_adm_client.add_bgp_gateway_network(speaker_id,
                                                    ext_net_id)
        peer_ids = []
        for peer_args in peer_infos:
            peer = self.create_bgp_peer(auto_delete=auto_delete,
                                        **peer_args)
            peer_id = peer['id']
            peer_ids.append(peer_id)
            self.bgp_adm_client.add_bgp_peer_with_id(speaker_id,
                                                     peer_id)
        return (speaker_id, peer_ids)

    def get_remote_as_state(self, l_as, r_as,
                            expected_state,
                            init_state=ctn_base.BGP_FSM_IDLE,
                            checktime=CHECKTIME,
                            checktime_int=CHECKTIME_INT):
        neighbor_state = init_state
        for i in range(0, checktime):
            neighbor_state = r_as.get_neighbor_state(l_as)
            if neighbor_state == expected_state:
                break
            time.sleep(checktime_int)
        return neighbor_state

    def check_remote_as_state(self, l_as, r_as,
                              expected_state,
                              init_state=ctn_base.BGP_FSM_IDLE,
                              checktime=CHECKTIME,
                              checktime_int=CHECKTIME_INT):
        neighbor_state = self.get_remote_as_state(l_as, r_as,
                                                  expected_state,
                                                  init_state=init_state,
                                                  checktime=checktime,
                                                  checktime_int=checktime_int)
        self.assertEqual(neighbor_state, expected_state)

    def get_remote_as_of_state_ok(self, l_as, r_ass,
                                  expected_state,
                                  init_state=ctn_base.BGP_FSM_IDLE,
                                  checktime=CHECKTIME,
                                  checktime_int=CHECKTIME_INT):
        neighbor_state = init_state
        ras_list = []
        ras_max = len(r_ass)
        for r_as in r_ass:
            ras_list.append({'as': r_as, 'check': False})
        ok_ras = []
        for i in range(0, checktime):
            for ras in ras_list:
                if ras['check']:
                    continue
                neighbor_state = ras['as'].get_neighbor_state(l_as)
                if neighbor_state == expected_state:
                    ras['check'] = True
                    ok_ras.append(ras['as'])
            if len(ok_ras) >= ras_max:
                break
            time.sleep(checktime_int)
        return ok_ras

    def check_multi_remote_as_state(self, l_as, r_ass,
                                    expected_state,
                                    init_state=ctn_base.BGP_FSM_IDLE,
                                    checktime=CHECKTIME,
                                    checktime_int=CHECKTIME_INT):
        ok_ras = self.get_remote_as_of_state_ok(
            l_as, r_ass,
            expected_state,
            init_state=init_state,
            checktime=checktime,
            checktime_int=checktime_int)
        self.assertEqual(len(ok_ras), len(r_ass))

    def get_remote_as_rib(self, r_as, prefix, rf, key, expected_item,
                          checktime=CHECKTIME_INFO,
                          checktime_int=CHECKTIME_INT):
        item = None
        for i in range(0, checktime):
            rib = r_as.get_global_rib(prefix=prefix, rf=rf)
            if rib and key in rib[0]:
                if expected_item == rib[0][key]:
                    item = rib[0][key]
                    break
            time.sleep(checktime_int)
        return item

    def check_remote_as_rib(self, r_as, prefix, rf, key, expected_item,
                            checktime=CHECKTIME_INFO,
                            checktime_int=CHECKTIME_INT):
        item = self.get_remote_as_rib(r_as=r_as, prefix=prefix, rf=rf,
                                      key=key, expected_item=expected_item,
                                      checktime=checktime,
                                      checktime_int=checktime_int)
        self.assertEqual(expected_item, item)

    def get_remote_as_of_rib_ok(self, r_ass, prefix, rf, key, expected_item,
                                checktime=CHECKTIME_INFO,
                                checktime_int=CHECKTIME_INT):
        ras_list = []
        ras_max = len(r_ass)
        for r_as in r_ass:
            ras_list.append({'as': r_as, 'check': False})
        ok_ras = []
        for i in range(0, checktime):
            for ras in ras_list:
                if ras['check']:
                    continue
                rib = r_as.get_global_rib(prefix=prefix, rf=rf)
                if rib and key in rib[0]:
                    if expected_item == rib[0][key]:
                        ras['check'] = True
                        ok_ras.append(ras['as'])
            if len(ok_ras) >= ras_max:
                break
            time.sleep(checktime_int)
        return ok_ras

    def check_multi_remote_as_rib(self, r_ass, prefix, rf, key, expected_item,
                                  checktime=CHECKTIME_INFO,
                                  checktime_int=CHECKTIME_INT):
        ok_ras = self.get_remote_as_of_rib_ok(
                   r_ass=r_ass, prefix=prefix, rf=rf,
                   key=key, expected_item=expected_item,
                   checktime=checktime,
                   checktime_int=checktime_int)
        self.assertEqual(len(ok_ras), len(r_ass))

    def get_next_hop(self, speaker_id, dest_addr):
        routes = self.bgp_adm_client.get_bgp_advertised_routes(speaker_id)
        next_hop = ''
        for route in routes['advertised_routes']:
            if route['destination'] == dest_addr:
                next_hop = route['next_hop']
                break
        return next_hop
