#  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 argparse
import logging
import types
import unittest
from unittest import mock

import shade.exc

from ospurge import exceptions
from ospurge import main
from ospurge.resources.base import ServiceResource
from ospurge import utils


class TestFunctions(unittest.TestCase):
    @mock.patch('logging.basicConfig', autospec=True)
    def test_configure_logging_verbose(self, m_basicConfig):
        main.configure_logging(verbose=True)
        m_basicConfig.assert_called_with(format=mock.ANY, level=logging.INFO)

    @mock.patch('logging.basicConfig', autospec=True)
    def test_configure_logging(self, m_basicConfig):
        main.configure_logging(verbose=False)
        m_basicConfig.assert_called_with(format=mock.ANY, level=logging.WARN)

    def test_create_argument_parser_with_purge_project(self):
        parser = main.create_argument_parser()
        self.assertIsInstance(parser, argparse.ArgumentParser)

        options = parser.parse_args([
            '--verbose', '--dry-run', '--purge-project', 'foo',
            '--delete-shared-resources'
        ])
        self.assertEqual(True, options.verbose)
        self.assertEqual(True, options.dry_run)
        self.assertEqual(True, options.delete_shared_resources)
        self.assertEqual('foo', options.purge_project)

    def test_create_argument_parser_with_purge_own_project(self):
        parser = main.create_argument_parser()
        options = parser.parse_args(['--purge-own-project'])

        self.assertEqual(False, options.verbose)
        self.assertEqual(False, options.dry_run)
        self.assertEqual(False, options.delete_shared_resources)
        self.assertEqual(True, options.purge_own_project)

    def test_runner(self):
        resources = [mock.Mock(), mock.Mock(), mock.Mock()]
        resource_manager = mock.Mock(list=mock.Mock(return_value=resources))
        options = mock.Mock(dry_run=False)
        exit = mock.Mock(is_set=mock.Mock(side_effect=[False, False, True]))

        main.runner(resource_manager, options, exit)

        resource_manager.list.assert_called_once_with()
        resource_manager.wait_for_check_prerequisite.assert_called_once_with(
            exit)
        self.assertEqual(
            [mock.call(resources[0]), mock.call(resources[1])],
            resource_manager.should_delete.call_args_list
        )
        self.assertEqual(2, resource_manager.delete.call_count)
        self.assertEqual(
            [mock.call(resources[0]), mock.call(resources[1])],
            resource_manager.delete.call_args_list
        )

    def test_runner_dry_run(self):
        resources = [mock.Mock(), mock.Mock()]
        resource_manager = mock.Mock(list=mock.Mock(return_value=resources))
        options = mock.Mock(dry_run=True)
        exit = mock.Mock(is_set=mock.Mock(return_value=False))

        main.runner(resource_manager, options, exit)

        resource_manager.wait_for_check_prerequisite.assert_not_called()
        resource_manager.delete.assert_not_called()

    def test_runner_with_unrecoverable_exception(self):
        resource_manager = mock.Mock(list=mock.Mock(side_effect=Exception))
        exit = mock.Mock()

        main.runner(resource_manager, mock.Mock(dry_run=True), exit)

        exit.set.assert_called_once_with()

    def test_runner_with_recoverable_exception(self):
        class MyEndpointNotFound(Exception):
            pass
        exc = shade.exc.OpenStackCloudException("")
        exc.inner_exception = (MyEndpointNotFound, )
        resource_manager = mock.Mock(list=mock.Mock(side_effect=exc))
        exit = mock.Mock()

        main.runner(resource_manager, mock.Mock(dry_run=True), exit)

        self.assertFalse(exit.set.called)

    @mock.patch.object(main, 'os_client_config', autospec=True)
    @mock.patch.object(main, 'shade')
    @mock.patch('argparse.ArgumentParser.parse_args')
    @mock.patch('threading.Event', autospec=True)
    @mock.patch('concurrent.futures.ThreadPoolExecutor', autospec=True)
    @mock.patch('sys.exit', autospec=True)
    def test_main(self, m_sys_exit, m_tpe, m_event, m_parse_args, m_shade,
                  m_oscc):
        m_tpe.return_value.__enter__.return_value.map.side_effect = \
            KeyboardInterrupt
        m_parse_args.return_value.purge_own_project = False
        m_shade.operator_cloud().get_project().enabled = False

        main.main()

        m_oscc.OpenStackConfig.assert_called_once_with()

        m_parse_args.assert_called_once_with()

        self.assertIsInstance(m_tpe.call_args[0][0], int)
        m_tpe.return_value.__enter__.assert_called_once_with()
        self.assertEqual(1, m_tpe.return_value.__exit__.call_count)

        executor = m_tpe.return_value.__enter__.return_value
        self.assertEqual(1, executor.map.call_count)
        map_args = executor.map.call_args[0]
        self.assertEqual(True, callable(map_args[0]))
        for obj in map_args[1]:
            self.assertIsInstance(obj, ServiceResource)

        m_event.return_value.set.assert_called_once_with()
        m_event.return_value.is_set.assert_called_once_with()
        self.assertIsInstance(m_sys_exit.call_args[0][0], int)


