#
# Copyright (c) 2014 VMware, Inc. 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.

"""
test_congress
----------------------------------

Tests for `congress` module.
"""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import

import mock
import neutronclient.v2_0
from oslo_log import log as logging

from congress.api import base as api_base
from congress.common import config
from congress.datalog import compile
from congress.datasources import neutronv2_driver
from congress.datasources import nova_driver
from congress.db import db_library_policies
from congress.tests.api import base as tests_api_base
from congress.tests import base
from congress.tests.datasources import fakes
from congress.tests import helper


LOG = logging.getLogger(__name__)


class BaseTestPolicyCongress(base.SqlTestCase):

    def setUp(self):
        super(BaseTestPolicyCongress, self).setUp()
        self.services = tests_api_base.setup_config(with_fake_datasource=False)
        self.api = self.services['api']
        self.node = self.services['node']
        self.engine = self.services['engine']
        self.library = self.services['library']

        self.neutronv2 = self._create_neutron_mock('neutron')

    def tearDown(self):
        self.node.stop()
        super(BaseTestPolicyCongress, self).tearDown()

    def _create_neutron_mock(self, name):
        # Register Neutron service
        args = helper.datasource_openstack_args()
        neutronv2 = neutronv2_driver.NeutronV2Driver(name, args=args)
        # FIXME(ekcs): this is a hack to prevent the synchronizer from
        # attempting to delete this DSD because it's not in DB
        neutronv2.type = 'no_sync_datasource_driver'
        neutron_mock = mock.MagicMock(spec=neutronclient.v2_0.client.Client)
        neutronv2.neutron = neutron_mock

        # initialize neutron_mocks
        network1 = fakes.network_response
        port_response = fakes.port_response
        router_response = fakes.router_response
        sg_group_response = fakes.security_group_response
        neutron_mock.list_networks.return_value = network1
        neutron_mock.list_ports.return_value = port_response
        neutron_mock.list_routers.return_value = router_response
        neutron_mock.list_security_groups.return_value = sg_group_response
        self.node.register_service(neutronv2)
        return neutronv2


