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

from barbican.model import models
from barbican.model import repositories
from barbican.queue import server
from barbican.tasks import common
from barbican.tests import database_utils
from barbican.tests import utils


class WhenUsingTransactionalDecorator(utils.BaseTestCase):
    """Test using the 'transactional' decorator in server.py.

    Note that only the 'I am a server' logic is tested here, as the alternate
    mode is only used for direct invocation of Task methods in the standalone
    server mode, which is also thoroughly tested in WhenUsingBeginTypeOrderTask
    below.
    """

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

        # Ensure we always thing we are in 'I am a server' mode.
        is_server_side_config = {
            'return_value': True
        }
        self.is_server_side_patcher = mock.patch(
            'barbican.queue.is_server_side',
            **is_server_side_config
        )
        self.is_server_side_patcher.start()

        self.commit_patcher = mock.patch(
            'barbican.model.repositories.commit'
        )
        self.commit_mock = self.commit_patcher.start()

        self.rollback_patcher = mock.patch(
            'barbican.model.repositories.rollback'
        )
        self.rollback_mock = self.rollback_patcher.start()

        self.clear_patcher = mock.patch(
            'barbican.model.repositories.clear'
        )
        self.clear_mock = self.clear_patcher.start()

        self.args = ('foo', 'bar')
        self.kwargs = {'k_foo': 1, 'k_bar': 2}

        # Class/decorator under test.
        class TestClass(object):
            my_args = None
            my_kwargs = None
            is_exception_needed = False

            @server.transactional
            def test_method(self, *args, **kwargs):
                if self.is_exception_needed:
                    raise ValueError()
                self.my_args = args
                self.my_kwargs = kwargs
        self.test_object = TestClass()

    def tearDown(self):
        super(WhenUsingTransactionalDecorator, self).tearDown()
        self.is_server_side_patcher.stop()
        self.commit_patcher.stop()
        self.rollback_patcher.stop()
        self.clear_patcher.stop()

    def test_should_commit(self):
        self.test_object.test_method(*self.args, **self.kwargs)

        self.assertEqual(self.args, self.test_object.my_args)
        self.assertEqual(self.kwargs, self.test_object.my_kwargs)

        self.assertEqual(1, self.commit_mock.call_count)
        self.assertEqual(0, self.rollback_mock.call_count)
        self.assertEqual(1, self.clear_mock.call_count)

    def test_should_rollback(self):
        self.test_object.is_exception_needed = True

        self.test_object.test_method(*self.args, **self.kwargs)

        self.assertEqual(0, self.commit_mock.call_count)
        self.assertEqual(1, self.rollback_mock.call_count)
        self.assertEqual(1, self.clear_mock.call_count)


class WhenUsingRetryableOrderDecorator(utils.BaseTestCase):
    """Test using the 'retryable_order' decorator in server.py."""

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

        self.schedule_retry_tasks_patcher = mock.patch(
            'barbican.queue.server.schedule_order_retry_tasks'
        )
        self.schedule_retry_tasks_mock = (
            self.schedule_retry_tasks_patcher.start()
        )

        self.order_id = 'order-id'
        self.args = ('foo', 'bar')
        self.kwargs = {'k_foo': 1, 'k_bar': 2}

        # Class/decorator under test.
        class TestClass(object):
            self.order_id = None
            my_args = None
            my_kwargs = None
            is_exception_needed = False
            result = common.FollowOnProcessingStatusDTO()

            @server.retryable_order
            def test_method(self, order_id, *args, **kwargs):
                if self.is_exception_needed:
                    raise ValueError()
                self.order_id = order_id
                self.my_args = args
                self.my_kwargs = kwargs
                return self.result
        self.test_object = TestClass()
        self.test_method = TestClass.test_method

    def tearDown(self):
        super(WhenUsingRetryableOrderDecorator, self).tearDown()
        self.schedule_retry_tasks_patcher.stop()

    def test_should_successfully_schedule_a_task_for_retry(self):
        self.test_object.test_method(self.order_id, *self.args, **self.kwargs)

        self.assertEqual(self.order_id, self.test_object.order_id)
        self.assertEqual(self.args, self.test_object.my_args)
        self.assertEqual(self.kwargs, self.test_object.my_kwargs)

        self.assertEqual(1, self.schedule_retry_tasks_mock.call_count)
        self.schedule_retry_tasks_mock.assert_called_with(
            mock.ANY,
            self.test_object.result,
            self.order_id,
            *self.args,
            **self.kwargs)

    def test_retry_should_not_be_scheduled_if_exception_is_raised(self):
        self.test_object.is_exception_needed = True

        self.assertRaises(
            ValueError,
            self.test_object.test_method,
            self.order_id,
            self.args,
            self.kwargs,
        )

        self.assertEqual(0, self.schedule_retry_tasks_mock.call_count)