@mock.patch.object(main, 'shade')
class TestCredentialsManager(unittest.TestCase):
    def test_init_with_purge_own_project(self, m_shade):
        _options = types.SimpleNamespace(
            purge_own_project=True, purge_project=None)
        creds_mgr = main.CredentialsManager(_options)

        self.assertEqual(_options, creds_mgr.options)
        self.assertEqual(False, creds_mgr.revoke_role_after_purge)
        self.assertEqual(False, creds_mgr.disable_project_after_purge)
        self.assertIsNone(creds_mgr.operator_cloud)

        m_shade.openstack_cloud.assert_called_once_with(argparse=_options)
        self.assertEqual(m_shade.openstack_cloud.return_value,
                         creds_mgr.cloud)

        self.assertEqual(
            creds_mgr.cloud.keystone_session.get_user_id(),
            creds_mgr.user_id
        )
        self.assertEqual(
            creds_mgr.cloud.keystone_session.get_project_id(),
            creds_mgr.project_id
        )

        creds_mgr.cloud.cloud_config.get_auth_args.assert_called_once_with()

    @mock.patch.object(utils, 'replace_project_info')
    def test_init_with_purge_project(self, m_replace, m_shade):
        _options = types.SimpleNamespace(
            purge_own_project=False, purge_project=mock.sentinel.purge_project)
        creds_mgr = main.CredentialsManager(_options)

        m_shade.operator_cloud.assert_called_once_with(argparse=_options)
        self.assertEqual(m_shade.operator_cloud.return_value,
                         creds_mgr.operator_cloud)

        creds_mgr.operator_cloud.get_project.assert_called_once_with(
            _options.purge_project)

        self.assertEqual(
            creds_mgr.operator_cloud.keystone_session.get_user_id.return_value,
            creds_mgr.user_id
        )
        self.assertEqual(
            creds_mgr.operator_cloud.get_project()['id'],
            creds_mgr.project_id
        )
        self.assertFalse(creds_mgr.disable_project_after_purge)
        self.assertEqual(
            m_shade.openstack_cloud.return_value,
            creds_mgr.cloud
        )
        m_replace.assert_called_once_with(
            creds_mgr.operator_cloud.cloud_config.config,
            creds_mgr.project_id
        )
        creds_mgr.cloud.cloud_config.get_auth_args.assert_called_once_with()

    def test_init_with_project_not_found(self, m_shade):
        m_shade.operator_cloud.return_value.get_project.return_value = None
        self.assertRaises(
            exceptions.OSProjectNotFound,
            main.CredentialsManager, mock.Mock(purge_own_project=False)
        )

    def test_ensure_role_on_project(self, m_shade):
        options = mock.Mock(purge_own_project=False)
        creds_manager = main.CredentialsManager(options)
        creds_manager.ensure_role_on_project()

        m_shade.operator_cloud.return_value.grant_role.assert_called_once_with(
            options.admin_role_name, project=options.purge_project,
            user=mock.ANY)
        self.assertEqual(True, creds_manager.revoke_role_after_purge)

        # If purge_own_project is not False, we purge our own project
        # so no need to revoke role after purge
        creds_manager = main.CredentialsManager(mock.Mock())
        creds_manager.ensure_role_on_project()
        self.assertEqual(False, creds_manager.revoke_role_after_purge)

    def test_revoke_role_on_project(self, m_shade):
        options = mock.Mock(purge_own_project=False)
        creds_manager = main.CredentialsManager(options)
        creds_manager.revoke_role_on_project()

        m_shade.operator_cloud().revoke_role.assert_called_once_with(
            options.admin_role_name, project=options.purge_project,
            user=mock.ANY)

    def test_ensure_enabled_project(self, m_shade):
        m_shade.operator_cloud().get_project().enabled = False
        creds_manager = main.CredentialsManager(
            mock.Mock(purge_own_project=False))
        creds_manager.ensure_enabled_project()

        self.assertEqual(True, creds_manager.disable_project_after_purge)
        m_shade.operator_cloud().update_project.assert_called_once_with(
            mock.ANY, enabled=True)

        # If project is enabled before purge, no need to disable it after
        # purge
        creds_manager = main.CredentialsManager(mock.Mock())
        creds_manager.ensure_enabled_project()
        self.assertEqual(False, creds_manager.disable_project_after_purge)
        self.assertEqual(1, m_shade.operator_cloud().update_project.call_count)

    def test_disable_project(self, m_shade):
        options = mock.Mock(purge_own_project=False)
        creds_manager = main.CredentialsManager(options)
        creds_manager.disable_project()

        m_shade.operator_cloud().update_project.assert_called_once_with(
            mock.ANY, enabled=False
        )
