# 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.

from unittest import mock
import uuid

from oslo_config import cfg
from oslo_messaging import conffixture
from oslo_messaging.rpc import dispatcher

from heat.common import exception
from heat.common import template_format
from heat.engine import service
from heat.engine import stack
from heat.objects import snapshot as snapshot_objects
from heat.tests import common
from heat.tests.engine import tools
from heat.tests import utils


class SnapshotServiceTest(common.HeatTestCase):
    # TODO(Qiming): Rework this test to handle OS::Nova::Server which
    # has a real snapshot support.
    def setUp(self):
        super(SnapshotServiceTest, self).setUp()
        self.ctx = utils.dummy_context()

        self.engine = service.EngineService('a-host', 'a-topic')
        self.engine.thread_group_mgr = tools.DummyThreadGroupManager()
        self.useFixture(conffixture.ConfFixture(cfg.CONF))

    def _create_stack(self, stack_name, files=None):
        t = template_format.parse(tools.wp_template)
        stk = utils.parse_stack(t, stack_name=stack_name, files=files)
        stk.state_set(stk.CREATE, stk.COMPLETE, 'mock completion')

        return stk

    def test_show_snapshot_not_found(self):
        stk = self._create_stack('stack_snapshot_not_found')
        snapshot_id = str(uuid.uuid4())
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.show_snapshot,
                               self.ctx, stk.identifier(),
                               snapshot_id)
        expected = 'Snapshot with id %s not found' % snapshot_id
        self.assertEqual(exception.NotFound, ex.exc_info[0])
        self.assertIn(expected, str(ex.exc_info[1]))

    def test_show_snapshot_not_belong_to_stack(self):
        stk1 = self._create_stack('stack_snaphot_not_belong_to_stack_1')
        stk1._persist_state()
        snapshot1 = self.engine.stack_snapshot(
            self.ctx, stk1.identifier(), 'snap1')
        snapshot_id = snapshot1['id']

        stk2 = self._create_stack('stack_snaphot_not_belong_to_stack_2')
        stk2._persist_state()
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.show_snapshot,
                               self.ctx, stk2.identifier(),
                               snapshot_id)
        expected = ('The Snapshot (%(snapshot)s) for Stack (%(stack)s) '
                    'could not be found') % {'snapshot': snapshot_id,
                                             'stack': stk2.name}
        self.assertEqual(exception.SnapshotNotFound, ex.exc_info[0])
        self.assertIn(expected, str(ex.exc_info[1]))

    @mock.patch.object(stack.Stack, 'load')
    def test_create_snapshot(self, mock_load):
        files = {'a_file': 'the contents'}
        stk = self._create_stack('stack_snapshot_create', files=files)
        mock_load.return_value = stk

        snapshot = self.engine.stack_snapshot(
            self.ctx, stk.identifier(), 'snap1')
        self.assertIsNotNone(snapshot['id'])
        self.assertIsNotNone(snapshot['creation_time'])
        self.assertEqual('snap1', snapshot['name'])
        self.assertEqual("CREATE", snapshot['action'])
        self.assertEqual("IN_PROGRESS", snapshot['status'])
        snapshot = self.engine.show_snapshot(
            self.ctx, stk.identifier(), snapshot['id'])
        self.assertEqual("CREATE", snapshot['action'])
        self.assertEqual("COMPLETE", snapshot['status'])
        self.assertEqual("SNAPSHOT", snapshot['data']['action'])
        self.assertEqual("COMPLETE", snapshot['data']['status'])
        self.assertEqual(files, snapshot['data']['files'])
        self.assertEqual(stk.id, snapshot['data']['id'])
        self.assertIsNotNone(stk.updated_time)
        self.assertIsNotNone(snapshot['creation_time'])
        mock_load.assert_called_once_with(self.ctx, stack=mock.ANY)

    @mock.patch.object(stack.Stack, 'load')
    def test_create_snapshot_exceeds_max_per_stack(self, mock_load):
        stk = self._create_stack('stack_snapshot_exceeds_max')
        mock_load.return_value = stk

        cfg.CONF.set_override('max_snapshots_per_stack', 0)
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.stack_snapshot,
                               self.ctx, stk.identifier(), 'snap_none')
        self.assertEqual(exception.RequestLimitExceeded, ex.exc_info[0])
        self.assertIn("You have reached the maximum snapshots per stack",
                      str(ex.exc_info[1]))

    @mock.patch.object(stack.Stack, 'load')
    def test_create_snapshot_action_in_progress(self, mock_load):
        stack_name = 'stack_snapshot_action_in_progress'
        stk = self._create_stack(stack_name)
        mock_load.return_value = stk

        stk.state_set(stk.UPDATE, stk.IN_PROGRESS, 'test_override')
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.stack_snapshot,
                               self.ctx, stk.identifier(), 'snap_none')
        self.assertEqual(exception.ActionInProgress, ex.exc_info[0])
        msg = ("Stack %(stack)s already has an action (%(action)s) "
               "in progress.") % {'stack': stack_name, 'action': stk.action}
        self.assertEqual(msg, str(ex.exc_info[1]))

        mock_load.assert_called_once_with(self.ctx, stack=mock.ANY)

    @mock.patch.object(stack.Stack, 'load')
    def test_delete_snapshot_not_found(self, mock_load):
        stk = self._create_stack('stack_snapshot_delete_not_found')
        mock_load.return_value = stk

        snapshot_id = str(uuid.uuid4())
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.delete_snapshot,
                               self.ctx, stk.identifier(), snapshot_id)
        self.assertEqual(exception.NotFound, ex.exc_info[0])

        mock_load.assert_called_once_with(self.ctx, stack=mock.ANY)

    @mock.patch.object(stack.Stack, 'load')
    def test_delete_snapshot_not_belong_to_stack(self, mock_load):
        stk1 = self._create_stack('stack_snapshot_delete_not_belong_1')
        mock_load.return_value = stk1

        snapshot1 = self.engine.stack_snapshot(
            self.ctx, stk1.identifier(), 'snap1')
        snapshot_id = snapshot1['id']

        mock_load.assert_called_once_with(self.ctx, stack=mock.ANY)
        mock_load.reset_mock()

        stk2 = self._create_stack('stack_snapshot_delete_not_belong_2')
        mock_load.return_value = stk2

        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.delete_snapshot,
                               self.ctx,
                               stk2.identifier(),
                               snapshot_id)
        expected = ('The Snapshot (%(snapshot)s) for Stack (%(stack)s) '
                    'could not be found') % {'snapshot': snapshot_id,
                                             'stack': stk2.name}
        self.assertEqual(exception.SnapshotNotFound, ex.exc_info[0])
        self.assertIn(expected, str(ex.exc_info[1]))

        mock_load.assert_called_once_with(self.ctx, stack=mock.ANY)
        mock_load.reset_mock()

    @mock.patch.object(stack.Stack, 'load')
    def test_delete_snapshot_in_progress(self, mock_load):
        # can not delete the snapshot in snapshotting
        stk = self._create_stack('test_delete_snapshot_in_progress')
        mock_load.return_value = stk
        snapshot = mock.Mock()
        snapshot.id = str(uuid.uuid4())
        snapshot.action = 'CREATE'
        snapshot.status = 'IN_PROGRESS'
        self.patchobject(snapshot_objects.Snapshot,
                         'get_snapshot_by_stack').return_value = snapshot
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.delete_snapshot,
                               self.ctx, stk.identifier(), snapshot.id)
        msg = 'Deleting in-progress snapshot is not supported'
        self.assertIn(msg, str(ex.exc_info[1]))
        self.assertEqual(exception.NotSupported, ex.exc_info[0])

    @mock.patch.object(stack.Stack, 'load')
    def test_delete_snapshot(self, mock_load):
        stk = self._create_stack('stack_snapshot_delete_normal')
        mock_load.return_value = stk

        snapshot = self.engine.stack_snapshot(
            self.ctx, stk.identifier(), 'snap1')
        snapshot_id = snapshot['id']
        self.engine.delete_snapshot(self.ctx, stk.identifier(), snapshot_id)

        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.show_snapshot, self.ctx,
                               stk.identifier(), snapshot_id)
        self.assertEqual(exception.NotFound, ex.exc_info[0])

        self.assertEqual(2, mock_load.call_count)

    @mock.patch.object(stack.Stack, 'load')
    def test_list_snapshots(self, mock_load):
        stk = self._create_stack('stack_snapshot_list')
        mock_load.return_value = stk

        snapshot = self.engine.stack_snapshot(
            self.ctx, stk.identifier(), 'snap1')
        self.assertIsNotNone(snapshot['id'])
        self.assertEqual("IN_PROGRESS", snapshot['status'])

        snapshots = self.engine.stack_list_snapshots(
            self.ctx, stk.identifier())
        expected = {
            "id": snapshot["id"],
            "name": "snap1",
            "action": "CREATE",
            "status": "COMPLETE",
            "status_reason": "Stack SNAPSHOT completed successfully",
            "data": stk.prepare_abandon(),
            "creation_time": snapshot['creation_time']}

        self.assertEqual([expected], snapshots)
        mock_load.assert_called_once_with(self.ctx, stack=mock.ANY)

    @mock.patch.object(stack.Stack, 'load')
    def test_restore_snapshot(self, mock_load):
        stk = self._create_stack('stack_snapshot_restore_normal')
        mock_load.return_value = stk

        snapshot = self.engine.stack_snapshot(
            self.ctx, stk.identifier(), 'snap1')
        snapshot_id = snapshot['id']
        self.engine.stack_restore(self.ctx, stk.identifier(), snapshot_id)
        self.assertEqual((stk.RESTORE, stk.COMPLETE), stk.state)
        self.assertEqual(2, mock_load.call_count)

    @mock.patch.object(stack.Stack, 'load')
    def test_restore_snapshot_other_stack(self, mock_load):
        stk1 = self._create_stack('stack_snapshot_restore_other_stack_1')
        mock_load.return_value = stk1

        snapshot1 = self.engine.stack_snapshot(
            self.ctx, stk1.identifier(), 'snap1')
        snapshot_id = snapshot1['id']

        mock_load.assert_called_once_with(self.ctx, stack=mock.ANY)
        mock_load.reset_mock()

        stk2 = self._create_stack('stack_snapshot_restore_other_stack_2')
        mock_load.return_value = stk2

        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.stack_restore,
                               self.ctx, stk2.identifier(),
                               snapshot_id)
        expected = ('The Snapshot (%(snapshot)s) for Stack (%(stack)s) '
                    'could not be found') % {'snapshot': snapshot_id,
                                             'stack': stk2.name}
        self.assertEqual(exception.SnapshotNotFound, ex.exc_info[0])
        self.assertIn(expected, str(ex.exc_info[1]))

        mock_load.assert_called_once_with(self.ctx, stack=mock.ANY)

    @mock.patch.object(stack.Stack, 'load')
    def test_restore_snapshot_in_progress(self, mock_load):
        stk = self._create_stack('stack_snapshot_restore_normal')
        mock_load.return_value = stk

        snapshot_obj = mock.Mock()
        snapshot_obj.id = str(uuid.uuid4())
        snapshot_obj.action = 'CREATE'
        snapshot_obj.status = 'IN_PROGRESS'
        self.patchobject(snapshot_objects.Snapshot,
                         'get_snapshot_by_stack').return_value = snapshot_obj
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.stack_restore,
                               self.ctx, stk.identifier(),
                               snapshot_obj.id)
        msg = 'Restoring incomplete snapshot is not supported'
        self.assertIn(msg, str(ex.exc_info[1]))
        self.assertEqual(exception.NotSupported, ex.exc_info[0])

    @mock.patch.object(stack.Stack, 'load')
    def test_restore_snapshot_failed(self, mock_load):
        stk = self._create_stack('stack_snapshot_restore_normal')
        mock_load.return_value = stk

        snapshot_obj = mock.Mock()
        snapshot_obj.id = str(uuid.uuid4())
        snapshot_obj.action = 'CREATE'
        snapshot_obj.status = 'FAILED'
        self.patchobject(snapshot_objects.Snapshot,
                         'get_snapshot_by_stack').return_value = snapshot_obj
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.stack_restore,
                               self.ctx, stk.identifier(),
                               snapshot_obj.id)
        msg = 'Restoring incomplete snapshot is not supported'
        self.assertIn(msg, str(ex.exc_info[1]))
        self.assertEqual(exception.NotSupported, ex.exc_info[0])