class WhenCallingScheduleOrderRetryTasks(database_utils.RepositoryTestCase):
    """Test calling schedule_order_retry_tasks() in server.py."""

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

        self.project = database_utils.create_project()
        self.order = database_utils.create_order(self.project)
        database_utils.get_session().commit()

        self.repo = repositories.OrderRetryTaskRepo()

        self.result = common.FollowOnProcessingStatusDTO()

        self.args = ['args-foo', 'args-bar']
        self.kwargs = {'order_id': self.order.id, 'foo': 1, 'bar': 2}
        self.date_to_retry_at = (
            datetime.datetime.utcnow() + datetime.timedelta(
                milliseconds=self.result.retry_msec)
        )

    def test_should_not_schedule_task_due_to_no_result(self):
        retry_rpc_method = server.schedule_order_retry_tasks(None, None, None)

        self.assertIsNone(retry_rpc_method)

    def test_should_not_schedule_task_due_to_no_action_required_result(self):
        self.result.retry_task = common.RetryTasks.NO_ACTION_REQUIRED

        retry_rpc_method = server.schedule_order_retry_tasks(
            None, self.result, None)

        self.assertIsNone(retry_rpc_method)

    def test_should_schedule_invoking_task_for_retry(self):
        self.result.retry_task = common.RetryTasks.INVOKE_SAME_TASK

        # Schedule this test method as the passed-in 'retry' function.
        retry_rpc_method = server.schedule_order_retry_tasks(
            self.test_should_schedule_invoking_task_for_retry,
            self.result,
            None,  # Not used.
            *self.args,
            **self.kwargs)
        database_utils.get_session().commit()  # Flush to the database.

        self.assertEqual(
            'test_should_schedule_invoking_task_for_retry', retry_rpc_method)

    def test_should_schedule_certificate_status_task_for_retry(self):
        self.result.retry_task = (
            common.RetryTasks.INVOKE_CERT_STATUS_CHECK_TASK
        )

        # Schedule this test method as the passed-in 'retry' function.
        retry_rpc_method = server.schedule_order_retry_tasks(
            None,  # Should be ignored for non-self retries.
            self.result,
            None,  # Not used.
            *self.args,
            **self.kwargs)
        database_utils.get_session().commit()  # Flush to the database.

        self.assertEqual(
            'check_certificate_status', retry_rpc_method)
        self._verify_retry_task_entity(
            'check_certificate_status')

    def _verify_retry_task_entity(self, retry_task):
        # Retrieve the task retry entity created above and verify it.
        entities, offset, limit, total = self.repo.get_by_create_date()
        self.assertEqual(1, total)
        retry_model = entities[0]
        self.assertEqual(retry_task, retry_model.retry_task)
        self.assertEqual(self.args, retry_model.retry_args)
        self.assertEqual(self.kwargs, retry_model.retry_kwargs)
        self.assertEqual(0, retry_model.retry_count)

        # Compare retry_at times.
        # Note that the expected retry_at time is computed at setUp() time, but
        # the retry_at time on the task retry entity/model is computed and set
        # a few milliseconds after this setUp() time, hence they will vary by a
        # small amount of time.
        delta = retry_model.retry_at - self.date_to_retry_at
        delta_seconds = delta.seconds
        self.assertLessEqual(delta_seconds, 2)


