#    Copyright (c) 2015 Telefonica I+D
#    Copyright (c) 2016 AT&T Corp
#
#    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 copy
import datetime
import json
import os
import tempfile
from unittest import mock

from oslo_serialization import base64
import yaml as yamllib

from murano.common import exceptions
from murano.dsl import dsl
from murano.dsl import murano_object
from murano.dsl import murano_type
from murano.dsl import object_store
from murano.engine.system import agent
from murano.engine.system import resource_manager
from murano.tests.unit import base


class TestAgent(base.MuranoTestCase):
    def setUp(self):
        super(TestAgent, self).setUp()
        if hasattr(yamllib, 'CSafeLoader'):
            self.yaml_loader = yamllib.CSafeLoader
        else:
            self.yaml_loader = yamllib.SafeLoader

        self.override_config('disable_murano_agent', False, group='engine')
        self.override_config('signing_key', False, group='engine')

        mock_host = mock.MagicMock()
        mock_host.id = '1234'
        mock_host.find_owner = lambda *args, **kwargs: mock_host
        mock_host().getRegion.return_value = mock.MagicMock(
            __class__=dsl.MuranoObjectInterface)
        self.rabbit_mq_settings = {
            'agentRabbitMq': {'login': 'test_login',
                              'password': 'test_password',
                              'host': 'test_host',
                              'port': 123,
                              'virtual_host': 'test_virtual_host'}
        }
        mock_host().getRegion()().getConfig.return_value =\
            self.rabbit_mq_settings

        self.agent = agent.Agent(mock_host)
        self.resources = mock.Mock(spec=resource_manager.ResourceManager)
        self.resources.string.return_value = 'text'

        self.addCleanup(mock.patch.stopall)

    def _read(self, path):
        execution_plan_dir = os.path.abspath(
            os.path.join(__file__, '../execution_plans/')
        )
        with open(execution_plan_dir + "/" + path) as file:
            return file.read()

    @mock.patch('murano.common.messaging.mqclient.kombu')
    def test_send_creates_queue(self, mock_kombu):
        self.agent.send_raw({})

        # Verify that MQClient was instantiated, by checking whether
        # kombu.Connection was called.
        mock_kombu.Connection.assert_called_with(
            'amqp://{login}:{password}@{host}:{port}/{virtual_host}'.format(
                **self.rabbit_mq_settings['agentRabbitMq']
            ), ssl=None)

        # Verify that client.declare() was called by checking whether kombu
        # functions were called.
        self.assertEqual(1, mock_kombu.Exchange.call_count)
        self.assertEqual(1, mock_kombu.Queue.call_count)

    @mock.patch('murano.engine.system.agent.LOG')
    def test_send_with_murano_agent_disabled(self, mock_log):
        self.override_config('disable_murano_agent', True, group='engine')

        self.assertRaises(exceptions.PolicyViolationException,
                          self.agent.send_raw, {})

    @mock.patch('murano.engine.system.agent.Agent._sign')
    @mock.patch('murano.common.messaging.mqclient.kombu')
    def test_send(self, mock_kombu, mock_sign):
        template = yamllib.load(
            self._read('template_with_files.template'),
            Loader=self.yaml_loader)

        self.agent._queue = 'test_queue'
        mock_sign.return_value = 'SIGNATURE'
        plan = self.agent.build_execution_plan(template, self.resources)
        with mock.patch.object(self.agent, 'build_execution_plan',
                               return_value=plan):
            self.agent.send(template, self.resources)

        self.assertEqual(1, mock_kombu.Producer.call_count)
        mock_kombu.Producer().publish.assert_called_once_with(
            exchange='',
            routing_key='test_queue',
            body=json.dumps(plan),
            message_id=plan['ID'],
            headers={'signature': 'SIGNATURE'}
        )

    @mock.patch('murano.engine.system.agent.eventlet.event.Event')
    @mock.patch('murano.common.messaging.mqclient.kombu')
    def test_is_ready(self, mock_kombu, mock_event):
        v2_result = yamllib.load(
            self._read('application.template'),
            Loader=self.yaml_loader)

        mock_event().wait.side_effect = None
        mock_event().wait.return_value = v2_result

        self.assertTrue(self.agent.is_ready(1))

        mock_event().wait.side_effect = agent.eventlet.Timeout

        self.assertFalse(self.agent.is_ready(1))

    @mock.patch('murano.engine.system.agent.Agent._sign')
    @mock.patch('murano.engine.system.agent.eventlet.event.Event')
    @mock.patch('murano.common.messaging.mqclient.kombu')
    def test_call_with_v1_result(self, mock_kombu, mock_event, mock_sign):
        template = yamllib.load(
            self._read('template_with_files.template'),
            Loader=self.yaml_loader)

        test_v1_result = {
            'FormatVersion': '1.0.0',
            'IsException': False,
            'Result': [
                {
                    'IsException': False,
                    'Result': 'test_result'
                }
            ]
        }

        mock_event().wait.side_effect = None
        mock_event().wait.return_value = test_v1_result
        mock_sign.return_value = 'SIGNATURE'

        self.agent._queue = 'test_queue'
        plan = self.agent.build_execution_plan(template, self.resources)
        mock.patch.object(
            self.agent, 'build_execution_plan', return_value=plan).start()

        result = self.agent.call(template, self.resources, None)
        self.assertIsNotNone(result)
        self.assertEqual('test_result', result)

        self.assertEqual(1, mock_kombu.Producer.call_count)
        mock_kombu.Producer().publish.assert_called_once_with(
            exchange='',
            routing_key='test_queue',
            body=json.dumps(plan),
            message_id=plan['ID'],
            headers={'signature': 'SIGNATURE'}
        )

    @mock.patch('murano.engine.system.agent.Agent._sign')
    @mock.patch('murano.engine.system.agent.eventlet.event.Event')
    @mock.patch('murano.common.messaging.mqclient.kombu')
    def test_call_with_v2_result(self, mock_kombu, mock_event, mock_sign):
        template = yamllib.load(
            self._read('template_with_files.template'),
            Loader=self.yaml_loader)

        v2_result = yamllib.load(
            self._read('application.template'),
            Loader=self.yaml_loader)

        mock_event().wait.side_effect = None
        mock_event().wait.return_value = v2_result
        mock_sign.return_value = 'SIGNATURE'

        self.agent._queue = 'test_queue'
        plan = self.agent.build_execution_plan(template, self.resources)
        mock.patch.object(
            self.agent, 'build_execution_plan', return_value=plan).start()

        result = self.agent.call(template, self.resources, None)
        self.assertIsNotNone(result)
        self.assertEqual(v2_result['Body'], result)

        self.assertEqual(1, mock_kombu.Producer.call_count)
        mock_kombu.Producer().publish.assert_called_once_with(
            exchange='',
            routing_key='test_queue',
            body=json.dumps(plan),
            message_id=plan['ID'],
            headers={'signature': 'SIGNATURE'}
        )

    @mock.patch('murano.engine.system.agent.eventlet.event.Event')
    @mock.patch('murano.common.messaging.mqclient.kombu')
    def test_call_with_no_result(self, mock_kombu, mock_event):
        template = yamllib.load(
            self._read('template_with_files.template'),
            Loader=self.yaml_loader)

        mock_event().wait.side_effect = None
        mock_event().wait.return_value = None

        result = self.agent.call(template, self.resources, None)
        self.assertIsNone(result)

    @mock.patch('murano.engine.system.agent.eventlet.event.Event')
    @mock.patch('murano.common.messaging.mqclient.kombu')
    def test_call_except_timeout(self, mock_kombu, mock_event):
        self.override_config('agent_timeout', 1, group='engine')

        mock_event().wait.side_effect = agent.eventlet.Timeout

        template = yamllib.load(
            self._read('template_with_files.template'),
            Loader=self.yaml_loader)

        expected_error_msg = 'The murano-agent did not respond within 1 '\
                             'seconds'
        with self.assertRaisesRegex(exceptions.TimeoutException,
                                    expected_error_msg):
            self.agent.call(template, self.resources, None)

    @mock.patch('murano.engine.system.agent.datetime')
    def test_process_v1_result_with_error_code(self, mock_datetime):
        now = datetime.datetime.now().isoformat()
        mock_datetime.datetime.now().isoformat.return_value = now

        v1_result = {
            'IsException': True,
            'Result': [
                'Error Type',
                'Error Message',
                'Error Command',
                'Error Details'
            ]
        }

        expected_error = {
            'source': 'execution_plan',
            'command': 'Error Command',
            'details': 'Error Details',
            'message': 'Error Message',
            'type': 'Error Type',
            'timestamp': now
        }

        self.assertTrue(self._are_exceptions_equal(
            agent.AgentException, expected_error,
            self.agent._process_v1_result, v1_result))

        v1_result = {
            'IsException': False,
            'Result': [
                'Error Type',
                'Error Message',
                'Error Command',
                'Error Details',
                {
                    'IsException': True,
                    'Result': [
                        'Nested Error Type',
                        'Nested Error Message',
                        'Nested Error Command',
                        'Nested Error Details'
                    ]
                }
            ]
        }

        expected_error = {
            'source': 'command',
            'command': 'Nested Error Command',
            'details': 'Nested Error Details',
            'message': 'Nested Error Message',
            'type': 'Nested Error Type',
            'timestamp': now
        }

        self.assertTrue(self._are_exceptions_equal(
            agent.AgentException, expected_error,
            self.agent._process_v1_result, v1_result))

    def test_process_v2_result_with_error_code(self):
        v2_result = {
            'Body': {
                'Message': 'Test Error Message',
                'AdditionalInfo': 'Test Additional Info',
                'ExtraAttr': 'Test extra data'
            },
            'FormatVersion': '2.0.0',
            'Name': 'TestApp',
            'ErrorCode': 123,
            'Time': 'Right now'
        }

        expected_error = {
            'errorCode': 123,
            'message': 'Test Error Message',
            'details': 'Test Additional Info',
            'time': 'Right now',
            'extra': {'ExtraAttr': 'Test extra data'}
        }

        self.assertTrue(self._are_exceptions_equal(
            agent.AgentException, expected_error,
            self.agent._process_v2_result, v2_result))

    def _are_exceptions_equal(self, exception, expected_error, function,
                              result):
        """Checks whether expected and returned dict from exception are equal.

        Because casting dicts to strings changes the ordering of the keys,
        manual comparison of the result and expected result is performed.
        """
        try:
            # deepcopy must be performed because _process_v1_result
            # deletes attrs from the original dict passed in.
            self.assertRaises(exception, function, copy.deepcopy(result))
            function(result)
        except exception as e:
            e_string = str(e).replace("'", "\"").replace('None', 'null')
            e_dict = json.loads(e_string)
            self.assertEqual(sorted(expected_error.keys()),
                             sorted(e_dict.keys()))
            for key, val in expected_error.items():
                self.assertEqual(val, e_dict[key])
        except Exception:
            return False
        return True