class TestCongress(BaseTestPolicyCongress):

    def setUp(self):
        """Setup tests that use multiple mock neutron instances."""
        super(TestCongress, self).setUp()

        # clear the library policies loaded on startup
        db_library_policies.delete_policies()

    def tearDown(self):
        super(TestCongress, self).tearDown()

    def setup_config(self):
        args = ['--config-file', helper.etcdir('congress.conf.test')]
        config.init(args)

    def test_startup(self):
        self.assertIsNotNone(self.services['api'])
        self.assertIsNotNone(self.services['engine'])
        self.assertIsNotNone(self.services['library'])
        self.assertIsNotNone(self.services['engine'].node)

    def test_policy(self):
        self.create_policy('alpha')
        self.insert_rule('q(1, 2) :- true', 'alpha')
        self.insert_rule('q(2, 3) :- true', 'alpha')
        helper.retry_check_function_return_value(
            lambda: sorted(self.query('q', 'alpha')['results'],
                           key=lambda x: x['data']),
            sorted([{'data': (1, 2)}, {'data': (2, 3)}],
                   key=lambda x: x['data']))
        helper.retry_check_function_return_value(
            lambda: list(self.query('q', 'alpha').keys()),
            ['results'])

    def test_policy_datasource(self):
        self.create_policy('alpha')
        self.create_fake_datasource('fake')
        data = self.node.service_object('fake')
        data.state = {'fake_table': set([(1, 2)])}

        data.poll()
        self.insert_rule('q(x) :- fake:fake_table(x,y)', 'alpha')
        helper.retry_check_function_return_value(
            lambda: self.query('q', 'alpha'), {'results': [{'data': (1,)}]})

        # TODO(dse2): enable rules to be inserted before data created.
        #  Maybe just have subscription handle errors graceful when
        #  asking for a snapshot and return [].
        # self.insert_rule('p(x) :- fake:fake_table(x)', 'alpha')

    def test_policy_create_from_library(self):
        def adjust_for_comparison(rules):
            # compile rule string into rule object
            # replace dict with tuple for sorting
            # 'id' field implicitly dropped if present
            rules = [(compile.parse1(rule['rule']), rule['name'],
                      rule['comment']) for rule in rules]

            # sort lists for comparison
            return sorted(rules)

        test_policy = {
            "name": "test_policy",
            "description": "test policy description",
            "kind": "nonrecursive",
            "abbreviation": "abbr",
            "rules": [{"rule": "p(x) :- q(x)", "comment": "test comment",
                       "name": "test name"},
                      {"rule": "p(x) :- q2(x)", "comment": "test comment2",
                       "name": "test name2"}]
        }
        test_policy_id, test_policy_obj = self.api[
            'api-library-policy'].add_item(test_policy, {})

        add_policy_id, add_policy_obj = self.api['api-policy'].add_item(
            None, {'library_policy': test_policy_id})

        test_policy['id'] = add_policy_id

        # adjust for comparison
        test_policy['owner_id'] = 'user'
        test_policy['rules'] = adjust_for_comparison(test_policy['rules'])

        add_policy_obj['rules'] = adjust_for_comparison(
            add_policy_obj['rules'])

        self.assertEqual(add_policy_obj, test_policy)

        context = {'policy_id': test_policy['name']}
        rules = self.api['api-rule'].get_items({}, context)['results']
        rules = adjust_for_comparison(rules)
        self.assertEqual(rules, test_policy['rules'])

        res = self.api['api-policy'].get_items({})['results']
        del test_policy['rules']
        self.assertIn(test_policy, res)

    def test_policy_create_with_rules(self):
        def adjust_for_comparison(rules):
            # compile rule string into rule object
            # replace dict with tuple for sorting
            # 'id' field implicitly dropped if present
            rules = [(compile.parse1(rule['rule']), rule['name'],
                      rule['comment']) for rule in rules]

            # sort lists for comparison
            return sorted(rules)

        test_policy = {
            "name": "test_policy",
            "description": "test policy description",
            "kind": "nonrecursive",
            "abbreviation": "abbr",
            "rules": [{"rule": "p(x) :- q(x)", "comment": "test comment",
                       "name": "test name"},
                      {"rule": "p(x) :- q2(x)", "comment": "test comment2",
                       "name": "test name2"}]
        }

        add_policy_id, add_policy_obj = self.api['api-policy'].add_item(
            test_policy, {})

        test_policy['id'] = add_policy_id

        # adjust for comparison
        test_policy['owner_id'] = 'user'
        test_policy['rules'] = adjust_for_comparison(test_policy['rules'])

        add_policy_obj['rules'] = adjust_for_comparison(
            add_policy_obj['rules'])

        self.assertEqual(add_policy_obj, test_policy)

        context = {'policy_id': test_policy['name']}
        rules = self.api['api-rule'].get_items({}, context)['results']
        rules = adjust_for_comparison(rules)
        self.assertEqual(rules, test_policy['rules'])

        res = self.api['api-policy'].get_items({})['results']
        del test_policy['rules']
        self.assertIn(test_policy, res)

    def create_policy(self, name):
        return self.api['api-policy'].add_item({'name': name}, {})

    def create_policy_from_obj(self, policy_obj):
        return self.api['api-policy'].add_item(policy_obj, {})

    def insert_rule(self, rule, policy):
        context = {'policy_id': policy}
        return self.api['api-rule'].add_item(
            {'rule': rule}, {}, context=context)

    def create_fake_datasource(self, name):
        item = {'name': name,
                'driver': 'fake_datasource',
                'description': 'hello world!',
                'enabled': True,
                'type': None,
                'config': {'auth_url': 'foo',
                           'username': 'armax',
                           'password': '<hidden>',
                           'tenant_name': 'armax'}}

        return self.api['api-datasource'].add_item(item, params={})

    def query(self, tablename, policyname):
        context = {'policy_id': policyname,
                   'table_id': tablename}
        return self.api['api-row'].get_items({}, context)

    def test_rule_insert_delete(self):
        self.api['api-policy'].add_item({'name': 'alice'}, {})
        context = {'policy_id': 'alice'}
        (id1, _) = self.api['api-rule'].add_item(
            {'rule': 'p(x) :- plus(y, 1, x), q(y)'}, {}, context=context)
        ds = self.api['api-rule'].get_items({}, context)['results']
        self.assertEqual(len(ds), 1)
        self.api['api-rule'].delete_item(id1, {}, context)
        ds = self.engine.policy_object('alice').content()
        self.assertEqual(len(ds), 0)

    def test_datasource_request_refresh(self):
        # neutron polls automatically here, which is why register_service
        # starts its service.
        neutron = self.neutronv2
        neutron.stop()

        self.assertEqual(neutron.refresh_request_queue.qsize(), 0)
        neutron.request_refresh()
        self.assertEqual(neutron.refresh_request_queue.qsize(), 1)
        neutron.start()

        neutron.request_refresh()
        f = lambda: neutron.refresh_request_queue.qsize()
        helper.retry_check_function_return_value(f, 0)

    def test_datasource_poll(self):
        neutron = self.neutronv2
        neutron.stop()
        neutron._translate_ports({'ports': []})
        self.assertEqual(len(neutron.state['ports']), 0)
        neutron.start()
        f = lambda: len(neutron.state['ports'])
        helper.retry_check_function_return_value_not_eq(f, 0)

    def test_library_service(self):
        # NOTE(ekcs): only the most basic test right now, more detailed testing
        # done in test_library_service.py
        res = self.library.get_policies()
        self.assertEqual(res, [])

    def test_load_library_policies(self):
        self.assertEqual(self.library.load_policies_from_files(), 0)


