# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
# Copyright (c) 2016 AT&T Corp
#
# 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 heatclient.v1 import stacks
from unittest import mock

from oslo_config import cfg

from murano.engine.system import heat_stack
from murano.tests.unit import base

CLS_NAME = 'murano.engine.system.heat_stack.HeatStack'
CONF = cfg.CONF


class TestHeatStack(base.MuranoTestCase):
    def setUp(self):
        super(TestHeatStack, self).setUp()
        self.heat_client_mock = mock.Mock()
        self.heat_client_mock.stacks = mock.MagicMock(spec=stacks.StackManager)
        self.override_config('stack_tags', ['test-murano'], 'heat')
        self.mock_tag = ','.join(CONF.heat.stack_tags)
        self._patch_get_client()

    def tearDown(self):
        super(TestHeatStack, self).tearDown()
        self.addCleanup(mock.patch.stopall)

    def _patch_get_client(self):
        self.get_client_patcher = mock.patch(
            'murano.engine.system.heat_stack.HeatStack._get_client',
            return_value=self.heat_client_mock)
        self.get_token_client_patcher = mock.patch.object(
            heat_stack.HeatStack, '_get_token_client',
            return_value=self.heat_client_mock)
        self.get_client_patcher.start()
        self.get_token_client_patcher.start()

    def _unpatch_get_client(self):
        self.get_client_patcher.stop()
        self.get_token_client_patcher.stop()

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_push_adds_version(self, status_get, wait_st):
        """Assert that if heat_template_version is omitted, it's added."""

        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        hs = heat_stack.HeatStack('test-stack', 'Generated by TestHeatStack')
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = {}
        hs._parameters = {}
        hs._applied = False
        hs.push()

        hs = heat_stack.HeatStack(
            'test-stack', 'Generated by TestHeatStack')
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._parameters = {}
        hs._applied = False
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'description': 'Generated by TestHeatStack',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={},
            environment={},
            tags=self.mock_tag
        )
        self.assertTrue(hs._applied)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_description_is_optional(self, status_get, wait_st):
        """Assert that if heat_template_version is omitted, it's added."""

        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        hs = heat_stack.HeatStack('test-stack', None)
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={},
            environment='',
            tags=self.mock_tag
        )
        self.assertTrue(hs._applied)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_heat_files_are_sent(self, status_get, wait_st):
        """Assert that if heat_template_version is omitted, it's added."""

        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {"heatFile": "file"}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={"heatFile": "file"},
            environment='',
            tags=self.mock_tag
        )
        self.assertTrue(hs._applied)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_heat_environments_are_sent(self, status_get, wait_st):
        """Assert that if heat_template_version is omitted, it's added."""

        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {"heatFile": "file"}
        hs._hot_environment = 'environments'
        hs._parameters = {}
        hs._applied = False
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={"heatFile": "file"},
            environment='environments',
            tags=self.mock_tag
        )
        self.assertTrue(hs._applied)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_heat_async_push(self, status_get, wait_st):
        """Assert that if heat_template_version is omitted, it's added."""

        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {"heatFile": "file"}
        hs._hot_environment = 'environments'
        hs._parameters = {}
        hs._applied = False
        with mock.patch('murano.dsl.dsl.get_execution_session'):
            hs.push(is_async=True)

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_not_called()
        hs.output()
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={"heatFile": "file"},
            environment='environments',
            tags=self.mock_tag
        )
        self.assertTrue(hs._applied)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    @mock.patch.object(heat_stack, 'LOG')
    def test_push_except_http_conflict(self, mock_log, status_get, wait_st):
        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {"heatFile": "file"}
        hs._hot_environment = 'environments'
        hs._parameters = {}
        hs._applied = False
        hs._get_token_client().stacks.create.side_effect = [
            heat_stack.heat_exc.HTTPConflict('test_error_msg'),
            None
        ]

        hs.push()

        mock_log.warning.assert_called_with(
            'Conflicting operation: ERROR: test_error_msg')

    @mock.patch(CLS_NAME + '.current')
    def test_update_wrong_template_version(self, current):
        """Template version other than expected should cause error."""

        hs = heat_stack.HeatStack(
            'test-stack', 'Generated by TestHeatStack')
        hs._template = {'resources': {'test': 1}}

        invalid_template = {
            'heat_template_version': 'something else'
        }

        current.return_value = {}

        e = self.assertRaises(heat_stack.HeatStackError,
                              hs.update_template,
                              invalid_template)
        err_msg = "Currently only heat_template_version 2013-05-23 "\
                  "is supported."
        self.assertEqual(err_msg, str(e))

        # Check it's ok without a version
        hs.update_template({})
        expected = {'resources': {'test': 1}}
        self.assertEqual(expected, hs._template)

        # .. or with a good version
        hs.update_template({'heat_template_version': '2013-05-23'})
        expected['heat_template_version'] = '2013-05-23'
        self.assertEqual(expected, hs._template)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_heat_stack_tags_are_sent(self, status_get, wait_st):
        """Assert heat_stack tags are sent

        Assert that heat_stack `tags` parameter get push & with
        value from config parameter `stack_tags`.
        """

        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        self.override_config('stack_tags', ['test-murano', 'murano-tag'],
                             'heat')
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs._tags = ','.join(CONF.heat.stack_tags)
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={},
            environment='',
            tags=','.join(CONF.heat.stack_tags)
        )
        self.assertTrue(hs._applied)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_parameters(self, status_get, wait_st):
        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        self.override_config('stack_tags', ['test-murano', 'murano-tag'],
                             'heat')
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs._tags = ','.join(CONF.heat.stack_tags)
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={},
            environment='',
            tags=','.join(CONF.heat.stack_tags)
        )

        self.assertEqual(hs.parameters(), hs._parameters)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_reload(self, status_get, wait_st):
        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        self.override_config('stack_tags', ['test-murano', 'murano-tag'],
                             'heat')
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs._tags = ','.join(CONF.heat.stack_tags)
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={},
            environment='',
            tags=','.join(CONF.heat.stack_tags)
        )

        hs.reload()
        stack_info = self.heat_client_mock.stacks.get(stack_id=hs._name)
        self.assertEqual(hs._template, hs._client.stacks.template(
            stack_id='{0}/{1}'.format(
                stack_info.stack_name,
                stack_info.id)))

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_delete(self, status_get, wait_st):
        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        self.override_config('stack_tags', ['test-murano', 'murano-tag'],
                             'heat')
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs._tags = ','.join(CONF.heat.stack_tags)
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={},
            environment='',
            tags=','.join(CONF.heat.stack_tags)
        )

        hs.delete()
        self.assertEqual({}, hs._template)
        self.assertTrue(hs._applied)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    @mock.patch.object(heat_stack, 'LOG')
    def test_delete_except_not_found(self, mock_log, status_get, wait_st):
        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}

        hs = heat_stack.HeatStack('test-stack', None)
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs.push()

        hs._client.stacks.delete.side_effect =\
            heat_stack.heat_exc.NotFound
        hs.delete()

        self.assertTrue(hs._applied)
        self.assertEqual({}, hs._template)
        mock_log.warning.assert_called_with(
            'Stack test-stack already deleted?')

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    @mock.patch.object(heat_stack, 'LOG')
    def test_delete_except_http_conflict(self, mock_log, status_get, wait_st):
        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}

        hs = heat_stack.HeatStack('test-stack', None)
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs.push()

        hs._client.stacks.delete.side_effect = [
            heat_stack.heat_exc.HTTPConflict('test_error_msg'),
            None
        ]
        hs.delete()

        self.assertTrue(hs._applied)
        self.assertEqual({}, hs._template)
        mock_log.warning.assert_called_with('Conflicting operation: '
                                            'ERROR: test_error_msg')

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_set_template_and_params(self, status_get, wait_st):
        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        self.override_config('stack_tags', ['test-murano', 'murano-tag'],
                             'heat')
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs._tags = ','.join(CONF.heat.stack_tags)
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={},
            environment='',
            tags=','.join(CONF.heat.stack_tags)
        )

        new_template = {'resources': {'test': 2}}
        new_parameters = {'parameters': {'test': 1}}
        hs.set_template(new_template)
        self.assertEqual(new_template, hs._template)
        hs.set_parameters(new_parameters)
        self.assertEqual(new_parameters, hs._parameters)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_set_hot_env_and_files(self, status_get, wait_st):
        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        self.override_config('stack_tags', ['test-murano', 'murano-tag'],
                             'heat')
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs._tags = ','.join(CONF.heat.stack_tags)
        hs.push()

        expected_template = {
            'heat_template_version': '2013-05-23',
            'resources': {'test': 1}
        }
        self.heat_client_mock.stacks.create.assert_called_with(
            stack_name='test-stack',
            disable_rollback=True,
            parameters={},
            template=expected_template,
            files={},
            environment='',
            tags=','.join(CONF.heat.stack_tags)
        )

        new_hot_env = 'test'
        new_files = {'files': {'test': 1}}
        hs.set_hot_environment(new_hot_env)
        self.assertEqual(new_hot_env, hs._hot_environment)
        hs.set_files(new_files)
        self.assertEqual(new_files, hs._files)

    @mock.patch(CLS_NAME + '._wait_state')
    @mock.patch(CLS_NAME + '._get_status')
    def test_none_template(self, status_get, wait_st):
        status_get.return_value = 'NOT_FOUND'
        wait_st.return_value = {}
        self.override_config('stack_tags', ['test-murano', 'murano-tag'],
                             'heat')
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = None
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = True
        hs._tags = ','.join(CONF.heat.stack_tags)
        self.assertIsNone(hs.push())

    @mock.patch(CLS_NAME + '._wait_state')
    def test_get_hot_status(self, wait_st):
        wait_st.return_value = {}
        self.override_config('stack_tags', ['test-murano', 'murano-tag'],
                             'heat')
        hs = heat_stack.HeatStack('test-stack', None)
        hs._description = None
        hs._template = {'resources': {'test': 1}}
        hs._files = {}
        hs._hot_environment = ''
        hs._parameters = {}
        hs._applied = False
        hs._tags = ','.join(CONF.heat.stack_tags)
        hs.push()

        self.assertIsNone(hs._get_status())
        self.assertTrue(wait_st.called)
        self.assertEqual({}, hs.output())

    def test_current_except_http_notfound(self):
        hs = heat_stack.HeatStack(
            'test-stack', 'Generated by TestHeatStack')
        hs._template = None
        hs._applied = False
        hs._parameters = {'param1': 'val1', 'param2': 'val2'}
        hs._client.stacks.get.side_effect = heat_stack.heat_exc.HTTPNotFound

        current = hs.current()

        self.assertEqual({}, current)
        self.assertEqual(True, hs._applied)
        self.assertEqual({}, hs._template)
        self.assertEqual({}, hs._parameters)

    @mock.patch.object(heat_stack, 'auth_utils')
    def test_get_client(self, mock_auth_utils):
        self._unpatch_get_client()

        mock_auth_utils.get_session_client_parameters.return_value =\
            {'endpoint': 'test_endpoint/v1'}

        client = heat_stack.HeatStack._get_client('test_region_name')

        self.assertIsNotNone(client)
        self.assertEqual("<class 'heatclient.v1.client.Client'>",
                         str(client.__class__))
        mock_auth_utils.get_client_session.assert_called_with(
            conf='heat')

    @mock.patch.object(heat_stack, 'auth_utils')
    def test_get_token_client(self, mock_auth_utils):
        self._unpatch_get_client()

        mock_auth_utils.get_session_client_parameters.return_value =\
            {'endpoint': 'test_endpoint/v1'}

        hs = heat_stack.HeatStack('test-stack', 'Generated by TestHeatStack')

        token_client = hs._get_token_client()

        self.assertIsNotNone(token_client)
        self.assertEqual("<class 'heatclient.v1.client.Client'>",
                         str(token_client.__class__))
        mock_auth_utils.get_token_client_session.assert_called_with(
            conf='heat')

    def test_wait_state(self):
        hs = heat_stack.HeatStack('test-stack', None)
        hs._client.stacks.get.return_value =\
            mock.Mock(stack_status='CREATE_COMPLETE')

        result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE')

        self.assertEqual({}, result)

    def test_wait_state_with_outputs(self):
        hs = heat_stack.HeatStack('test-stack', None)
        hs._client.stacks.get.side_effect = [
            mock.Mock(stack_status='IN_PROGRESS'),
            mock.Mock(stack_status='CREATE_COMPLETE',
                      outputs=[{'output_key': 'key1', 'output_value': 'val1'},
                               {'output_key': 'key2', 'output_value': 'val2'}])
        ]

        result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE')

        self.assertNotEqual({}, result)
        self.assertEqual({'key1': 'val1', 'key2': 'val2'}, result)

    def test_wait_state_with_multiple_states(self):
        """Test that only the first state is checked."""

        hs = heat_stack.HeatStack('test-stack', None)
        hs._client.stacks.get.side_effect = [
            mock.Mock(stack_status=['IN_PROGRESS', 'NOT_FOUND']),
            mock.Mock(stack_status='CREATE_COMPLETE')
        ]

        result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE')

        self.assertEqual({}, result)

    @mock.patch.object(heat_stack, 'eventlet')
    def test_wait_state_with_wait_progress_true(self, mock_eventlet):
        hs = heat_stack.HeatStack('test-stack', None)
        hs._last_stack_timestamps = ('creation_time', 'updated_time')
        hs._client.stacks.get.side_effect = [
            mock.Mock(stack_status='TEST_STATUS',
                      creation_time='creation_time',
                      updated_time='updated_time'),
            mock.Mock(stack_status='TEST_STATUS',
                      creation_time='creation_time',
                      updated_time='updated_time'),
            mock.Mock(stack_status='CREATE_COMPLETE')
        ]

        result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE',
                                wait_progress=True)

        self.assertEqual({}, result)
        self.assertEqual(3, hs._client.stacks.get.call_count)
        self.assertEqual(2, mock_eventlet.sleep.call_count)
        expected_calls = [mock.call.sleep(2), mock.call.sleep(2)]
        self.assertEqual(expected_calls, mock_eventlet.sleep.mock_calls)

    def test_wait_state_except_http_not_found(self):
        hs = heat_stack.HeatStack('test-stack', None)
        hs._client.stacks.get.side_effect = heat_stack.heat_exc.HTTPNotFound

        # If NOT FOUND is the expected status, then should run successfully.
        result = hs._wait_state(lambda status: status == 'NOT_FOUND')
        self.assertEqual({}, result)

        # Else EnvironmentError should be thrown.
        expected_error_msg = "Unexpected stack state {0}"\
            .format('NOT_FOUND')
        with self.assertRaisesRegex(EnvironmentError,
                                    expected_error_msg):
            hs._wait_state(lambda status: status == 'CREATE_COMPLETE')

    @mock.patch.object(heat_stack, 'eventlet')
    def test_wait_state_except_general_exception(self, mock_eventlet):
        """Test whether 4 tries are executed before exception raised."""

        hs = heat_stack.HeatStack('test-stack', None)
        hs._client.stacks.get.side_effect = Exception('test_exception_msg')

        with self.assertRaisesRegex(Exception,
                                    'test_exception_msg'):
            hs._wait_state(lambda status: status == 'CREATE_COMPLETE')

        expected_calls = [mock.call.sleep(2), mock.call.sleep(4),
                          mock.call.sleep(8)]
        self.assertEqual(4, hs._client.stacks.get.call_count)
        self.assertEqual(3, mock_eventlet.sleep.call_count)
        self.assertEqual(expected_calls, mock_eventlet.sleep.mock_calls)

    def test_wait_state_except_environment_error(self):
        hs = heat_stack.HeatStack('test-stack', None)
        hs._client.stacks.get.side_effect = [
            mock.Mock(stack_status='IN_PROGRESS'),
            mock.Mock(stack_status='UNEXPECTED_STATUS',
                      stack_status_reason='test_reason')
        ]

        expected_error_msg = "Unexpected stack state {0}: {1}"\
            .format('UNEXPECTED_STATUS', 'test_reason')
        with self.assertRaisesRegex(EnvironmentError,
                                    expected_error_msg):
            hs._wait_state(lambda status: status == 'CREATE_COMPLETE')
