# Copyright (c) 2016 Red Hat, Inc.
# All Rights Reserved.
#
#    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 inspect
from unittest import mock

from cinder import context
from cinder import exception
from cinder.objects import cleanable
from cinder import rpc
from cinder import service
from cinder.tests.unit import objects as test_objects
from cinder.volume import rpcapi


# NOTE(geguileo): We use Backup because we have version changes from 1.0 to 1.3

class Backup(cleanable.CinderCleanableObject):
    def __init__(self, *args, **kwargs):
        super(Backup, self).__init__(*args)
        for attr, value in kwargs.items():
            setattr(self, attr, value)

    @staticmethod
    def _is_cleanable(status, obj_version):
        if obj_version and obj_version <= 1003:
            return False
        return status == 'cleanable'


class TestCleanable(test_objects.BaseObjectsTestCase):
    MOCK_WORKER = False

    def setUp(self):
        super(TestCleanable, self).setUp()
        self.context = context.RequestContext(self.user_id, self.project_id,
                                              is_admin=True)

    def test_get_rpc_api(self):
        """Test get_rpc_api."""
        vol_rpcapi = cleanable.CinderCleanableObject.get_rpc_api()
        self.assertEqual(rpcapi.VolumeAPI, vol_rpcapi)

    def test_get_pinned_version(self):
        """Test that we get the pinned version for this specific object."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.0'
        version = Backup.get_pinned_version()
        self.assertEqual(1003, version)

    def test_is_cleanable_pinned_pinned_too_old(self):
        """Test is_cleanable with pinned version with uncleanable version."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.0'
        backup = Backup(status='cleanable')
        self.assertFalse(backup.is_cleanable(pinned=True))

    def test_is_cleanable_pinned_result_true(self):
        """Test with pinned version with cleanable version and status."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.3'
        backup = Backup(status='cleanable')
        self.assertTrue(backup.is_cleanable(pinned=True))

    def test_is_cleanable_pinned_result_false(self):
        """Test with pinned version with cleanable version but not status."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.3'
        backup = Backup(status='not_cleanable')
        self.assertFalse(backup.is_cleanable(pinned=True))

    def test_is_cleanable_unpinned_result_false(self):
        """Test unpinned version with old version and non cleanable status."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.0'
        backup = Backup(status='not_cleanable')
        self.assertFalse(backup.is_cleanable(pinned=False))

    def test_is_cleanable_unpinned_result_true(self):
        """Test unpinned version with old version and cleanable status."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.0'
        backup = Backup(status='cleanable')
        self.assertTrue(backup.is_cleanable(pinned=False))

    @mock.patch('cinder.db.worker_create', autospec=True)
    def test_create_worker(self, mock_create):
        """Test worker creation as if it were from an rpc call."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.3'
        mock_create.return_value = mock.sentinel.worker
        backup = Backup(_context=self.context, status='cleanable',
                        id=mock.sentinel.id)
        res = backup.create_worker()
        self.assertTrue(res)
        mock_create.assert_called_once_with(self.context,
                                            status='cleanable',
                                            resource_type='Backup',
                                            resource_id=mock.sentinel.id)

    @mock.patch('cinder.db.worker_create', autospec=True)
    def test_create_worker_pinned_too_old(self, mock_create):
        """Test worker creation when we are pinnned with an old version."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.0'
        mock_create.return_value = mock.sentinel.worker
        backup = Backup(_context=self.context, status='cleanable',
                        id=mock.sentinel.id)
        res = backup.create_worker()
        self.assertFalse(res)
        self.assertFalse(mock_create.called)

    @mock.patch('cinder.db.worker_create', autospec=True)
    def test_create_worker_non_cleanable(self, mock_create):
        """Test worker creation when status is non cleanable."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.3'
        mock_create.return_value = mock.sentinel.worker
        backup = Backup(_context=self.context, status='non_cleanable',
                        id=mock.sentinel.id)
        res = backup.create_worker()
        self.assertFalse(res)
        self.assertFalse(mock_create.called)

    @mock.patch('cinder.db.worker_update', autospec=True)
    @mock.patch('cinder.db.worker_create', autospec=True)
    def test_create_worker_already_exists(self, mock_create, mock_update):
        """Test worker creation when a worker for the resource exists."""
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.3'
        mock_create.side_effect = exception.WorkerExists(type='type', id='id')

        backup = Backup(_context=self.context, status='cleanable',
                        id=mock.sentinel.id)
        res = backup.create_worker()
        self.assertTrue(res)
        self.assertTrue(mock_create.called)
        mock_update.assert_called_once_with(
            self.context, None,
            filters={'resource_type': 'Backup',
                     'resource_id': mock.sentinel.id},
            service_id=None, status='cleanable')

    @mock.patch('cinder.db.worker_update', autospec=True)
    @mock.patch('cinder.db.worker_create', autospec=True)
    def test_create_worker_cleaning(self, mock_create, mock_update):
        """Test worker creation on race condition.

        Test that we still create an entry if there is a rare race condition
        that the entry gets removed from the DB between our failure to create
        it and our try to update the entry.
        """
        rpc.LAST_OBJ_VERSIONS[Backup.get_rpc_api().BINARY] = '1.3'
        mock_create.side_effect = [
            exception.WorkerExists(type='type', id='id'), mock.sentinel.worker]
        mock_update.side_effect = exception.WorkerNotFound

        backup = Backup(_context=self.context, status='cleanable',
                        id=mock.sentinel.id)
        self.assertTrue(backup.create_worker())
        self.assertEqual(2, mock_create.call_count)
        self.assertTrue(mock_update.called)

    @mock.patch('cinder.db.worker_update', autospec=True)
    @mock.patch('cinder.db.worker_get', autospec=True)
    def test_set_worker(self, mock_get, mock_update):
        """Test set worker for a normal job received from an rpc call."""
        service.Service.service_id = mock.sentinel.service_id
        mock_get.return_value.cleaning = False
        backup = Backup(_context=self.context, status=mock.sentinel.status,
                        id=mock.sentinel.id)

        backup.set_worker()
        mock_get.assert_called_once_with(self.context, resource_type='Backup',
                                         resource_id=mock.sentinel.id)
        worker = mock_get.return_value
        mock_update.assert_called_once_with(
            self.context, worker.id,
            filters={'service_id': worker.service_id,
                     'status': worker.status,
                     'race_preventer': worker.race_preventer,
                     'updated_at': worker.updated_at},
            service_id=mock.sentinel.service_id,
            status=mock.sentinel.status,
            orm_worker=worker)
        self.assertEqual(worker, backup.worker)

    @mock.patch('cinder.db.worker_create', autospec=True)
    @mock.patch('cinder.db.worker_get', autospec=True)
    def test_set_worker_direct(self, mock_get, mock_create):
        """Test set worker for direct call (non rpc call)."""
        mock_get.side_effect = exception.WorkerNotFound
        service_id = mock.sentinel.service_id
        service.Service.service_id = service_id
        mock_create.return_value = mock.Mock(service_id=service_id,
                                             status=mock.sentinel.status,
                                             deleted=False, cleaning=False)

        backup = Backup(_context=self.context, status=mock.sentinel.status,
                        id=mock.sentinel.id)

        backup.set_worker()
        mock_get.assert_called_once_with(self.context, resource_type='Backup',
                                         resource_id=mock.sentinel.id)
        mock_create.assert_called_once_with(self.context,
                                            status=mock.sentinel.status,
                                            resource_type='Backup',
                                            resource_id=mock.sentinel.id,
                                            service_id=service_id)
        self.assertEqual(mock_create.return_value, backup.worker)

    @mock.patch('cinder.db.worker_update', autospec=True)
    @mock.patch('cinder.db.worker_get', autospec=True)
    def test_set_worker_claim_from_another_host(self, mock_get, mock_update):
        """Test set worker when the job was started on another failed host."""
        service_id = mock.sentinel.service_id
        service.Service.service_id = service_id
        worker = mock.Mock(service_id=mock.sentinel.service_id2,
                           status=mock.sentinel.status, cleaning=False,
                           updated_at=mock.sentinel.updated_at)
        mock_get.return_value = worker

        backup = Backup(_context=self.context, status=mock.sentinel.status,
                        id=mock.sentinel.id)

        backup.set_worker()

        mock_update.assert_called_once_with(
            self.context, worker.id,
            filters={'service_id': mock.sentinel.service_id2,
                     'status': mock.sentinel.status,
                     'race_preventer': worker.race_preventer,
                     'updated_at': mock.sentinel.updated_at},
            service_id=service_id, status=mock.sentinel.status,
            orm_worker=worker)
        self.assertEqual(worker, backup.worker)

    @mock.patch('cinder.db.worker_create', autospec=True)
    @mock.patch('cinder.db.worker_get', autospec=True)
    def test_set_worker_race_condition_fail(self, mock_get, mock_create):
        """Test we cannot claim a work if we lose race condition."""
        service.Service.service_id = mock.sentinel.service_id
        mock_get.side_effect = exception.WorkerNotFound
        mock_create.side_effect = exception.WorkerExists(type='type', id='id')

        backup = Backup(_context=self.context, status=mock.sentinel.status,
                        id=mock.sentinel.id)

        self.assertRaises(exception.CleanableInUse, backup.set_worker)
        self.assertTrue(mock_get.called)
        self.assertTrue(mock_create.called)

    @mock.patch('cinder.db.worker_update', autospec=True)
    @mock.patch('cinder.db.worker_get', autospec=True)
    def test_set_worker_claim_fail_after_get(self, mock_get, mock_update):
        """Test we don't have race condition if worker changes after get."""
        service.Service.service_id = mock.sentinel.service_id
        worker = mock.Mock(service_id=mock.sentinel.service_id2,
                           status=mock.sentinel.status, deleted=False,
                           cleaning=False)
        mock_get.return_value = worker
        mock_update.side_effect = exception.WorkerNotFound

        backup = Backup(_context=self.context, status=mock.sentinel.status,
                        id=mock.sentinel.id)

        self.assertRaises(exception.CleanableInUse, backup.set_worker)
        self.assertTrue(mock_get.called)
        self.assertTrue(mock_update.called)

    @mock.patch('cinder.db.worker_destroy')
    def test_unset_worker(self, destroy_mock):
        backup = Backup(_context=self.context, status=mock.sentinel.status,
                        id=mock.sentinel.id)
        worker = mock.Mock()
        backup.worker = worker
        backup.unset_worker()
        destroy_mock.assert_called_once_with(self.context, id=worker.id,
                                             status=worker.status,
                                             service_id=worker.service_id)
        self.assertIsNone(backup.worker)

    @mock.patch('cinder.db.worker_destroy')
    def test_unset_worker_not_set(self, destroy_mock):
        backup = Backup(_context=self.context, status=mock.sentinel.status,
                        id=mock.sentinel.id)
        backup.unset_worker()
        self.assertFalse(destroy_mock.called)

    @mock.patch('cinder.db.worker_update', autospec=True)
    @mock.patch('cinder.db.worker_get', autospec=True)
    def test_set_workers_no_arguments(self, mock_get, mock_update):
        """Test set workers decorator without arguments."""
        @Backup.set_workers
        def my_function(arg1, arg2, kwarg1=None, kwarg2=True):
            return arg1, arg2, kwarg1, kwarg2

        # Decorator with no args must preserve the method's signature
        self.assertEqual('my_function', my_function.__name__)
        call_args = inspect.getcallargs(
            my_function, mock.sentinel.arg1, mock.sentinel.arg2,
            mock.sentinel.kwargs1, kwarg2=mock.sentinel.kwarg2)
        expected = {'arg1': mock.sentinel.arg1,
                    'arg2': mock.sentinel.arg2,
                    'kwarg1': mock.sentinel.kwargs1,
                    'kwarg2': mock.sentinel.kwarg2}
        self.assertDictEqual(expected, call_args)

        service.Service.service_id = mock.sentinel.service_id
        mock_get.return_value.cleaning = False
        backup = Backup(_context=self.context, status='cleanable',
                        id=mock.sentinel.id)
        backup2 = Backup(_context=self.context, status='non-cleanable',
                         id=mock.sentinel.id2)

        res = my_function(backup, backup2)
        self.assertEqual((backup, backup2, None, True), res)

        mock_get.assert_called_once_with(self.context, resource_type='Backup',
                                         resource_id=mock.sentinel.id)
        worker = mock_get.return_value
        mock_update.assert_called_once_with(
            self.context, worker.id,
            filters={'service_id': worker.service_id,
                     'status': worker.status,
                     'race_preventer': worker.race_preventer,
                     'updated_at': worker.updated_at},
            service_id=mock.sentinel.service_id,
            status='cleanable',
            orm_worker=worker)
        self.assertEqual(worker, backup.worker)

    @mock.patch('cinder.db.worker_update', autospec=True)
    @mock.patch('cinder.db.worker_get', autospec=True)
    def test_set_workers_with_arguments(self, mock_get, mock_update):
        """Test set workers decorator with an argument."""
        @Backup.set_workers('arg2', 'kwarg1')
        def my_function(arg1, arg2, kwarg1=None, kwarg2=True):
            return arg1, arg2, kwarg1, kwarg2

        # Decorator with args must preserve the method's signature
        self.assertEqual('my_function', my_function.__name__)
        call_args = inspect.getcallargs(
            my_function, mock.sentinel.arg1, mock.sentinel.arg2,
            mock.sentinel.kwargs1, kwarg2=mock.sentinel.kwarg2)
        expected = {'arg1': mock.sentinel.arg1,
                    'arg2': mock.sentinel.arg2,
                    'kwarg1': mock.sentinel.kwargs1,
                    'kwarg2': mock.sentinel.kwarg2}
        self.assertDictEqual(expected, call_args)

        service.Service.service_id = mock.sentinel.service_id
        mock_get.return_value.cleaning = False
        backup = Backup(_context=self.context, status='cleanable',
                        id=mock.sentinel.id)
        backup2 = Backup(_context=self.context, status='non-cleanable',
                         id=mock.sentinel.id2)
        backup3 = Backup(_context=self.context, status='cleanable',
                         id=mock.sentinel.id3)

        res = my_function(backup, backup2, backup3)
        self.assertEqual((backup, backup2, backup3, True), res)

        mock_get.assert_called_once_with(self.context, resource_type='Backup',
                                         resource_id=mock.sentinel.id3)
        worker = mock_get.return_value
        mock_update.assert_called_once_with(
            self.context, worker.id,
            filters={'service_id': worker.service_id,
                     'status': worker.status,
                     'race_preventer': worker.race_preventer,
                     'updated_at': worker.updated_at},
            service_id=mock.sentinel.service_id,
            status='cleanable',
            orm_worker=worker)
        self.assertEqual(worker, backup3.worker)
