# 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 datetime
from unittest import mock

import testtools

from barbican.common import utils as common_utils
from barbican.model import models
from barbican.plugin.interface import certificate_manager as cm
from barbican.tests import database_utils
from barbican.tests import utils


class WhenTestingCertificateEventPluginManager(testtools.TestCase):

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

        self.project_id = '1234'
        self.order_ref = 'http://www.mycerts.com/v1/orders/123456'
        self.container_ref = 'http://www.mycerts.com/v1/containers/654321'
        self.error_msg = 'Something is broken'
        self.retry_in_msec = 5432

        self.plugin_returned = mock.MagicMock()
        self.plugin_name = common_utils.generate_fullname_for(
            self.plugin_returned)
        self.plugin_loaded = mock.MagicMock(obj=self.plugin_returned)
        self.manager = cm.get_event_plugin_manager()
        self.manager.extensions = [self.plugin_loaded]

    def test_get_plugin_by_name(self):
        self.assertEqual(self.plugin_returned,
                         self.manager.get_plugin_by_name(self.plugin_name))

    def test_notify_ca_is_unavailable(self):
        self.manager.notify_ca_is_unavailable(
            self.project_id,
            self.order_ref,
            self.error_msg,
            self.retry_in_msec)

        self.plugin_returned.notify_ca_is_unavailable.assert_called_once_with(
            self.project_id,
            self.order_ref,
            self.error_msg,
            self.retry_in_msec)

    def test_notify_certificate_is_ready(self):
        self.manager.notify_certificate_is_ready(
            self.project_id,
            self.order_ref,
            self.container_ref)

        pr = self.plugin_returned
        pr.notify_certificate_is_ready.assert_called_once_with(
            self.project_id,
            self.order_ref,
            self.container_ref)

    def test_invoke_certificate_plugins(self):
        self.manager._invoke_certificate_plugins(
            'test_invoke_certificate_plugins',
            self.project_id,
            self.order_ref,
            self.container_ref)

        # The _invoke_certificate_plugins method should invoke on
        #   self.plugin_returned the same method by name as the function
        #   that invoked it...in this case it is this test method.
        pr = self.plugin_returned
        pr.test_invoke_certificate_plugins.assert_called_once_with(
            self.project_id,
            self.order_ref,
            self.container_ref)

    def test_raises_error_with_no_plugin_by_name_found(self):
        self.manager.extensions = []
        self.assertRaises(
            cm.CertificateEventPluginNotFound,
            self.manager.get_plugin_by_name,
            'any-name-here'
        )

    def test_raises_error_with_no_plugin_for_invoke_certificate_plugins(self):
        self.manager.extensions = []
        self.assertRaises(
            cm.CertificateEventPluginNotFound,
            self.manager._invoke_certificate_plugins,
            self.project_id,
            self.order_ref,
            self.error_msg,
            self.retry_in_msec,
        )


