# 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 itertools

import copy
import heatclient
import mock
from neutron import context as neutron_context
from neutron.extensions import external_net as external_net
from neutron.plugins.common import constants
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
import webob

from gbpservice.neutron.services.servicechain.plugins.ncp import config
from gbpservice.neutron.services.servicechain.plugins.ncp.node_drivers import (
    heat_node_driver as heat_node_driver)
from gbpservice.neutron.services.servicechain.plugins.ncp.node_drivers import (
    openstack_heat_api_client as heatClient)
from gbpservice.neutron.tests.unit.services.grouppolicy import (
    test_resource_mapping as test_gp_driver)
from gbpservice.neutron.tests.unit.services.servicechain.ncp import (
    test_ncp_plugin as test_ncp_plugin)


STACK_ACTION_WAIT_TIME = 15


class MockStackObject(object):
    def __init__(self, status):
        self.stack_status = status


class MockHeatClientFunctionsDeleteNotFound(object):
    def delete(self, stack_id):
        raise heatclient.exc.HTTPNotFound()

    def create(self, **fields):
        return {'stack': {'id': uuidutils.generate_uuid()}}

    def get(self, stack_id):
        return MockStackObject('DELETE_COMPLETE')


class MockHeatClientFunctions(object):
    def delete(self, stack_id):
        pass

    def create(self, **fields):
        return {'stack': {'id': uuidutils.generate_uuid()}}

    def get(self, stack_id):
        return MockStackObject('DELETE_COMPLETE')

    def update(self, *args, **fields):
        return {'stack': {'id': uuidutils.generate_uuid()}}


class MockHeatClientDeleteNotFound(object):
    def __init__(self, api_version, endpoint, **kwargs):
        self.stacks = MockHeatClientFunctionsDeleteNotFound()


class MockHeatClient(object):
    def __init__(self, api_version, endpoint, **kwargs):
        self.stacks = MockHeatClientFunctions()


