#    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.
""" Tests for manage_existing TaskFlow """

import inspect
from unittest import mock

import taskflow.engines
from taskflow.types import failure as ft

from cinder import context
from cinder import exception
from cinder.tests.unit import fake_constants as fakes
from cinder.tests.unit import fake_volume
from cinder.tests.unit import test
from cinder.tests.unit.volume.flows import fake_volume_api
from cinder.volume.flows.api import manage_existing
from cinder.volume.flows import common as flow_common
from cinder.volume.flows.manager import manage_existing as manager


class ManageVolumeFlowTestCase(test.TestCase):

    def setUp(self):
        super(ManageVolumeFlowTestCase, self).setUp()
        self.ctxt = context.get_admin_context()
        self.counter = float(0)

    def test_cast_manage_existing(self):
        volume = fake_volume.fake_volume_obj(self.ctxt)

        spec = {
            'name': 'name',
            'description': 'description',
            'host': 'host',
            'ref': 'ref',
            'volume_type': 'volume_type',
            'metadata': 'metadata',
            'availability_zone': 'availability_zone',
            'bootable': 'bootable',
            'volume_id': volume.id,
        }

        # Fake objects assert specs
        task = manage_existing.ManageCastTask(
            fake_volume_api.FakeSchedulerRpcAPI(spec, self),
            fake_volume_api.FakeDb())

        create_what = spec.copy()
        create_what.update({'volume': volume})
        create_what.pop('volume_id')
        task.execute(self.ctxt, **create_what)

    def test_cast_manage_existing_revert(self):
        volume = fake_volume.fake_volume_obj(self.ctxt)
        volume.save = mock.MagicMock()

        # Fake objects assert specs
        task = manage_existing.ManageCastTask(
            fake_volume_api.FakeSchedulerRpcAPI({}, self),
            fake_volume_api.FakeDb())
        flow_failures = [mock.MagicMock()]
        task.revert(self.ctxt, {}, flow_failures, volume)

        # Check that volume status is updated and saved
        self.assertEqual('error_managing', volume.status)
        volume.save.assert_called_once()

    def test_create_db_entry_task_with_multiattach(self):

        fake_volume_type = fake_volume.fake_volume_type_obj(
            self.ctxt, extra_specs={'multiattach': '<is> True'})

        spec = {
            'name': 'name',
            'description': 'description',
            'host': 'host',
            'ref': 'ref',
            'volume_type': fake_volume_type,
            'metadata': {},
            'availability_zone': 'availability_zone',
            'bootable': 'bootable',
            'volume_type_id': fake_volume_type.id,
            'cluster_name': 'fake_cluster'
        }
        task = manage_existing.EntryCreateTask(fake_volume_api.FakeDb())

        result = task.execute(self.ctxt, **spec)
        self.assertTrue(result['volume_properties']['multiattach'])

    def test_revert_manage_existing(self):
        fake_db = fake_volume_api.FakeDb()
        fake_db.volume_destroy = mock.MagicMock()

        self.ctxt.elevated = mock.Mock()

        task = manage_existing.EntryCreateTask(fake_db)
        task.revert(self.ctxt, {'volume_id': fakes.VOLUME_ID})

        # Check DB entry task is destroyed
        fake_db.volume_destroy.assert_called_once_with(
            self.ctxt.elevated.return_value,
            fakes.VOLUME_ID
        )

    def test_revert_manage_existing_with_ft_failure(self):
        fake_db = fake_volume_api.FakeDb()
        fake_db.volume_destroy = mock.MagicMock()

        mock_failure = mock.Mock(spec=ft.Failure)
        task = manage_existing.EntryCreateTask(fake_db)
        task.revert(self.ctxt, mock_failure)

        # Check DB entry task is not destroyed
        fake_db.volume_destroy.assert_not_called()

    @staticmethod
    def _stub_volume_object_get(self):
        volume = {
            'id': fakes.VOLUME_ID,
            'volume_type_id': fakes.VOLUME_TYPE_ID,
            'status': 'creating',
            'name': fakes.VOLUME_NAME,
        }
        return fake_volume.fake_volume_obj(self.ctxt, **volume)

    def test_prepare_for_quota_reserveration_task_execute(self):
        mock_db = mock.MagicMock()
        mock_driver = mock.MagicMock()
        mock_manage_existing_ref = mock.MagicMock()
        mock_get_size = self.mock_object(
            mock_driver, 'manage_existing_get_size')
        mock_get_size.return_value = '5'

        volume_ref = self._stub_volume_object_get(self)
        task = manager.PrepareForQuotaReservationTask(mock_db, mock_driver)

        result = task.execute(self.ctxt, volume_ref, mock_manage_existing_ref)

        self.assertEqual(volume_ref, result['volume_properties'])
        self.assertEqual('5', result['size'])
        self.assertEqual(volume_ref.id, result['volume_spec']['volume_id'])
        mock_get_size.assert_called_once_with(
            volume_ref, mock_manage_existing_ref)

    def test_prepare_for_quota_reservation_task_revert(self):
        mock_db = mock.MagicMock()
        mock_driver = mock.MagicMock()
        mock_result = mock.MagicMock()
        mock_flow_failures = mock.MagicMock()
        mock_error_out = self.mock_object(flow_common, 'error_out')
        volume_ref = self._stub_volume_object_get(self)
        task = manager.PrepareForQuotaReservationTask(mock_db, mock_driver)

        task.revert(self.ctxt, mock_result, mock_flow_failures, volume_ref)
        mock_error_out.assert_called_once_with(volume_ref,
                                               reason='Volume manage failed.',
                                               status='error_managing')

    def test_prepare_for_quota_reservation_with_wrong_volume(self):
        """Test the class PrepareForQuotaReservationTas with wrong vol."""
        mock_db = mock.MagicMock()
        mock_driver = mock.MagicMock()
        wrong_volume = mock.MagicMock()
        mock_manage_existing_ref = mock.MagicMock()
        mock_except = exception.CinderException

        mock_driver.manage_existing_get_size.side_effect = mock_except
        task = manager.PrepareForQuotaReservationTask(mock_db, mock_driver)
        self.assertRaises(exception.CinderException,
                          task.execute,
                          self.ctxt,
                          wrong_volume,
                          mock_manage_existing_ref)

    def test_manage_existing_task(self):
        """Test the class ManageExistingTask."""
        mock_db = mock.MagicMock()
        mock_driver = mock.MagicMock()
        mock_volume = mock.MagicMock()
        mock_manage_existing_ref = mock.MagicMock()
        mock_size = mock.MagicMock()

        task = manager.ManageExistingTask(mock_db, mock_driver)
        rv = task.execute(self.ctxt, mock_volume, mock_manage_existing_ref,
                          mock_size)

        expected_output = {'volume': mock_volume}
        self.assertDictEqual(rv, expected_output)

    def test_manage_existing_task_with_wrong_volume(self):
        """Test the class ManageExistingTask with wrong volume."""
        mock_db = mock.MagicMock()
        mock_driver = mock.MagicMock()
        mock_volume = mock.MagicMock()
        mock_volume.update.side_effect = exception.CinderException
        mock_manage_existing_ref = mock.MagicMock()
        mock_size = mock.MagicMock()

        task = manager.ManageExistingTask(mock_db, mock_driver)
        self.assertRaises(exception.CinderException,
                          task.execute,
                          self.ctxt,
                          mock_volume,
                          mock_manage_existing_ref,
                          mock_size)

    def test_get_flow(self):
        mock_volume_flow = mock.Mock()
        mock_linear_flow = self.mock_object(manager.linear_flow, 'Flow')
        mock_linear_flow.return_value = mock_volume_flow
        mock_taskflow_engine = self.mock_object(taskflow.engines, 'load')
        expected_store = {
            'context': mock.sentinel.context,
            'volume': mock.sentinel.volume,
            'manage_existing_ref': mock.sentinel.ref,
            'group_snapshot': None,
            'optional_args': {'is_quota_committed': False,
                              'update_size': True}
        }

        manager.get_flow(
            mock.sentinel.context, mock.sentinel.db, mock.sentinel.driver,
            mock.sentinel.host, mock.sentinel.volume, mock.sentinel.ref)

        mock_linear_flow.assert_called_once_with(
            'volume_manage_existing_manager')
        mock_taskflow_engine.assert_called_once_with(
            mock_volume_flow, store=expected_store)

    def test_get_flow_volume_flow_tasks(self):
        """Test that all expected parameter names exist for added tasks."""
        mock_taskflow_engine = self.mock_object(taskflow.engines, 'load')
        mock_taskflow_engine.side_effect = self._verify_volume_flow_tasks

        manager.get_flow(
            mock.sentinel.context, mock.sentinel.db, mock.sentinel.driver,
            mock.sentinel.host, mock.sentinel.volume, mock.sentinel.ref)

    def _verify_volume_flow_tasks(self, volume_flow, store=None):
        param_names = [
            'context',
            'volume',
            'manage_existing_ref',
            'group_snapshot',
            'optional_args',
        ]

        provides = {'self'}
        revert_provides = ['self', 'result', 'flow_failures']
        for node in volume_flow.iter_nodes():
            task = node[0]
            # Subsequent tasks may use parameters defined in a previous task's
            # default_provides list. Add these names to the provides set.
            if task.default_provides:
                for p in task.default_provides:
                    provides.add(p)

            execute_args = inspect.getfullargspec(task.execute)[0]
            execute_args = [x for x in execute_args if x not in provides]
            [self.assertIn(arg, param_names) for arg in execute_args]

            revert_args = inspect.getfullargspec(task.revert)[0]
            revert_args = [x for x in revert_args if x not in revert_provides]
            [self.assertIn(arg, param_names) for arg in revert_args]