class SnapshotServiceConvergenceTest(common.HeatTestCase):

    def setUp(self):
        super(SnapshotServiceConvergenceTest, self).setUp()
        self.ctx = utils.dummy_context()
        cfg.CONF.set_default('convergence_engine', True)
        self.engine = service.EngineService('a-host', 'a-topic')
        self.engine.thread_group_mgr = tools.DummyThreadGroupManager()
        self.useFixture(conffixture.ConfFixture(cfg.CONF))

    def _create_convergence_stack(self, stack_name, files=None):
        t = template_format.parse(tools.wp_template)
        stk = utils.parse_stack(t, stack_name=stack_name, files=files)
        stk.convergence = True
        stk.state_set(stk.CREATE, stk.COMPLETE, 'mock completion')
        return stk

    @mock.patch.object(stack.Stack, 'load')
    def test_delete_snapshot_convergence_no_resources(self, mock_load):
        """Test convergence delete with no resources calls mark_complete."""
        stk = self._create_convergence_stack(
            'stack_snapshot_delete_conv_no_rsrc')
        mock_load.return_value = stk

        # Create snapshot with empty resources - this makes
        # do_delete_snapshot go through mark_complete directly
        data = stk.prepare_abandon(no_resources=True)
        snapshot_obj = snapshot_objects.Snapshot.create(
            self.ctx, {
                'tenant': self.ctx.project_id,
                'data': data,
                'name': 'snap1',
                'stack_id': stk.id,
                'action': 'CREATE',
                'status': 'COMPLETE'})
        snapshot_id = snapshot_obj.id

        self.engine.delete_snapshot(
            self.ctx, stk.identifier(), snapshot_id)

        # Snapshot should be deleted after the call returns
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.show_snapshot, self.ctx,
                               stk.identifier(), snapshot_id)
        self.assertEqual(exception.NotFound, ex.exc_info[0])

    @mock.patch.object(snapshot_objects.Snapshot, 'update')
    @mock.patch.object(stack.Stack, 'load')
    def test_delete_snapshot_convergence_with_resources(
            self, mock_load, mock_update):
        """Test convergence delete with resources initiates async delete."""
        stk = self._create_convergence_stack(
            'stack_snapshot_delete_conv_rsrc')
        mock_load.return_value = stk

        data = stk.prepare_abandon()
        snapshot_obj = snapshot_objects.Snapshot.create(
            self.ctx, {
                'tenant': self.ctx.project_id,
                'data': data,
                'name': 'snap1',
                'stack_id': stk.id,
                'action': 'CREATE',
                'status': 'COMPLETE'})
        snapshot_id = snapshot_obj.id

        self.engine.delete_snapshot(
            self.ctx, stk.identifier(), snapshot_id)

        mock_update.assert_called_once_with(
            mock.ANY, snapshot_id,
            {'action': 'DELETE',
             'status': 'IN_PROGRESS',
             'status_reason': 'Snapshot delete started'})

    @mock.patch.object(stack.Stack, 'load')
    def test_delete_snapshot_convergence_not_found(self, mock_load):
        """Test convergence delete of non-existent snapshot raises NotFound."""
        stk = self._create_convergence_stack(
            'stack_snapshot_delete_conv_not_found')
        mock_load.return_value = stk

        snapshot_id = str(uuid.uuid4())
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.delete_snapshot,
                               self.ctx, stk.identifier(), snapshot_id)
        self.assertEqual(exception.NotFound, ex.exc_info[0])

    @mock.patch.object(stack.Stack, 'load')
    def test_delete_snapshot_convergence_in_progress(self, mock_load):
        """Test convergence delete rejects in-progress snapshot."""
        stk = self._create_convergence_stack(
            'stack_snapshot_delete_conv_inprog')
        mock_load.return_value = stk

        snapshot_obj = mock.Mock()
        snapshot_obj.id = str(uuid.uuid4())
        snapshot_obj.action = 'CREATE'
        snapshot_obj.status = 'IN_PROGRESS'
        self.patchobject(snapshot_objects.Snapshot,
                         'get_snapshot_by_stack').return_value = snapshot_obj
        ex = self.assertRaises(dispatcher.ExpectedException,
                               self.engine.delete_snapshot,
                               self.ctx, stk.identifier(), snapshot_obj.id)
        msg = 'Deleting in-progress snapshot is not supported'
        self.assertIn(msg, str(ex.exc_info[1]))
        self.assertEqual(exception.NotSupported, ex.exc_info[0])