class HeatNodeDriverTestCase(
        test_ncp_plugin.NodeCompositionPluginTestCase):

    DEFAULT_LB_CONFIG_DICT = {
            "AWSTemplateFormatVersion": "2010-09-09",
            "Resources": {
                "test_pool": {
                    "Type": "OS::Neutron::Pool",
                    "Properties": {
                        "admin_state_up": True,
                        "description": "Haproxy pool from teplate",
                        "lb_method": "ROUND_ROBIN",
                        "monitors": [{"Ref": "HttpHM"}],
                        "name": "Haproxy pool",
                        "protocol": "HTTP",
                        "subnet_id": {"Ref": "Subnet"},
                        "vip": {
                            "subnet": {"Ref": "Subnet"},
                            "address": {"Ref": "vip_ip"},
                            "name": "Haproxy vip",
                            "protocol_port": 80,
                            "connection_limit": -1,
                            "admin_state_up": True,
                            "description": "Haproxy vip from template"
                        }
                    }
                },
                "test_lb": {
                    "Type": "OS::Neutron::LoadBalancer",
                    "Properties": {
                        "pool_id": {"Ref": "HaproxyPool"},
                        "protocol_port": 80
                    }
                }
            }
    }
    DEFAULT_LB_CONFIG = jsonutils.dumps(DEFAULT_LB_CONFIG_DICT)
    DEFAULT_FW_CONFIG_DICT = {
            "heat_template_version": "2013-05-23",
            "resources": {
                'test_fw': {
                    "type": "OS::Neutron::Firewall",
                    "properties": {
                        "admin_state_up": True,
                        "firewall_policy_id": {
                            "get_resource": "Firewall_policy"},
                        "name": "testFirewall",
                        "description": "test Firewall"
                    }
                },
                'test_fw_policy': {
                    "type": "OS::Neutron::FirewallPolicy",
                    "properties": {
                        "shared": False,
                        "description": "test firewall policy",
                        "name": "testFWPolicy",
                        "firewall_rules": [{
                                "get_resource": "Rule_1"}],
                        "audited": True
                    }
                }
            }
    }
    DEFAULT_FW_CONFIG = jsonutils.dumps(DEFAULT_FW_CONFIG_DICT)
    SERVICE_PROFILE_VENDOR = 'heat_based_node_driver'

    def setUp(self):
        config.cfg.CONF.set_override('stack_action_wait_time',
                                     STACK_ACTION_WAIT_TIME,
                                     group='heat_node_driver')
        mock.patch(heatclient.__name__ + ".client.Client",
                   new=MockHeatClient).start()
        super(HeatNodeDriverTestCase, self).setUp(
            node_drivers=['heat_node_driver'],
            node_plumber='stitching_plumber',
            core_plugin=test_gp_driver.CORE_PLUGIN)

    def _create_network(self, fmt, name, admin_state_up, **kwargs):
        """Override the routine for allowing the router:external attribute."""
        # attributes containing a colon should be passed with
        # a double underscore
        new_args = dict(itertools.izip(map(lambda x: x.replace('__', ':'),
                                           kwargs),
                                       kwargs.values()))
        arg_list = new_args.pop('arg_list', ()) + (external_net.EXTERNAL,)
        return super(HeatNodeDriverTestCase, self)._create_network(
            fmt, name, admin_state_up, arg_list=arg_list, **new_args)

    def test_manager_initialized(self):
        mgr = self.plugin.driver_manager
        self.assertIsInstance(mgr.ordered_drivers[0].obj,
                              heat_node_driver.HeatNodeDriver)
        for driver in mgr.ordered_drivers:
            self.assertTrue(driver.obj.initialized)

    def _create_profiled_servicechain_node(
            self, service_type=constants.LOADBALANCER, shared_profile=False,
            profile_tenant_id=None, profile_id=None, **kwargs):
        if not profile_id:
            prof = self.create_service_profile(
                service_type=service_type,
                shared=shared_profile,
                vendor=self.SERVICE_PROFILE_VENDOR,
                tenant_id=profile_tenant_id or self._tenant_id)[
                                                    'service_profile']
        else:
            prof = self.get_service_profile(profile_id)

        service_config = kwargs.get('config')
        if not service_config or service_config == '{}':
            if service_type == constants.FIREWALL:
                kwargs['config'] = self.DEFAULT_FW_CONFIG
            else:
                kwargs['config'] = self.DEFAULT_LB_CONFIG
        return self.create_servicechain_node(
            service_profile_id=prof['id'], **kwargs)


