# Copyright 2013 Cloudbase Solutions Srl
#
#    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 sys
import unittest

try:
    import unittest.mock as mock
except ImportError:
    import mock

from cloudbaseinit import conf as cloudbaseinit_conf
from cloudbaseinit import init
from cloudbaseinit.plugins.common import base
from cloudbaseinit.tests import testutils

CONF = cloudbaseinit_conf.CONF


class TestInitManager(unittest.TestCase):

    def setUp(self):
        self._win32com_mock = mock.MagicMock()
        self._comtypes_mock = mock.MagicMock()
        self._pywintypes_mock = mock.MagicMock()
        self._ctypes_mock = mock.MagicMock()
        self._ctypes_util_mock = mock.MagicMock()

        self._module_patcher = mock.patch.dict(
            'sys.modules',
            {'ctypes.util': self._ctypes_util_mock,
             'win32com': self._win32com_mock,
             'comtypes': self._comtypes_mock,
             'pywintypes': self._pywintypes_mock,
             'ctypes': self._ctypes_mock})

        self._module_patcher.start()

        self.osutils = mock.MagicMock()
        self.plugin = mock.MagicMock()

        self._init = init.InitManager()

    def tearDown(self):
        self._module_patcher.stop()

    def _test_get_plugin_section(self, instance_id):
        response = self._init._get_plugins_section(instance_id=instance_id)
        if not instance_id:
            self.assertEqual(self._init._PLUGINS_CONFIG_SECTION, response)
        else:
            self.assertEqual(
                instance_id + "/" + self._init._PLUGINS_CONFIG_SECTION,
                response)

    def test_get_plugin_section_id(self):
        fake_id = "100"
        self._test_get_plugin_section(instance_id=fake_id)

    def test_get_plugin_section_no_id(self):
        self._test_get_plugin_section(instance_id=None)

    @mock.patch('cloudbaseinit.init.InitManager._get_plugins_section')
    def test_get_plugin_status(self, mock_get_plugins_section):
        self.osutils.get_config_value.return_value = 1
        response = self._init._get_plugin_status(self.osutils, 'fake id',
                                                 'fake plugin')
        mock_get_plugins_section.assert_called_once_with('fake id')
        self.osutils.get_config_value.assert_called_once_with(
            'fake plugin', mock_get_plugins_section())
        self.assertTrue(response == 1)

    @mock.patch('cloudbaseinit.init.InitManager._get_plugins_section')
    def test_set_plugin_status(self, mock_get_plugins_section):
        self._init._set_plugin_status(self.osutils, 'fake id',
                                      'fake plugin', 'status')
        mock_get_plugins_section.assert_called_once_with('fake id')
        self.osutils.set_config_value.assert_called_once_with(
            'fake plugin', 'status', mock_get_plugins_section())

    @mock.patch('cloudbaseinit.init.InitManager._get_plugin_status')
    @mock.patch('cloudbaseinit.init.InitManager._set_plugin_status')
    def _test_exec_plugin(self, status, mock_set_plugin_status,
                          mock_get_plugin_status):
        fake_name = 'fake name'
        self.plugin.get_name.return_value = fake_name
        self.plugin.execute.return_value = (status, True)
        mock_get_plugin_status.return_value = status

        response = self._init._exec_plugin(osutils=self.osutils,
                                           service='fake service',
                                           plugin=self.plugin,
                                           instance_id='fake id',
                                           shared_data='shared data')

        mock_get_plugin_status.assert_called_once_with(self.osutils,
                                                       'fake id',
                                                       fake_name)
        if status is base.PLUGIN_EXECUTE_ON_NEXT_BOOT:
            self.plugin.execute.assert_called_once_with('fake service',
                                                        'shared data')
            mock_set_plugin_status.assert_called_once_with(self.osutils,
                                                           'fake id',
                                                           fake_name, status)
            self.assertTrue(response)

    def test_exec_plugin_exception_occurs(self):
        fake_name = 'fake name'
        mock_plugin = mock.MagicMock()
        mock_plugin.get_name.return_value = fake_name
        mock_plugin.execute.side_effect = Exception
        expected_logging = ["Executing plugin 'fake name'",
                            "plugin 'fake name' failed with error ''"]
        with testutils.LogSnatcher('cloudbaseinit.init') as snatcher:
            self._init._exec_plugin(osutils=self.osutils,
                                    service='fake service',
                                    plugin=mock_plugin,
                                    instance_id='fake id',
                                    shared_data='shared data')
        self.assertEqual(expected_logging, snatcher.output[:2])

    def test_exec_plugin_execution_done(self):
        self._test_exec_plugin(base.PLUGIN_EXECUTION_DONE)

    def test_exec_plugin(self):
        self._test_exec_plugin(base.PLUGIN_EXECUTE_ON_NEXT_BOOT)

    def _test_check_plugin_os_requirements(self, requirements):
        sys.platform = 'win32'
        fake_name = 'fake name'
        self.plugin.get_name.return_value = fake_name
        self.plugin.get_os_requirements.return_value = requirements

        response = self._init._check_plugin_os_requirements(self.osutils,
                                                            self.plugin)

        self.plugin.get_name.assert_called_once_with()
        self.plugin.get_os_requirements.assert_called_once_with()
        if requirements[0] == 'win32':
            self.assertTrue(response)
        else:
            self.assertFalse(response)

    def test_check_plugin_os_requirements(self):
        self._test_check_plugin_os_requirements(('win32', (5, 2)))

    def test_check_plugin_os_requirements_other_requirenments(self):
        self._test_check_plugin_os_requirements(('linux', (5, 2)))

    def test_check_plugins_os_not_min_os_version(self):
        sys.platform = 'win32'
        fake_name = 'fake name'
        self.plugin.get_name.return_value = fake_name
        self.plugin.get_os_requirements.return_value = ('win32', 0)
        response = self._init._check_plugin_os_requirements(self.osutils,
                                                            self.plugin)
        self.plugin.get_name.assert_called_once_with()
        self.assertTrue(response)

    def test_check_plugins_os_not_supported(self):
        fake_name = 'fake name'
        self.plugin.get_name.return_value = fake_name
        mock_osutils = mock.MagicMock()
        mock_osutils.check_os_version.return_value = None
        self.plugin.get_os_requirements.return_value = ('win32', (5, 2))
        response = self._init._check_plugin_os_requirements(mock_osutils,
                                                            self.plugin)
        self.assertFalse(response)

    @mock.patch('cloudbaseinit.init.InitManager.'
                '_exec_plugin')
    @mock.patch('cloudbaseinit.init.InitManager.'
                '_check_plugin_os_requirements')
    @mock.patch('cloudbaseinit.plugins.factory.load_plugins')
    def _test_handle_plugins_stage(self, mock_load_plugins,
                                   mock_check_plugin_os_requirements,
                                   mock_exec_plugin,
                                   reboot=True, fast_reboot=True):
        stage = "fake stage"
        service, instance_id = mock.Mock(), mock.Mock()
        plugins = [mock.Mock() for _ in range(3)]
        mock_check_plugin_os_requirements.return_value = True
        mock_exec_plugin.return_value = reboot
        mock_load_plugins.return_value = plugins
        requirements_calls = [mock.call(self.osutils, plugin)
                              for plugin in plugins]
        exec_plugin_calls = [mock.call(self.osutils, service, plugin,
                                       instance_id, {})
                             for plugin in plugins]

        with testutils.LogSnatcher('cloudbaseinit.init') as snatcher:
            response = self._init._handle_plugins_stage(
                self.osutils, service, instance_id, stage)
        self.assertEqual(
            ["Executing plugins for stage '{}':".format(stage)],
            snatcher.output)
        mock_load_plugins.assert_called_once_with(stage)
        idx = 1 if (reboot and fast_reboot) else len(plugins)
        mock_check_plugin_os_requirements.assert_has_calls(
            requirements_calls[:idx])
        mock_exec_plugin.assert_has_calls(exec_plugin_calls[:idx])
        self.assertEqual(reboot, response)

    def test_handle_plugins_stage(self):
        self._test_handle_plugins_stage()

    def test_handle_plugins_stage_no_reboot(self):
        self._test_handle_plugins_stage(reboot=False, fast_reboot=False)

    @testutils.ConfPatcher('allow_reboot', False)
    def test_handle_plugins_stage_no_fast_reboot(self):
        self._test_handle_plugins_stage(fast_reboot=False)

    @mock.patch('cloudbaseinit.init.InitManager.'
                '_reset_service_password_and_respawn')
    @mock.patch('cloudbaseinit.init.InitManager'
                '._handle_plugins_stage')
    @mock.patch('cloudbaseinit.init.InitManager._check_latest_version')
    @mock.patch('cloudbaseinit.version.get_version')
    @mock.patch('cloudbaseinit.plugins.factory.load_plugins')
    @mock.patch('cloudbaseinit.osutils.factory.get_os_utils')
    @mock.patch('cloudbaseinit.metadata.factory.get_metadata_service')
    def _test_configure_host(self, mock_get_metadata_service,
                             mock_get_os_utils, mock_load_plugins,
                             mock_get_version, mock_check_latest_version,
                             mock_handle_plugins_stage, mock_reset_service,
                             expected_logging,
                             version, name, instance_id, reboot=True):
        sys.platform = 'win32'
        mock_get_version.return_value = version
        fake_service = mock.MagicMock()
        fake_plugin = mock.MagicMock()
        mock_load_plugins.return_value = [fake_plugin]
        mock_get_os_utils.return_value = self.osutils
        mock_get_metadata_service.return_value = fake_service
        fake_service.get_name.return_value = name
        fake_service.get_instance_id.return_value = instance_id
        mock_handle_plugins_stage.side_effect = [False, False, True]
        stages = [
            base.PLUGIN_STAGE_PRE_NETWORKING,
            base.PLUGIN_STAGE_PRE_METADATA_DISCOVERY,
            base.PLUGIN_STAGE_MAIN]
        stage_calls_list = [[self.osutils, None, None, stage]
                            for stage in stages]
        stage_calls_list[2][1] = fake_service
        stage_calls_list[2][2] = instance_id
        stage_calls = [mock.call(*args) for args in stage_calls_list]

        with testutils.LogSnatcher('cloudbaseinit.init') as snatcher:
            self._init.configure_host()
        self.assertEqual(expected_logging, snatcher.output)
        mock_check_latest_version.assert_called_once_with()
        if CONF.reset_service_password:
            mock_reset_service.assert_called_once_with(self.osutils)

        self.osutils.wait_for_boot_completion.assert_called_once_with()
        mock_get_metadata_service.assert_called_once_with()
        fake_service.get_name.assert_called_once_with()
        fake_service.get_instance_id.assert_called_once_with()
        fake_service.cleanup.assert_called_once_with()
        mock_handle_plugins_stage.assert_has_calls(stage_calls)
        if reboot:
            self.osutils.reboot.assert_called_once_with()
        else:
            self.assertFalse(self.osutils.reboot.called)

    def _test_configure_host_with_logging(self, extra_logging, reboot=True):
        instance_id = 'fake id'
        name = 'fake name'
        version = 'version'
        expected_logging = [
            'Cloudbase-Init version: %s' % version,
            'Metadata service loaded: %r' % name,
            'Instance id: %s' % instance_id,
        ]
        self._test_configure_host(
            expected_logging=expected_logging + extra_logging,
            version=version, name=name, instance_id=instance_id,
            reboot=reboot)

    @testutils.ConfPatcher('allow_reboot', False)
    @testutils.ConfPatcher('stop_service_on_exit', False)
    def test_configure_host_no_reboot_no_service_stopping(self):
        self._test_configure_host_with_logging(
            reboot=False,
            extra_logging=['Plugins execution done'])

    @testutils.ConfPatcher('allow_reboot', False)
    @testutils.ConfPatcher('stop_service_on_exit', True)
    def test_configure_host_no_reboot_allow_service_stopping(self):
        self._test_configure_host_with_logging(
            reboot=False,
            extra_logging=['Plugins execution done',
                           'Stopping Cloudbase-Init service'])
        self.osutils.terminate.assert_called_once_with()

    @testutils.ConfPatcher('allow_reboot', True)
    def test_configure_host_reboot(self):
        self._test_configure_host_with_logging(
            extra_logging=['Rebooting'])

    @testutils.ConfPatcher('check_latest_version', False)
    @mock.patch('cloudbaseinit.version.check_latest_version')
    def test_configure_host(self, mock_check_last_version):
        self._init._check_latest_version()

        self.assertFalse(mock_check_last_version.called)

    @testutils.ConfPatcher('check_latest_version', True)
    @mock.patch('functools.partial')
    @mock.patch('cloudbaseinit.version.check_latest_version')
    def test_configure_host_with_version_check(self, mock_check_last_version,
                                               mock_partial):
        self._init._check_latest_version()

        mock_check_last_version.assert_called_once_with(
            mock_partial.return_value)
        mock_partial.assert_called_once_with(
            init.LOG.info, 'Found new version of cloudbase-init %s')

    @mock.patch('os.path.basename')
    @mock.patch("sys.executable")
    @mock.patch("sys.argv")
    @mock.patch("sys.exit")
    def _test_reset_service_password_and_respawn(self, mock_exit, mock_argv,
                                                 mock_executable, mock_os_path,
                                                 credentials, current_user):
        token = mock.sentinel.token
        self.osutils.create_user_logon_session.return_value = token
        self.osutils.execute_process_as_user.return_value = 0
        self.osutils.reset_service_password.return_value = credentials
        self.osutils.get_current_user.return_value = current_user
        expected_logging = []
        arguments = sys.argv + ["--noreset_service_password"]

        with testutils.LogSnatcher('cloudbaseinit.init') as snatcher:
            self._init._reset_service_password_and_respawn(self.osutils)

        if not credentials:
            return

        if credentials[1] != current_user[1]:
            expected_logging = [
                "No need to respawn process. Current user: "
                "%(current_user)s. Service user: %(service_user)s" %
                {"current_user": current_user[1],
                 "service_user": credentials[1]}
            ]
            self.assertEqual(expected_logging, snatcher.output)
        else:
            self.osutils.create_user_logon_session.assert_called_once_with(
                credentials[1], credentials[2], credentials[0],
                logon_type=self.osutils.LOGON32_LOGON_BATCH)
            self.osutils.execute_process_as_user.assert_called_once_with(
                token, arguments)
            mock_exit.assert_called_once_with(0)

    def test_reset_service_password_and_respawn(self):
        current_user = [mock.sentinel.domain, mock.sentinel.current_user]
        self._test_reset_service_password_and_respawn(
            credentials=None,
            current_user=current_user
        )
        self._test_reset_service_password_and_respawn(
            credentials=[mock.sentinel.domain, mock.sentinel.user,
                         mock.sentinel.password],
            current_user=current_user
        )
        self._test_reset_service_password_and_respawn(
            credentials=[mock.sentinel.domain, mock.sentinel.current_user,
                         mock.sentinel.password],
            current_user=current_user
        )