class WhenTestingCertificatePluginManager(database_utils.RepositoryTestCase,
                                          utils.MockModelRepositoryMixin):

    def setUp(self):
        super(WhenTestingCertificatePluginManager, self).setUp()
        self.cert_spec = {}

        self.plugin_returned = mock.MagicMock()
        self.plugin_name = common_utils.generate_fullname_for(
            self.plugin_returned)
        types_list = [cm.CertificateRequestType.SIMPLE_CMC_REQUEST,
                      cm.CertificateRequestType.CUSTOM_REQUEST]
        self.plugin_returned.supported_request_types.return_value = types_list
        self.plugin_returned.supports.return_value = True
        self.plugin_loaded = mock.MagicMock(obj=self.plugin_returned)

        expiration = (datetime.datetime.utcnow() + datetime.timedelta(
            days=cm.CA_INFO_DEFAULT_EXPIRATION_DAYS))
        ca_info = {
            cm.INFO_NAME: "my_ca",
            cm.INFO_DESCRIPTION: "Certificate Authority my_ca",
            cm.INFO_CA_SIGNING_CERT: "Undefined",
            cm.INFO_INTERMEDIATES: "Undefined",
            cm.INFO_EXPIRATION: expiration.isoformat()
        }
        self.plugin_returned.get_ca_info.return_value = {
            'plugin_ca_id1': ca_info
        }

        parsed_ca = {
            'plugin_name': self.plugin_name,
            'plugin_ca_id': 'plugin_ca_id1',
            'name': self.plugin_name,
            'description': 'Master CA for default plugin',
            'ca_signing_certificate': 'ZZZZZ',
            'intermediates': 'YYYYY'
        }
        self.ca = models.CertificateAuthority(parsed_ca)
        self.ca.id = 'ca_id'

        self.ca_repo = mock.MagicMock()
        self.ca_repo.get_by_create_date.return_value = (
            self.ca, 0, 1, 1)
        self.ca_repo.create_from.return_value = None
        self.ca_repo.get.return_value = self.ca

        self.project = models.Project()
        self.project.id = '12345'

        self.setup_ca_repository_mock(self.ca_repo)

        self.plugin_loaded = mock.MagicMock(obj=self.plugin_returned)
        self.manager = cm.CertificatePluginManager()
        self.manager.extensions = [self.plugin_loaded]

    def test_get_plugin_by_name(self):
        self.assertEqual(self.plugin_returned,
                         self.manager.get_plugin_by_name(self.plugin_name))

    def test_get_plugin_by_ca_id(self):
        self.assertEqual(self.plugin_returned,
                         self.manager.get_plugin_by_ca_id('ca_id'))

    def test_raises_error_with_no_plugin_by_ca_id_found(self):
        self.ca_repo.get.return_value = None
        self.assertRaises(
            cm.CertificatePluginNotFoundForCAID,
            self.manager.get_plugin_by_ca_id,
            'any-name-here'
        )

    def test_raises_error_with_no_plugin_by_name_found(self):
        self.manager.extensions = []
        self.assertRaises(
            cm.CertificatePluginNotFound,
            self.manager.get_plugin_by_name,
            'any-name-here'
        )

    def test_get_plugin_no_request_type_provided(self):
        # no request_type defaults to "custom"
        self.assertEqual(self.plugin_returned,
                         self.manager.get_plugin(self.cert_spec))

    def test_get_plugin_request_type_supported(self):
        self.cert_spec = {
            cm.REQUEST_TYPE: cm.CertificateRequestType.SIMPLE_CMC_REQUEST}
        self.assertEqual(self.plugin_returned,
                         self.manager.get_plugin(self.cert_spec))

    def test_raises_error_get_plugin_request_type_not_supported(self):
        self.cert_spec = {
            cm.REQUEST_TYPE: cm.CertificateRequestType.FULL_CMC_REQUEST}
        self.assertRaises(
            cm.CertificatePluginNotFound,
            self.manager.get_plugin,
            self.cert_spec
        )

    def test_raises_error_with_no_plugin_found(self):
        self.manager.extensions = []
        self.assertRaises(
            cm.CertificatePluginNotFound,
            self.manager.get_plugin,
            self.cert_spec
        )

    def test_get_plugin_with_ca_to_be_added(self):
        self.ca_repo.get_by_create_date.return_value = (
            None, 0, 1, 0)

        self.assertEqual(self.plugin_returned,
                         self.manager.get_plugin(self.cert_spec))

    def test_refresh_ca_list(self):
        utc_now = datetime.datetime.utcnow()
        expired_time = utc_now - datetime.timedelta(days=1)
        expiration = utc_now + datetime.timedelta(days=1)

        ca1_info = {
            cm.INFO_NAME: "expired_ca_to_be_modified",
            cm.INFO_DESCRIPTION: "expired_ca to be modified",
            cm.INFO_CA_SIGNING_CERT: "XXXXXXX-expired-XXXXXX",
            cm.INFO_INTERMEDIATES: "YYYYYYY-expired-YYYYYYY",
            cm.INFO_EXPIRATION: expired_time.isoformat()
        }

        ca1_modified_info = {
            cm.INFO_NAME: "expired_ca_to_be_modified",
            cm.INFO_DESCRIPTION: "expired_ca to be modified",
            cm.INFO_CA_SIGNING_CERT: "XXXXXXX-no-longer-expired-XXXXXX",
            cm.INFO_INTERMEDIATES: "YYYYYYY-no-longer-expired-YYYYYYY",
            cm.INFO_EXPIRATION: expiration.isoformat()
        }

        ca2_info = {
            cm.INFO_NAME: "expired_ca_to_be_deleted",
            cm.INFO_DESCRIPTION: "expired ca to be deleted",
            cm.INFO_CA_SIGNING_CERT: "XXXX-expired-to-be-deleted-XXXX",
            cm.INFO_INTERMEDIATES: "YYYY-expired-to-be-deleted-YYYY",
            cm.INFO_EXPIRATION: expired_time.isoformat()
        }

        ca3_info = {
            cm.INFO_NAME: "new-ca-to-be-added",
            cm.INFO_DESCRIPTION: "new-ca-to-be-added",
            cm.INFO_CA_SIGNING_CERT: "XXXX-to-be-addeed-XXXX",
            cm.INFO_INTERMEDIATES: "YYYY-to-be-added-YYYY",
            cm.INFO_EXPIRATION: expiration.isoformat()
        }

        self.plugin_returned.get_ca_info.return_value = {
            'plugin_ca_id_ca1': ca1_modified_info,
            'plugin_ca_id_ca3': ca3_info
        }

        parsed_ca1 = dict(ca1_info)
        parsed_ca1[cm.PLUGIN_CA_ID] = 'plugin_ca_id_ca1'
        parsed_ca1['plugin_name'] = self.plugin_name
        ca1 = models.CertificateAuthority(parsed_ca1)
        ca1.id = "ca1_id"

        parsed_ca2 = dict(ca2_info)
        parsed_ca2[cm.PLUGIN_CA_ID] = 'plugin_ca_id_ca2'
        parsed_ca2['plugin_name'] = self.plugin_name
        ca2 = models.CertificateAuthority(parsed_ca2)
        ca2.id = "ca2_id"

        side_effect = [(None, 0, 4, 0),
                       ([ca1, ca2], 0, 4, 2)]
        self.ca_repo.get_by_create_date.side_effect = side_effect

        self.manager.refresh_ca_table()
        self.plugin_returned.get_ca_info.assert_called_once_with()
        self.ca_repo.update_entity.assert_called_once_with(
            ca1,
            ca1_modified_info)

        self.ca_repo.delete_entity_by_id.assert_called_once_with(
            ca2.id,
            None)
        self.ca_repo.create_from.assert_has_calls([])

    def test_refresh_ca_list_plugin_when_get_ca_info_raises(self):
        self.ca_repo.get_by_create_date.return_value = (None, 0, 4, 0)
        self.plugin_returned.get_ca_info.side_effect = Exception()

        self.manager.refresh_ca_table()

        self.plugin_returned.get_ca_info.assert_called_once_with()

    def test_refresh_ca_list_with_bad_ca_returned_from_plugin(self):

        ca3_info = {
            cm.INFO_DESCRIPTION: "PLUGIN FAIL: this-ca-has-no-info",
        }

        self.plugin_returned.get_ca_info.return_value = {
            'plugin_ca_id_ca3': ca3_info
        }

        self.ca_repo.get_by_create_date.return_value = (None, 0, 4, 0)
        self.ca_repo.create_from.side_effect = Exception()

        self.manager.refresh_ca_table()

        self.plugin_returned.get_ca_info.assert_called_once_with()
        self.ca_repo.create_from.assert_has_calls([])