class TestServiceChainInstance(HeatNodeDriverTestCase):

    def _get_node_instance_stacks(self, sc_node_id):
        context = neutron_context.get_admin_context()
        with context.session.begin(subtransactions=True):
            return (context.session.query(
                        heat_node_driver.ServiceNodeInstanceStack).
                    filter_by(sc_node_id=sc_node_id).
                    all())

    def test_invalid_service_type_rejected(self):
        node_used = self._create_profiled_servicechain_node(
            service_type="test")['servicechain_node']
        spec_used = self.create_servicechain_spec(
            nodes=[node_used['id']])['servicechain_spec']
        provider = self.create_policy_target_group()['policy_target_group']
        classifier = self.create_policy_classifier()['policy_classifier']
        res = self.create_servicechain_instance(
            provider_ptg_id=provider['id'],
            classifier_id=classifier['id'],
            servicechain_specs=[spec_used['id']],
            expected_res_status=webob.exc.HTTPBadRequest.code)
        self.assertEqual('NoDriverAvailableForAction',
                         res['NeutronError']['type'])

    def test_node_create(self):
        with mock.patch.object(heatClient.HeatClient,
                               'create') as stack_create:
            stack_create.return_value = {'stack': {
                                        'id': uuidutils.generate_uuid()}}
            self._create_simple_service_chain()
            expected_stack_name = mock.ANY
            expected_stack_params = mock.ANY
            stack_create.assert_called_once_with(
                    expected_stack_name,
                    self.DEFAULT_LB_CONFIG_DICT,
                    expected_stack_params)

    def _get_pool_member_resource_dict(self, port):
        member_ip = port['fixed_ips'][0]['ip_address']
        member_name = 'mem-' + member_ip
        member = {member_name: {
                        'Type': 'OS::Neutron::PoolMember',
                        'Properties': {
                            'protocol_port': '80',
                            'admin_state_up': True,
                            'pool_id': {'Ref': u'test_pool'},
                            'weight': 1,
                            'address': member_ip
                        }
                    }
                  }
        return member

    def _create_policy_target_port(self, policy_target_group_id):
        pt = self.create_policy_target(
                policy_target_group_id=policy_target_group_id)['policy_target']
        req = self.new_show_request('ports', pt['port_id'], fmt=self.fmt)
        port = self.deserialize(self.fmt,
                                req.get_response(self.api))['port']
        return (pt, port)

    def _create_external_policy(self, consumed_prs, routes=None):
        with self.network(router__external=True, shared=True) as net:
            with self.subnet(cidr='192.168.0.0/24', network=net) as sub:
                if not routes:
                    routes = [{'destination': '172.0.0.0/22', 'nexthop': None}]
                self.create_external_segment(
                    shared=True,
                    name="default",
                    external_routes=routes,
                    subnet_id=sub['subnet']['id'])
                return self.create_external_policy(
                    consumed_policy_rule_sets={consumed_prs: ''})

    def _test_lb_node_create(self, consumer_external=False):
        with mock.patch.object(heatClient.HeatClient,
                               'create') as stack_create:
            stack_create.return_value = {'stack': {
                                        'id': uuidutils.generate_uuid()}}

            node_id = self._create_profiled_servicechain_node(
                service_type=constants.LOADBALANCER)['servicechain_node']['id']
            spec = self.create_servicechain_spec(
                nodes=[node_id],
                expected_res_status=201)['servicechain_spec']

            prs = self._create_redirect_prs(spec['id'])['policy_rule_set']
            provider = self.create_policy_target_group()['policy_target_group']

            _, port1 = self._create_policy_target_port(provider['id'])
            _, port2 = self._create_policy_target_port(provider['id'])

            if consumer_external:
                self._create_external_policy(prs['id'])
            else:
                self.create_policy_target_group(
                    consumed_policy_rule_sets={prs['id']: ''})

            self.update_policy_target_group(
                provider['id'], provided_policy_rule_sets={prs['id']: ''})
            created_stacks_map = self._get_node_instance_stacks(node_id)
            self.assertEqual(1, len(created_stacks_map))

            pool_member1 = self._get_pool_member_resource_dict(port1)
            pool_member2 = self._get_pool_member_resource_dict(port2)

            # Instantiating the chain invokes stack create
            expected_stack_template = copy.deepcopy(
                                    self.DEFAULT_LB_CONFIG_DICT)
            expected_stack_template['Resources'].update(pool_member1)
            expected_stack_template['Resources'].update(pool_member2)
            expected_stack_name = mock.ANY
            # TODO(Magesh): Verify expected_stack_params with IP address from
            # Network Service Policy
            expected_stack_params = {}
            stack_create.assert_called_once_with(
                    expected_stack_name,
                    expected_stack_template,
                    expected_stack_params)
            return (expected_stack_template, provider,
                    created_stacks_map[0].stack_id)

    def _test_lb_dynamic_pool_member_add(self, expected_stack_template,
                                         provider, stack_id):
        with mock.patch.object(heatClient.HeatClient,
                           'update') as stack_update:
            stack_update.return_value = {'stack': {
                                        'id': stack_id}}

            # Creating PT will update the node, thereby adding the PT as an
            # LB Pool Member using heat stack
            pt, port = self._create_policy_target_port(provider['id'])

            pool_member = self._get_pool_member_resource_dict(port)
            expected_stack_template['Resources'].update(pool_member)
            expected_stack_id = stack_id
            expected_stack_params = {}
            stack_update.assert_called_once_with(
                    expected_stack_id,
                    expected_stack_template,
                    expected_stack_params)
            return (pt, pool_member)

    def _test_dynamic_lb_pool_member_delete(self, pt, pool_member,
                                            expected_stack_template,
                                            stack_id):
        # Deleting PT will update the node, thereby removing the Pool
        # Member from heat stack
        with mock.patch.object(heatClient.HeatClient,
                           'update') as stack_update:
            self.delete_policy_target(pt['id'])

            template_on_delete_pt = copy.deepcopy(expected_stack_template)
            template_on_delete_pt['Resources'].pop(pool_member.keys()[0])
            expected_stack_id = stack_id
            expected_stack_params = {}
            stack_update.assert_called_once_with(
                    expected_stack_id,
                    template_on_delete_pt,
                    expected_stack_params)

    def _test_node_cleanup(self, ptg, stack_id):
        with mock.patch.object(heatClient.HeatClient,
                               'delete') as stack_delete:
            self.update_policy_target_group(
                    ptg['id'], consumed_policy_rule_sets={},
                    expected_res_status=200)
            self.delete_policy_target_group(ptg['id'], expected_res_status=204)
            stack_delete.assert_called_once_with(stack_id)

    def test_lb_node_operations(self):
        expected_stack_template, provider, stack_id = (
                                self._test_lb_node_create())
        pt, pool_member = self._test_lb_dynamic_pool_member_add(
                expected_stack_template, provider, stack_id)
        self._test_dynamic_lb_pool_member_delete(
                pt, pool_member, expected_stack_template, stack_id)
        self._test_node_cleanup(provider, stack_id)

    def test_lb_redirect_from_external(self):
        expected_stack_template, provider, stack_id = (
                self._test_lb_node_create(consumer_external=True))
        pt, pool_member = self._test_lb_dynamic_pool_member_add(
                expected_stack_template, provider, stack_id)
        self._test_dynamic_lb_pool_member_delete(
                pt, pool_member, expected_stack_template, stack_id)
        self._test_node_cleanup(provider, stack_id)

    def _create_fwredirect_ruleset(self, classifier_port, classifier_protocol):
        node_id = self._create_profiled_servicechain_node(
                service_type=constants.FIREWALL)['servicechain_node']['id']
        spec = self.create_servicechain_spec(
            nodes=[node_id],
            expected_res_status=201)['servicechain_spec']
        action = self.create_policy_action(action_type='REDIRECT',
                                           action_value=spec['id'])
        classifier = self.create_policy_classifier(
            port_range=classifier_port, protocol=classifier_protocol,
            direction='bi')
        rule = self.create_policy_rule(
            policy_actions=[action['policy_action']['id']],
            policy_classifier_id=classifier['policy_classifier']['id'])
        rule = rule['policy_rule']
        prs = self.create_policy_rule_set(policy_rules=[rule['id']])
        return (prs['policy_rule_set'], node_id)

    def _get_ptg_cidr(self, ptg):
        req = self.new_show_request(
                'subnets', ptg['subnets'][0], fmt=self.fmt)
        ptg_subnet = self.deserialize(
                self.fmt, req.get_response(self.api))['subnet']
        return ptg_subnet['cidr']

    def _get_firewall_rule_dict(self, rule_name, protocol, port, provider_cidr,
                                consumer_cidr):
        if provider_cidr and consumer_cidr:
            fw_rule = {rule_name: {'type': "OS::Neutron::FirewallRule",
                                   'properties': {
                                       "protocol": protocol,
                                       "enabled": True,
                                       "destination_port": port,
                                       "action": "allow",
                                       "destination_ip_address": provider_cidr,
                                       "source_ip_address": consumer_cidr
                                   }
                                   }
                       }
            return fw_rule
        return {}

    def test_fw_node_east_west(self):
        classifier_port = '66'
        classifier_protocol = 'udp'
        with mock.patch.object(heatClient.HeatClient,
                               'create') as stack_create:
            stack_create.return_value = {'stack': {
                                        'id': uuidutils.generate_uuid()}}
            prs, node_id = self._create_fwredirect_ruleset(
                                    classifier_port, classifier_protocol)
            provider = self.create_policy_target_group(
                provided_policy_rule_sets={prs['id']: ''})[
                                                'policy_target_group']
            self.create_policy_target_group(
                consumed_policy_rule_sets={prs['id']: ''})

            created_stacks_map = self._get_node_instance_stacks(node_id)
            self.assertEqual(1, len(created_stacks_map))
            stack_id = created_stacks_map[0].stack_id

            provider_cidr = self._get_ptg_cidr(provider)
            # TODO(ivar): This has to be removed once support to consumer list
            # is implemented
            #consumer_cidr = self._get_ptg_cidr(consumer)
            consumer_cidr = []
            fw_rule = self._get_firewall_rule_dict(
                'Rule_1', classifier_protocol, classifier_port,
                provider_cidr, consumer_cidr)

            expected_stack_template = copy.deepcopy(
                                        self.DEFAULT_FW_CONFIG_DICT)
            expected_stack_template['resources'][
                'test_fw_policy']['properties']['firewall_rules'] = []
            expected_stack_template['resources'].update(fw_rule)
            expected_stack_name = mock.ANY
            expected_stack_params = {}
            stack_create.assert_called_once_with(
                    expected_stack_name,
                    expected_stack_template,
                    expected_stack_params)

            self._test_node_cleanup(provider, stack_id)

    def _test_fw_node_north_south(self, consumer_cidrs):
        classifier_port = '66'
        classifier_protocol = 'udp'
        with mock.patch.object(heatClient.HeatClient,
                               'create') as stack_create:
            stack_create.return_value = {'stack': {
                                        'id': uuidutils.generate_uuid()}}
            prs, node_id = self._create_fwredirect_ruleset(
                                    classifier_port, classifier_protocol)
            provider = self.create_policy_target_group(
                provided_policy_rule_sets={prs['id']: ''})[
                                                'policy_target_group']

            routes = []
            for consumer_cidr in consumer_cidrs:
                routes.append({'destination': consumer_cidr, 'nexthop': None})
            self._create_external_policy(prs['id'], routes=routes)

            # TODO(ivar): This has to be removed once support to consumer list
            # is implemented
            consumer_cidrs = []

            created_stacks_map = self._get_node_instance_stacks(node_id)
            self.assertEqual(1, len(created_stacks_map))
            stack_id = created_stacks_map[0].stack_id

            expected_stack_template = copy.deepcopy(
                                        self.DEFAULT_FW_CONFIG_DICT)
            expected_stack_template['resources']['test_fw_policy'][
                                        'properties']['firewall_rules'] = []
            provider_cidr = self._get_ptg_cidr(provider)

            rule_num = 1
            for consumer_cidr in consumer_cidrs:
                rule_name = 'Rule_' + str(rule_num)
                fw_rule = self._get_firewall_rule_dict(
                    rule_name, classifier_protocol, classifier_port,
                    provider_cidr, consumer_cidr)
                rule_num = rule_num + 1
                expected_stack_template['resources'].update(fw_rule)
                expected_stack_template['resources']['test_fw_policy'][
                    'properties']['firewall_rules'].append(
                                                {'get_resource': rule_name})

            expected_stack_name = mock.ANY
            expected_stack_params = {}
            stack_create.assert_called_once_with(
                    expected_stack_name,
                    expected_stack_template,
                    expected_stack_params)

            self._test_node_cleanup(provider, stack_id)

    def test_fw_node_north_south_single_external_cidr(self):
        self._test_fw_node_north_south(['172.0.0.0/22'])

    def test_fw_node_north_south_multiple_external_cidr(self):
        self._test_fw_node_north_south(['172.0.0.0/22', '20.0.0.0/16'])

    def test_node_update(self):
        with mock.patch.object(heatClient.HeatClient,
                               'create') as stack_create:
            stack_create.return_value = {'stack': {
                                        'id': uuidutils.generate_uuid()}}
            prof = self.create_service_profile(
                        service_type=constants.LOADBALANCER,
                        vendor=self.SERVICE_PROFILE_VENDOR)['service_profile']

            node = self.create_servicechain_node(
                        service_profile_id=prof['id'],
                        config=self.DEFAULT_LB_CONFIG,
                        expected_res_status=201)['servicechain_node']

            self._create_chain_with_nodes(node_ids=[node['id']])
            with mock.patch.object(heatClient.HeatClient,
                                   'update') as stack_update:
                self.update_servicechain_node(
                                        node['id'],
                                        name='newname',
                                        expected_res_status=200)
                # Name update should not update stack ??
                stack_update.assert_called_once_with(
                                    mock.ANY, mock.ANY, mock.ANY)

    def test_node_delete(self):
        with mock.patch.object(heatClient.HeatClient,
                               'create') as stack_create:
            stack_create.return_value = {'stack': {
                                        'id': uuidutils.generate_uuid()}}
            provider, _, _ = self._create_simple_service_chain()
            with mock.patch.object(heatClient.HeatClient,
                                   'delete'):
                self.update_policy_target_group(
                                        provider['id'],
                                        provided_policy_rule_sets={},
                                        expected_res_status=200)
                self.delete_policy_target_group(provider['id'],
                                                expected_res_status=204)

    def test_wait_stack_delete_for_instance_delete(self):

        with mock.patch.object(heatClient.HeatClient,
                               'create') as stack_create:
            stack_create.return_value = {'stack': {
                                        'id': uuidutils.generate_uuid()}}
            provider, _, _ = self._create_simple_service_chain()

            # Verify that as part of delete service chain instance we call
            # get method for heat stack 5 times before giving up if the state
            # does not become DELETE_COMPLETE
            with mock.patch.object(heatClient.HeatClient,
                                   'delete') as stack_delete:
                with mock.patch.object(heatClient.HeatClient,
                                       'get') as stack_get:
                    stack_get.return_value = MockStackObject(
                        'DELETE_IN_PROGRESS')
                    # Removing the PRSs will make the PTG deletable again
                    self.update_policy_target_group(
                                        provider['id'],
                                        provided_policy_rule_sets={},
                                        expected_res_status=200)
                    self.delete_policy_target_group(provider['id'],
                                                expected_res_status=204)
                    stack_delete.assert_called_once_with(mock.ANY)
                    self.assertEqual(STACK_ACTION_WAIT_TIME / 5,
                                     stack_get.call_count)

            # Create and delete another service chain instance and verify that
            # we call get method for heat stack only once if the stack state
            # is DELETE_COMPLETE
            provider, _, _ = self._create_simple_service_chain()
            with mock.patch.object(heatClient.HeatClient,
                                   'delete') as stack_delete:
                with mock.patch.object(heatClient.HeatClient,
                                       'get') as stack_get:
                    stack_get.return_value = MockStackObject(
                        'DELETE_COMPLETE')
                    # Removing the PRSs will make the PTG deletable again
                    self.update_policy_target_group(
                                        provider['id'],
                                        provided_policy_rule_sets={},
                                        expected_res_status=200)
                    self.delete_policy_target_group(provider['id'],
                                                expected_res_status=204)
                    stack_delete.assert_called_once_with(mock.ANY)
                    self.assertEqual(1, stack_get.call_count)

    def test_stack_not_found_ignored(self):
        mock.patch(heatclient.__name__ + ".client.Client",
                   new=MockHeatClientDeleteNotFound).start()

        provider, _, _ = self._create_simple_service_chain()

        # Removing the PRSs will make the PTG deletable again
        self.update_policy_target_group(provider['id'],
                                        provided_policy_rule_sets={},
                                        expected_res_status=200)
        self.delete_policy_target_group(provider['id'],
                                        expected_res_status=204)