class APILocalRouting(BaseTestPolicyCongress):

    def setUp(self):
        super(APILocalRouting, self).setUp()

        # set up second API+PE node
        self.services = tests_api_base.setup_config(
            with_fake_datasource=False, node_id='testnode2',
            same_partition_as_node=self.node)
        self.api2 = self.services['api']
        self.node2 = self.services['node']
        self.engine2 = self.services['engine']
        self.data = self.services['data']

        # add different data to two PE instances
        # going directly to agnostic not via API to make sure faulty API
        # routing (subject of the test) would not affect test accuracy
        self.engine.create_policy('policy')
        self.engine2.create_policy('policy')
        self.engine.insert('p(1) :- NOT q()', 'policy')
        # self.engine1.insert('p(1)', 'policy')
        self.engine2.insert('p(2) :- NOT q()', 'policy')
        self.engine2.insert('p(3) :- NOT q()', 'policy')

    def test_intranode_pe_routing(self):
        for i in range(0, 5):  # run multiple times (non-determinism)
            result = self.api['api-row'].get_items(
                {}, {'policy_id': 'policy', 'table_id': 'p'})
            self.assertEqual(len(result['results']), 1)
            result = self.api2['api-row'].get_items(
                {}, {'policy_id': 'policy', 'table_id': 'p'})
            self.assertEqual(len(result['results']), 2)

    def test_non_PE_service_reachable(self):
        # intranode
        result = self.api['api-row'].get_items(
            {}, {'ds_id': 'neutron', 'table_id': 'ports'})
        self.assertEqual(len(result['results']), 1)

        # internode
        result = self.api2['api-row'].get_items(
            {}, {'ds_id': 'neutron', 'table_id': 'ports'})
        self.assertEqual(len(result['results']), 1)

    def test_internode_pe_routing(self):
        '''test reach internode PE when intranode PE not available'''
        self.node.unregister_service(api_base.ENGINE_SERVICE_ID)
        result = self.api['api-row'].get_items(
            {}, {'policy_id': 'policy', 'table_id': 'p'})
        self.assertEqual(len(result['results']), 2)
        result = self.api2['api-row'].get_items(
            {}, {'policy_id': 'policy', 'table_id': 'p'})
        self.assertEqual(len(result['results']), 2)