class WhenCallingTasksMethod(utils.BaseTestCase):
    """Test calling methods on the Tasks class."""

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

        # Mock the 'am I a server process?' flag used by the decorator around
        #   all task methods. Since this test class focuses on testing task
        #   method behaviors, this flag is set to false to allow for direct
        #   testing of these tasks without database transactional interference.
        is_server_side_config = {
            'return_value': False
        }
        self.is_server_side_patcher = mock.patch(
            'barbican.queue.is_server_side',
            **is_server_side_config
        )
        self.is_server_side_patcher.start()

        self.tasks = server.Tasks()

    def tearDown(self):
        super(WhenCallingTasksMethod, self).tearDown()
        self.is_server_side_patcher.stop()

    @mock.patch('barbican.queue.server.schedule_order_retry_tasks')
    @mock.patch('barbican.tasks.resources.BeginTypeOrder')
    def test_should_process_begin_order(self, mock_begin_order, mock_schedule):
        method = mock_begin_order.return_value.process_and_suppress_exceptions
        method.return_value = 'result'

        self.tasks.process_type_order(
            None, self.order_id, self.external_project_id, self.request_id)

        mock_process = mock_begin_order.return_value
        mock_process.process_and_suppress_exceptions.assert_called_with(
            self.order_id, self.external_project_id)
        mock_schedule.assert_called_with(
            mock.ANY, 'result', None, 'order1234',
            'keystone1234', 'request1234')

    @mock.patch('barbican.queue.server.schedule_order_retry_tasks')
    @mock.patch('barbican.tasks.resources.CheckCertificateStatusOrder')
    def test_should_check_certificate_order(
            self, mock_check_cert, mock_schedule):
        method = mock_check_cert.return_value.process_and_suppress_exceptions
        method.return_value = 'result'

        self.tasks.check_certificate_status(
            None, self.order_id, self.external_project_id, self.request_id)

        mock_process = mock_check_cert.return_value
        mock_process.process_and_suppress_exceptions.assert_called_with(
            self.order_id, self.external_project_id
        )
        mock_schedule.assert_called_with(
            mock.ANY, 'result', None, 'order1234',
            'keystone1234', 'request1234')

    @mock.patch('barbican.tasks.resources.BeginTypeOrder')
    def test_process_order_catch_exception(self, mock_begin_order):
        """Test that BeginTypeOrder's process() handles all exceptions."""
        mock_begin_order.return_value._process.side_effect = Exception()

        self.tasks.process_type_order(None, self.order_id,
                                      self.external_project_id,
                                      self.request_id)


class WhenUsingTaskServer(database_utils.RepositoryTestCase):
    """Test using the asynchronous task client.

    This test suite performs a full-stack test of worker-side task
    processing (except for queue interactions, which are mocked). This
    includes testing database commit and session close behaviors.
    """

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

        # Queue target mocking setup.
        self.target = 'a target value here'
        queue_get_target_config = {
            'return_value': self.target
        }
        self.queue_get_target_patcher = mock.patch(
            'barbican.queue.get_target',
            **queue_get_target_config
        )
        self.queue_get_target_mock = self.queue_get_target_patcher.start()

        # Queue server mocking setup.
        self.server_mock = mock.MagicMock()
        self.server_mock.start.return_value = None
        self.server_mock.stop.return_value = None
        queue_get_server_config = {
            'return_value': self.server_mock
        }
        self.queue_get_server_patcher = mock.patch(
            'barbican.queue.get_server',
            **queue_get_server_config
        )
        self.queue_get_server_mock = self.queue_get_server_patcher.start()

        self.server = server.TaskServer()

        # Add an order to the in-memory database.
        self.external_id = 'keystone-id'
        project = database_utils.create_project(
            external_id=self.external_id)
        self.order = database_utils.create_order(
            project=project)
        self.request_id = 'request1234'

    def tearDown(self):
        super(WhenUsingTaskServer, self).tearDown()
        self.queue_get_target_patcher.stop()
        self.queue_get_server_patcher.stop()

    def test_should_start(self):
        self.server.start()

        self.queue_get_target_mock.assert_called_with()
        self.queue_get_server_mock.assert_called_with(
            target=self.target, endpoints=[self.server])
        self.server_mock.start.assert_called_with()

    def test_should_stop(self):
        self.server.stop()
        self.queue_get_target_mock.assert_called_with()
        self.queue_get_server_mock.assert_called_with(
            target=self.target, endpoints=[self.server])
        self.server_mock.stop.assert_called_with()

    def test_process_bogus_begin_type_order_should_not_rollback(self):
        order_id = self.order.id
        self.order.type = 'bogus-type'  # Force error out of business logic.

        # Invoke process, including the transactional decorator that terminates
        # the session when it is done. Hence we must re-retrieve the order for
        # verification afterwards.
        self.server.process_type_order(
            None, self.order.id, self.external_id, self.request_id)

        order_repo = repositories.get_order_repository()
        order_result = order_repo.get(order_id, self.external_id)

        self.assertEqual(models.States.ERROR, order_result.status)
        self.assertEqual(
            six.u(
                'Process TypeOrder failure seen - '
                'please contact site administrator.'),
            order_result.error_reason)
        self.assertEqual(
            six.u('500'),
            order_result.error_status_code)