class TestExecutionPlan(base.MuranoTestCase):
    def setUp(self):
        super(TestExecutionPlan, self).setUp()
        if hasattr(yamllib, 'CSafeLoader'):
            self.yaml_loader = yamllib.CSafeLoader
        else:
            self.yaml_loader = yamllib.SafeLoader

        self.override_config('signing_key', False, group='engine')

        self.mock_murano_class = mock.Mock(spec=murano_type.MuranoClass)
        self.mock_murano_class.name = 'io.murano.system.Agent'
        self.mock_murano_class.declared_parents = []
        self.mock_object_store = mock.Mock(spec=object_store.ObjectStore)

        object_interface = mock.Mock(spec=murano_object.MuranoObject)
        object_interface.id = '1234'
        object_interface.find_owner = lambda *args, **kwargs: object_interface

        self.agent = agent.Agent(object_interface)
        self.resources = mock.Mock(spec=resource_manager.ResourceManager)
        self.resources.string.return_value = 'text'
        self.uuids = ['ID1', 'ID2', 'ID3', 'ID4']
        self.mock_uuid = self._stub_uuid(self.uuids)
        time_mock = mock.patch('time.time').start()
        time_mock.return_value = 2
        self.addCleanup(mock.patch.stopall)

    def _read(self, path):
        execution_plan_dir = os.path.abspath(
            os.path.join(__file__, '../execution_plans/')
        )
        with open(execution_plan_dir + "/" + path) as file:
            return file.read()

    def test_execution_plan_v2_application_type(self):
        template = yamllib.load(
            self._read('application.template'),
            Loader=self.yaml_loader)
        template = self.agent.build_execution_plan(template, self.resources)
        self.assertEqual(self._get_application(), template)

    def test_execution_plan_v2_chef_type(self):
        template = yamllib.load(
            self._read('chef.template'),
            Loader=self.yaml_loader)
        template = self.agent.build_execution_plan(template, self.resources)
        self.assertEqual(self._get_chef(), template)

    def test_execution_plan_v2_telnet_application(self):
        template = yamllib.load(
            self._read('DeployTelnet.template'),
            Loader=self.yaml_loader)
        template = self.agent.build_execution_plan(template, self.resources)
        self.assertEqual(self._get_telnet_application(), template)

    def test_execution_plan_v2_tomcat_application(self):
        template = yamllib.load(
            self._read('DeployTomcat.template'),
            Loader=self.yaml_loader)
        template = self.agent.build_execution_plan(template, self.resources)

    def test_execution_plan_v2_app_without_files(self):
        template = yamllib.load(
            self._read('application_without_files.template'),
            Loader=self.yaml_loader)
        template = self.agent.build_execution_plan(template, self.resources)
        self.assertEqual(self._get_app_without_files(), template)

    def test_execution_plan_v2_app_with_file_in_template(self):
        template = yamllib.load(
            self._read('template_with_files.template'),
            Loader=self.yaml_loader)
        template = self.agent.build_execution_plan(template, self.resources)
        self.assertEqual(self._get_app_with_files_in_template(), template)

    def _get_application(self):
        return {
            'Action': 'Execute',
            'Body': 'return deploy(args.appName).stdout\n',
            'Files': {
                self.uuids[1]: {
                    'Body': 'text',
                    'BodyType': 'Text',
                    'Name': 'deployTomcat.sh'
                },
                self.uuids[2]: {
                    'Body': 'dGV4dA==\n',
                    'BodyType': 'Base64',
                    'Name': 'installer'
                },
                self.uuids[3]: {
                    'Body': 'dGV4dA==\n',
                    'BodyType': 'Base64',
                    'Name': 'common.sh'
                }
            },
            'FormatVersion': '2.0.0',
            'ID': self.uuids[0],
            'Stamp': 20000,
            'Name': 'Deploy Tomcat',
            'Parameters': {
                'appName': '$appName'
            },
            'Scripts': {
                'deploy': {
                    'EntryPoint': self.uuids[1],
                    'Files': [
                        self.uuids[2],
                        self.uuids[3]
                    ],
                    'Options': {
                        'captureStderr': True,
                        'captureStdout': True
                    },
                    'Type': 'Application',
                    'Version': '1.0.0'
                }
            },
            'Version': '1.0.0'
        }

    def _get_app_with_files_in_template(self):
        return {
            'Action': 'Execute',
            'Body': 'return deploy(args.appName).stdout\n',
            'Files': {
                self.uuids[1]: {
                    'Body': 'text',
                    'BodyType': 'Text',
                    'Name': 'deployTomcat.sh'
                },
                'updateScript': {
                    'Body': 'text',
                    'BodyType': 'Text',
                    'Name': 'updateScript'
                },
            },
            'FormatVersion': '2.0.0',
            'ID': self.uuids[0],
            'Stamp': 20000,
            'Name': 'Deploy Tomcat',
            'Parameters': {
                'appName': '$appName'
            },
            'Scripts': {
                'deploy': {
                    'EntryPoint': self.uuids[1],
                    'Files': [
                        'updateScript'
                    ],
                    'Options': {
                        'captureStderr': True,
                        'captureStdout': True
                    },
                    'Type': 'Application',
                    'Version': '1.0.0'
                }
            },
            'Version': '1.0.0'
        }

    def _get_app_without_files(self):
        return {
            'Action': 'Execute',
            'Body': 'return deploy(args.appName).stdout\n',
            'Files': {
                self.uuids[1]: {
                    'Body': 'text',
                    'BodyType': 'Text',
                    'Name': 'deployTomcat.sh'
                },
            },
            'FormatVersion': '2.0.0',
            'ID': self.uuids[0],
            'Stamp': 20000,
            'Name': 'Deploy Tomcat',
            'Parameters': {
                'appName': '$appName'
            },
            'Scripts': {
                'deploy': {
                    'EntryPoint': self.uuids[1],
                    'Files': [],
                    'Options': {
                        'captureStderr': True,
                        'captureStdout': True
                    },
                    'Type': 'Application',
                    'Version': '1.0.0'
                }
            },
            'Version': '1.0.0'
        }

    def _get_chef(self):
        return {
            'Action': 'Execute',
            'Body': 'return deploy(args.appName).stdout\n',
            'Files': {
                self.uuids[1]: {
                    'Name': 'tomcat.git',
                    'Type': 'Downloadable',
                    'URL': 'https://github.com/tomcat.git'
                },
                self.uuids[2]: {
                    'Name': 'java',
                    'Type': 'Downloadable',
                    'URL': 'https://github.com/java.git'
                },

            },
            'FormatVersion': '2.0.0',
            'ID': self.uuids[0],
            'Stamp': 20000,
            'Name': 'Deploy Chef',
            'Parameters': {
                'appName': '$appName'
            },
            'Scripts': {
                'deploy': {
                    'EntryPoint': 'cookbook/recipe',
                    'Files': [
                        self.uuids[1],
                        self.uuids[2]
                    ],
                    'Options': {
                        'captureStderr': True,
                        'captureStdout': True
                    },
                    'Type': 'Chef',
                    'Version': '1.0.0'
                }
            },
            'Version': '1.0.0'
        }

    def _get_telnet_application(self):
        return {
            'Action': 'Execute',
            'Body': 'return deploy(args.appName).stdout\n',
            'Files': {
                self.uuids[1]: {
                    'Body': 'text',
                    'BodyType': 'Text',
                    'Name': 'deployTelnet.sh'
                },
                self.uuids[2]: {
                    'Body': 'text',
                    'BodyType': 'Text',
                    'Name': 'installer.sh'
                },
                self.uuids[3]: {
                    'Body': 'text',
                    'BodyType': 'Text',
                    'Name': 'common.sh'
                }
            },
            'FormatVersion': '2.0.0',
            'ID': self.uuids[0],
            'Stamp': 20000,
            'Name': 'Deploy Telnet',
            'Parameters': {
                'appName': '$appName'
            },
            'Scripts': {
                'deploy': {
                    'EntryPoint': self.uuids[1],
                    'Files': [
                        self.uuids[2],
                        self.uuids[3]
                    ],
                    'Options': {
                        'captureStderr': True,
                        'captureStdout': True
                    },
                    'Type': 'Application',
                    'Version': '1.0.0'
                }
            },
            'Version': '1.0.0'
        }

    def _stub_uuid(self, values=None):
        class FakeUUID(object):
            def __init__(self, v):
                self.hex = v

        if values is None:
            values = []
        mock_uuid4 = mock.patch('uuid.uuid4').start()
        mock_uuid4.side_effect = [FakeUUID(v) for v in values]
        return mock_uuid4

    @mock.patch('murano.engine.system.resource_manager.ResourceManager'
                '._get_package')
    def test_file_line_endings(self, _get_package):
        class FakeResources(object):
            """Class with only string() method from ResourceManager class"""
            @staticmethod
            def string(name, owner=None, binary=False):
                return resource_manager.ResourceManager.string(
                    receiver=None, name=name, owner=owner, binary=binary)

        # make path equal to provided name inside resources.string()
        package = mock.Mock()
        package.get_resource.side_effect = lambda m: m
        _get_package.return_value = package

        text = b"First line\nSecond line\rThird line\r\nFourth line"
        modified_text = u"First line\nSecond line\nThird line\nFourth line"
        encoded_text = base64.encode_as_text(text) + "\n"
        resources = FakeResources()

        with tempfile.NamedTemporaryFile() as script_file:
            script_file.write(text)
            script_file.file.flush()
            os.fsync(script_file.file.fileno())

            # check that data has been written correctly
            script_file.seek(0)
            file_data = script_file.read()
            self.assertEqual(text, file_data)

            # check resources.string() output
            # text file
            result = resources.string(script_file.name)
            self.assertEqual(modified_text, result)
            # binary file
            result = resources.string(script_file.name, binary=True)
            self.assertEqual(text, result)

            # check _get_body() output
            filename = os.path.basename(script_file.name)
            folder = os.path.dirname(script_file.name)
            # text file
            body = self.agent._get_body(filename, resources, folder)
            self.assertEqual(modified_text, body)
            # binary file
            filename = '<{0}>'.format(filename)
            body = self.agent._get_body(filename, resources, folder)
            self.assertEqual(encoded_text, body)

    def test_queue_name(self):
        self.agent._queue = 'test_queue'
        self.assertEqual(self.agent.queue_name(), self.agent._queue)

    def test_prepare_message(self):
        template = {'test'}
        msg_id = 12345
        msg = self.agent._prepare_message(template, msg_id)
        self.assertEqual(msg.id, msg_id)
        self.assertEqual(msg._body, template)

    def test_execution_plan_v1(self):
        template = yamllib.load(
            self._read('application.template'),
            Loader=self.yaml_loader)
        rtn_template = self.agent._build_v1_execution_plan(template,
                                                           self.resources)
        self.assertEqual(template, rtn_template)

    def test_get_array_item(self):
        array = [1, 2, 3]
        index = 2
        self.assertEqual(array[2], self.agent._get_array_item(array, index))

        index = 3
        self.assertIsNone(self.agent._get_array_item(array, index))

    def test_execution_plan_error(self):
        template = None
        self.assertRaises(ValueError,
                          self.agent.build_execution_plan,
                          template, self.resources)