class TestPolicyExecute(BaseTestPolicyCongress):

    def setUp(self):
        super(TestPolicyExecute, self).setUp()
        self.nova = self._register_test_datasource('nova')

    def _register_test_datasource(self, name):
        args = helper.datasource_openstack_args()
        if name == 'nova':
            ds = nova_driver.NovaDriver('nova', args=args)
        if name == 'neutron':
            ds = neutronv2_driver.NeutronV2Driver('neutron', args=args)
        ds.update_from_datasource = mock.MagicMock()
        return ds

    def test_policy_execute(self):
        class NovaClient(object):
            def __init__(self, testkey):
                self.testkey = testkey

            def disconnectNetwork(self, arg1):
                LOG.info("disconnectNetwork called on %s", arg1)
                self.testkey = "arg1=%s" % arg1

        nova_client = NovaClient("testing")
        nova = self.nova
        nova.nova_client = nova_client
        self.node.register_service(nova)

        # insert rule and data
        self.api['api-policy'].add_item({'name': 'alice'}, {})
        (id1, _) = self.api['api-rule'].add_item(
            {'rule': 'execute[nova:disconnectNetwork(x)] :- q(x)'}, {},
            context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 0)
        (id2, _) = self.api['api-rule'].add_item(
            {'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 1)
        ans = "arg1=1"
        f = lambda: nova.nova_client.testkey
        helper.retry_check_function_return_value(f, ans)

        # insert more data
        self.api['api-rule'].add_item(
            {'rule': 'q(2)'}, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 2)
        ans = "arg1=2"
        f = lambda: nova.nova_client.testkey
        helper.retry_check_function_return_value(f, ans)

        # insert irrelevant data
        self.api['api-rule'].add_item(
            {'rule': 'r(3)'}, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 2)

        # delete relevant data
        self.api['api-rule'].delete_item(
            id2, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 2)

        # delete policy rule
        self.api['api-rule'].delete_item(
            id1, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 2)

    def test_policy_execute_data_first(self):
        class NovaClient(object):
            def __init__(self, testkey):
                self.testkey = testkey

            def disconnectNetwork(self, arg1):
                LOG.info("disconnectNetwork called on %s", arg1)
                self.testkey = "arg1=%s" % arg1

        nova_client = NovaClient(None)
        nova = self.nova
        nova.nova_client = nova_client
        self.node.register_service(nova)

        # insert rule and data
        self.api['api-policy'].add_item({'name': 'alice'}, {})
        self.api['api-rule'].add_item(
            {'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 0)
        self.api['api-rule'].add_item(
            {'rule': 'execute[nova:disconnectNetwork(x)] :- q(x)'}, {},
            context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 1)
        ans = "arg1=1"
        f = lambda: nova.nova_client.testkey
        helper.retry_check_function_return_value(f, ans)

    def test_policy_execute_dotted(self):
        class NovaClient(object):
            def __init__(self, testkey):
                self.testkey = testkey
                self.servers = ServersClass()

        class ServersClass(object):
            def __init__(self):
                self.ServerManager = ServerManagerClass()

        class ServerManagerClass(object):
            def __init__(self):
                self.testkey = None

            def pause(self, id_):
                self.testkey = "arg1=%s" % id_

        nova_client = NovaClient(None)
        nova = self.nova
        nova.nova_client = nova_client
        self.node.register_service(nova)

        self.api['api-policy'].add_item({'name': 'alice'}, {})
        self.api['api-rule'].add_item(
            {'rule': 'execute[nova:servers.ServerManager.pause(x)] :- q(x)'},
            {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 0)
        self.api['api-rule'].add_item(
            {'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 1)
        ans = "arg1=1"
        f = lambda: nova.nova_client.servers.ServerManager.testkey
        helper.retry_check_function_return_value(f, ans)

    def test_policy_execute_no_args(self):
        class NovaClient(object):
            def __init__(self, testkey):
                self.testkey = testkey

            def disconnectNetwork(self):
                LOG.info("disconnectNetwork called")
                self.testkey = "noargs"

        nova_client = NovaClient(None)
        nova = self.nova
        nova.nova_client = nova_client
        self.node.register_service(nova)

        # Note: this probably isn't the behavior we really want.
        #  But at least we have a test documenting that behavior.

        # insert rule and data
        self.api['api-policy'].add_item({'name': 'alice'}, {})
        (id1, rule1) = self.api['api-rule'].add_item(
            {'rule': 'execute[nova:disconnectNetwork()] :- q(x)'}, {},
            context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 0)
        (id2, rule2) = self.api['api-rule'].add_item(
            {'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 1)
        ans = "noargs"
        f = lambda: nova.nova_client.testkey
        helper.retry_check_function_return_value(f, ans)

        # insert more data (which DOES NOT cause an execution)
        (id3, rule3) = self.api['api-rule'].add_item(
            {'rule': 'q(2)'}, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 1)

        # delete all data
        self.api['api-rule'].delete_item(
            id2, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 1)

        self.api['api-rule'].delete_item(
            id3, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 1)

        # insert data (which now DOES cause an execution)
        (id4, rule3) = self.api['api-rule'].add_item(
            {'rule': 'q(3)'}, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 2)
        ans = "noargs"
        f = lambda: nova.nova_client.testkey
        helper.retry_check_function_return_value(f, ans)

        # delete policy rule
        self.api['api-rule'].delete_item(
            id1, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 2)

    def test_neutron_policy_execute(self):
        class NeutronClient(object):
            def __init__(self, testkey):
                self.testkey = testkey

            def disconnectNetwork(self, arg1):
                LOG.info("disconnectNetwork called on %s", arg1)
                self.testkey = "arg1=%s" % arg1

        neutron_client = NeutronClient(None)
        neutron = self.neutronv2
        neutron.neutron = neutron_client

        # insert rule and data
        self.api['api-policy'].add_item({'name': 'alice'}, {})
        (id1, _) = self.api['api-rule'].add_item(
            {'rule': 'execute[neutron:disconnectNetwork(x)] :- q(x)'}, {},
            context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 0)
        (id2, _) = self.api['api-rule'].add_item(
            {'rule': 'q(1)'}, {}, context={'policy_id': 'alice'})
        self.assertEqual(len(self.engine.logger.messages), 1)
        ans = "arg1=1"
        f = lambda: neutron.neutron.testkey
        helper.retry_check_function_return_value(f, ans)

    def test_neutron_policy_poll_and_subscriptions(self):
        """Test polling and publishing of neutron updates."""
        policy = self.engine.DEFAULT_THEORY
        neutron2 = self._create_neutron_mock('neutron2')
        self.engine.initialize_datasource('neutron',
                                          self.neutronv2.get_schema())
        self.engine.initialize_datasource('neutron2',
                                          self.neutronv2.get_schema())
        str_rule = ('p(x0, y0) :- neutron:networks(x0, x1, x2, x3, x4, x5), '
                    'neutron2:networks(y0, y1, y2, y3, y4, y5)')
        rule = {'rule': str_rule, 'name': 'testrule1', 'comment': 'test'}
        self.api['api-rule'].add_item(rule, {}, context={'policy_id': policy})
        # Test policy subscriptions
        subscriptions = self.engine.subscription_list()
        self.assertEqual(sorted([('neutron', 'networks'),
                         ('neutron2', 'networks')]), sorted(subscriptions))
        # Test multiple instances
        self.neutronv2.poll()
        neutron2.poll()
        ans = ('p("240ff9df-df35-43ae-9df5-27fae87f2492", '
               '  "240ff9df-df35-43ae-9df5-27fae87f2492") ')
        helper.retry_check_db_equal(self.engine, 'p(x, y)', ans, target=policy)
