#
#    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 contextlib
import json
import uuid

import mock
from oslo_config import cfg
from oslo_messaging import exceptions as msg_exceptions
from oslo_serialization import jsonutils
import six

from heat.common import exception
from heat.common import identifier
from heat.common import template_format
from heat.engine import node_data
from heat.engine import resource
from heat.engine.resources import stack_resource
from heat.engine import stack as parser
from heat.engine import template as templatem
from heat.objects import raw_template
from heat.objects import stack as stack_object
from heat.objects import stack_lock
from heat.rpc import api as rpc_api
from heat.tests import common
from heat.tests import generic_resource as generic_rsrc
from heat.tests import utils


ws_res_snippet = {"Type": "StackResourceType",
                  "Metadata": {
                      "key": "value",
                      "some": "more stuff"}}

param_template = '''
{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Parameters" : {
    "KeyName" : {
      "Description" : "KeyName",
      "Type" : "String",
      "Default" : "test"
    }
  },
  "Resources" : {
    "WebServer": {
      "Type": "GenericResource",
      "Properties": {}
    }
  }
}
'''


simple_template = '''
{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Parameters" : {},
  "Resources" : {
    "WebServer": {
      "Type": "GenericResource",
      "Properties": {}
    }
  }
}
'''

main_template = '''
heat_template_version: 2013-05-23
resources:
  volume_server:
      type: file://tmp/nested.yaml
'''

my_wrong_nested_template = '''
heat_template_version: 2013-05-23
resources:
  server:
    type: OS::Nova::Server
    properties:
      image: F17-x86_64-gold
      flavor: m1.small
  volume:
    type: OS::Cinder::Volume
    properties:
      size: 10
      description: Volume for stack
  volume_attachment:
    type: OS::Cinder::VolumeAttachment
    properties:
      volume_id: { get_resource: volume }
      instance_uuid: { get_resource: instance }
'''

resource_group_template = '''
heat_template_version: 2013-05-23
resources:
  my_resource_group:
    type: OS::Heat::ResourceGroup
    properties:
      resource_def:
        type: idontexist
'''

heat_autoscaling_group_template = '''
heat_template_version: 2013-05-23
resources:
  my_autoscaling_group:
    type: OS::Heat::AutoScalingGroup
    properties:
      resource:
        type: idontexist
      desired_capacity: 2
      max_size: 4
      min_size: 1
'''

nova_server_template = '''
heat_template_version: 2013-05-23
resources:
  group_server:
    type: idontexist
'''


class MyImplementedStackResource(generic_rsrc.StackResourceType):
    def child_template(self):
        return self.nested_template

    def child_params(self):
        return self.nested_params


class StackResourceBaseTest(common.HeatTestCase):
    def setUp(self):
        super(StackResourceBaseTest, self).setUp()
        self.ws_resname = "provider_resource"
        self.empty_temp = templatem.Template(
            {'HeatTemplateFormatVersion': '2012-12-12',
             'Resources': {self.ws_resname: ws_res_snippet}})
        self.ctx = utils.dummy_context()
        self.parent_stack = parser.Stack(self.ctx, 'test_stack',
                                         self.empty_temp,
                                         stack_id=str(uuid.uuid4()),
                                         user_creds_id='uc123',
                                         stack_user_project_id='aprojectid')
        resource_defns = self.empty_temp.resource_definitions(
            self.parent_stack)
        self.parent_resource = generic_rsrc.StackResourceType(
            'test', resource_defns[self.ws_resname], self.parent_stack)


class StackResourceTest(StackResourceBaseTest):

    def setUp(self):
        super(StackResourceTest, self).setUp()
        self.templ = template_format.parse(param_template)
        self.simple_template = template_format.parse(simple_template)

        # to get same json string from a dict for comparison,
        # make sort_keys True
        orig_dumps = jsonutils.dumps

        def sorted_dumps(*args, **kwargs):
            kwargs.setdefault('sort_keys', True)
            return orig_dumps(*args, **kwargs)
        patched_dumps = mock.patch(
            'oslo_serialization.jsonutils.dumps', sorted_dumps)
        patched_dumps.start()
        self.addCleanup(lambda: patched_dumps.stop())

    def test_child_template_defaults_to_not_implemented(self):
        self.assertRaises(NotImplementedError,
                          self.parent_resource.child_template)

    def test_child_params_defaults_to_not_implemented(self):
        self.assertRaises(NotImplementedError,
                          self.parent_resource.child_params)

    def test_preview_defaults_to_stack_resource_itself(self):
        preview = self.parent_resource.preview()
        self.assertIsInstance(preview, stack_resource.StackResource)

    def test_nested_stack_abandon(self):
        nest = mock.MagicMock()
        self.parent_resource.nested = nest
        nest.return_value.prepare_abandon.return_value = {'X': 'Y'}
        ret = self.parent_resource.prepare_abandon()
        nest.return_value.prepare_abandon.assert_called_once_with()
        self.assertEqual({'X': 'Y'}, ret)

    def test_nested_abandon_stack_not_found(self):
        self.parent_resource.nested = mock.MagicMock(return_value=None)
        ret = self.parent_resource.prepare_abandon()
        self.assertEqual({}, ret)

    def test_abandon_nested_sends_rpc_abandon(self):
        rpcc = mock.MagicMock()

        @contextlib.contextmanager
        def exc_filter(*args):
            try:
                yield
            except exception.NotFound:
                pass

        rpcc.ignore_error_by_name.side_effect = exc_filter
        self.parent_resource.rpc_client = rpcc
        self.parent_resource.resource_id = 'fake_id'

        self.parent_resource.prepare_abandon()
        self.parent_resource.delete_nested()

        rpcc.return_value.abandon_stack.assert_called_once_with(
            self.parent_resource.context, mock.ANY)
        rpcc.return_value.delete_stack.assert_not_called()

    def test_propagated_files(self):
        """Test passing of the files map in the top level to the child.

        Makes sure that the files map in the top level stack are passed on to
        the child stack.
        """
        self.parent_stack.t.files["foo"] = "bar"
        parsed_t = self.parent_resource._parse_child_template(self.templ, None)
        self.assertEqual({"foo": "bar"}, parsed_t.files.files)

    @mock.patch('heat.engine.environment.get_child_environment')
    @mock.patch.object(stack_resource.parser, 'Stack')
    def test_preview_with_implemented_child_resource(self, mock_stack_class,
                                                     mock_env_class):
        nested_stack = mock.Mock()
        mock_stack_class.return_value = nested_stack
        nested_stack.preview_resources.return_value = 'preview_nested_stack'
        mock_env_class.return_value = 'environment'
        template = templatem.Template(template_format.parse(param_template))
        parent_t = self.parent_stack.t
        resource_defns = parent_t.resource_definitions(self.parent_stack)
        parent_resource = MyImplementedStackResource(
            'test',
            resource_defns[self.ws_resname],
            self.parent_stack)
        params = {'KeyName': 'test'}
        parent_resource.set_template(template, params)
        validation_mock = mock.Mock(return_value=None)
        parent_resource._validate_nested_resources = validation_mock

        result = parent_resource.preview()
        mock_env_class.assert_called_once_with(
            self.parent_stack.env,
            params,
            child_resource_name='test',
            item_to_remove=None)
        self.assertEqual('preview_nested_stack', result)
        mock_stack_class.assert_called_once_with(
            mock.ANY,
            'test_stack-test',
            mock.ANY,
            timeout_mins=None,
            disable_rollback=True,
            parent_resource=parent_resource.name,
            owner_id=self.parent_stack.id,
            user_creds_id=self.parent_stack.user_creds_id,
            stack_user_project_id=self.parent_stack.stack_user_project_id,
            adopt_stack_data=None,
            nested_depth=1
        )

    @mock.patch('heat.engine.environment.get_child_environment')
    @mock.patch.object(stack_resource.parser, 'Stack')
    def test_preview_with_implemented_dict_child_resource(self,
                                                          mock_stack_class,
                                                          mock_env_class):
        nested_stack = mock.Mock()
        mock_stack_class.return_value = nested_stack
        nested_stack.preview_resources.return_value = 'preview_nested_stack'
        mock_env_class.return_value = 'environment'
        template_dict = template_format.parse(param_template)
        parent_t = self.parent_stack.t
        resource_defns = parent_t.resource_definitions(self.parent_stack)
        parent_resource = MyImplementedStackResource(
            'test',
            resource_defns[self.ws_resname],
            self.parent_stack)
        params = {'KeyName': 'test'}
        parent_resource.set_template(template_dict, params)
        validation_mock = mock.Mock(return_value=None)
        parent_resource._validate_nested_resources = validation_mock

        result = parent_resource.preview()
        mock_env_class.assert_called_once_with(
            self.parent_stack.env,
            params,
            child_resource_name='test',
            item_to_remove=None)
        self.assertEqual('preview_nested_stack', result)
        mock_stack_class.assert_called_once_with(
            mock.ANY,
            'test_stack-test',
            mock.ANY,
            timeout_mins=None,
            disable_rollback=True,
            parent_resource=parent_resource.name,
            owner_id=self.parent_stack.id,
            user_creds_id=self.parent_stack.user_creds_id,
            stack_user_project_id=self.parent_stack.stack_user_project_id,
            adopt_stack_data=None,
            nested_depth=1
        )

    def test_preview_propagates_files(self):
        self.parent_stack.t.files["foo"] = "bar"
        tmpl = self.parent_stack.t.t
        self.parent_resource.child_template = mock.Mock(return_value=tmpl)
        self.parent_resource.child_params = mock.Mock(return_value={})
        self.parent_resource.preview()
        self.stack = self.parent_resource.nested()
        self.assertEqual({"foo": "bar"}, self.stack.t.files.files)

    def test_preview_validates_nested_resources(self):
        parent_t = self.parent_stack.t
        resource_defns = parent_t.resource_definitions(self.parent_stack)
        stk_resource = MyImplementedStackResource(
            'test',
            resource_defns[self.ws_resname],
            self.parent_stack)
        stk_resource.child_params = mock.Mock(return_value={})
        stk_resource.child_template = mock.Mock(
            return_value=templatem.Template(self.simple_template,
                                            stk_resource.child_params))
        exc = exception.RequestLimitExceeded(message='Validation Failed')
        validation_mock = mock.Mock(side_effect=exc)
        stk_resource._validate_nested_resources = validation_mock

        self.assertRaises(exception.RequestLimitExceeded,
                          stk_resource.preview)

    def test_parent_stack_existing_of_nested_stack(self):
        parent_t = self.parent_stack.t
        resource_defns = parent_t.resource_definitions(self.parent_stack)
        stk_resource = MyImplementedStackResource(
            'test',
            resource_defns[self.ws_resname],
            self.parent_stack)
        stk_resource.child_params = mock.Mock(return_value={})
        stk_resource.child_template = mock.Mock(
            return_value=templatem.Template(self.simple_template,
                                            stk_resource.child_params))
        stk_resource._validate_nested_resources = mock.Mock()
        nest_stack = stk_resource._parse_nested_stack(
            "test_nest_stack", stk_resource.child_template(),
            stk_resource.child_params())
        self.assertEqual(self.parent_stack,
                         nest_stack.parent_resource._stack())

    def test_preview_dict_validates_nested_resources(self):
        parent_t = self.parent_stack.t
        resource_defns = parent_t.resource_definitions(self.parent_stack)
        stk_resource = MyImplementedStackResource(
            'test',
            resource_defns[self.ws_resname],
            self.parent_stack)
        stk_resource.child_params = mock.Mock(return_value={})
        stk_resource.child_template = mock.Mock(
            return_value=self.simple_template)
        exc = exception.RequestLimitExceeded(message='Validation Failed')
        validation_mock = mock.Mock(side_effect=exc)
        stk_resource._validate_nested_resources = validation_mock

        self.assertRaises(exception.RequestLimitExceeded,
                          stk_resource.preview)

    @mock.patch.object(stack_resource.parser, 'Stack')
    def test_preview_doesnt_validate_nested_stack(self, mock_stack_class):
        nested_stack = mock.Mock()
        mock_stack_class.return_value = nested_stack

        tmpl = self.parent_stack.t.t
        self.parent_resource.child_template = mock.Mock(return_value=tmpl)
        self.parent_resource.child_params = mock.Mock(return_value={})
        self.parent_resource.preview()

        self.assertFalse(nested_stack.validate.called)
        self.assertTrue(nested_stack.preview_resources.called)

    def test_validate_error_reference(self):
        stack_name = 'validate_error_reference'
        tmpl = template_format.parse(main_template)
        files = {'file://tmp/nested.yaml': my_wrong_nested_template}
        stack = parser.Stack(utils.dummy_context(), stack_name,
                             templatem.Template(tmpl, files=files))
        rsrc = stack['volume_server']
        raise_exc_msg = ('InvalidTemplateReference: '
                         'resources.volume_server<file://tmp/nested.yaml>: '
                         'The specified reference "instance" (in '
                         'volume_attachment.Properties.instance_uuid) is '
                         'incorrect.')
        exc = self.assertRaises(exception.StackValidationFailed,
                                rsrc.validate)
        self.assertEqual(raise_exc_msg, six.text_type(exc))

    def _test_validate_unknown_resource_type(self, stack_name, tmpl,
                                             resource_name):
        raise_exc_msg = 'The Resource Type (idontexist) could not be found.'
        stack = parser.Stack(utils.dummy_context(), stack_name, tmpl)
        rsrc = stack[resource_name]

        exc = self.assertRaises(exception.StackValidationFailed,
                                rsrc.validate)
        self.assertIn(raise_exc_msg, six.text_type(exc))

    def test_validate_resource_group(self):
        # test validate without nested template
        stack_name = 'validate_resource_group_template'
        t = template_format.parse(resource_group_template)
        tmpl = templatem.Template(t)
        self._test_validate_unknown_resource_type(stack_name, tmpl,
                                                  'my_resource_group')

        # validate with nested template
        res_prop = t['resources']['my_resource_group']['properties']
        res_prop['resource_def']['type'] = 'nova_server.yaml'
        files = {'nova_server.yaml': nova_server_template}
        tmpl = templatem.Template(t, files=files)
        self._test_validate_unknown_resource_type(stack_name, tmpl,
                                                  'my_resource_group')

    def test_validate_heat_autoscaling_group(self):
        # test validate without nested template
        stack_name = 'validate_heat_autoscaling_group_template'
        t = template_format.parse(heat_autoscaling_group_template)
        tmpl = templatem.Template(t)
        self._test_validate_unknown_resource_type(stack_name, tmpl,
                                                  'my_autoscaling_group')

        # validate with nested template
        res_prop = t['resources']['my_autoscaling_group']['properties']
        res_prop['resource']['type'] = 'nova_server.yaml'
        files = {'nova_server.yaml': nova_server_template}
        tmpl = templatem.Template(t, files=files)
        self._test_validate_unknown_resource_type(stack_name, tmpl,
                                                  'my_autoscaling_group')

    def test_get_attribute_autoscaling(self):
        t = template_format.parse(heat_autoscaling_group_template)
        tmpl = templatem.Template(t)
        stack = parser.Stack(utils.dummy_context(), 'test_att', tmpl)
        rsrc = stack['my_autoscaling_group']
        self.assertEqual(0, rsrc.FnGetAtt(rsrc.CURRENT_SIZE))

    def test_get_attribute_autoscaling_convg(self):
        t = template_format.parse(heat_autoscaling_group_template)
        tmpl = templatem.Template(t)
        cache_data = {'my_autoscaling_group': node_data.NodeData.from_dict({
            'uuid': mock.ANY,
            'id': mock.ANY,
            'action': 'CREATE',
            'status': 'COMPLETE',
            'attrs': {'current_size': 4}
        })}
        stack = parser.Stack(utils.dummy_context(), 'test_att', tmpl,
                             cache_data=cache_data)
        rsrc = stack.defn['my_autoscaling_group']
        self.assertEqual(4, rsrc.FnGetAtt('current_size'))

    def test__validate_nested_resources_checks_num_of_resources(self):
        stack_resource.cfg.CONF.set_override('max_resources_per_stack', 2)
        tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
                'Resources': {
                    'r': {
                        'Type': 'OS::Heat::None'
                    }}}
        template = stack_resource.template.Template(tmpl)
        root_resources = mock.Mock(return_value=2)
        self.parent_resource.stack.total_resources = root_resources

        self.assertRaises(exception.RequestLimitExceeded,
                          self.parent_resource._validate_nested_resources,
                          template)

    def test_load_nested_ok(self):
        self.parent_resource._nested = None
        self.parent_resource.resource_id = 319
        self.m.StubOutWithMock(parser.Stack, 'load')
        parser.Stack.load(self.parent_resource.context,
                          self.parent_resource.resource_id).AndReturn('s')
        self.m.ReplayAll()
        self.parent_resource.nested()
        self.m.VerifyAll()

    def test_load_nested_non_exist(self):
        self.parent_resource._nested = None
        self.parent_resource.resource_id = '90-8'
        self.m.StubOutWithMock(parser.Stack, 'load')
        parser.Stack.load(self.parent_resource.context,
                          self.parent_resource.resource_id).AndRaise(
            exception.NotFound)
        self.m.ReplayAll()

        self.assertIsNone(self.parent_resource.nested())
        self.m.VerifyAll()

    def test_load_nested_cached(self):
        self.parent_resource._nested = 'gotthis'
        self.assertEqual('gotthis', self.parent_resource.nested())

    def test_delete_nested_none_nested_stack(self):
        self.parent_resource._nested = None
        self.assertIsNone(self.parent_resource.delete_nested())

    def test_delete_nested_not_found_nested_stack(self):
        self.parent_resource.resource_id = 'fake_id'
        rpcc = mock.MagicMock()
        self.parent_resource.rpc_client = rpcc

        @contextlib.contextmanager
        def exc_filter(*args):
            try:
                yield
            except exception.NotFound:
                pass

        rpcc.return_value.ignore_error_by_name.side_effect = exc_filter
        rpcc.return_value.delete_stack = mock.Mock(
            side_effect=exception.NotFound())
        self.assertIsNone(self.parent_resource.delete_nested())
        rpcc.return_value.delete_stack.assert_called_once_with(
            self.parent_resource.context, mock.ANY, cast=False)

    def test_need_update_for_nested_resource(self):
        """Test the resource with nested stack should need update.

        The resource in Create or Update state and has nested stack, should
        need update.
        """
        self.parent_resource.action = self.parent_resource.CREATE

        self.parent_resource._rpc_client = mock.MagicMock()
        self.parent_resource._rpc_client.show_stack.return_value = [
            {'stack_action': self.parent_resource.CREATE,
             'stack_status': self.parent_resource.COMPLETE}]

        need_update = self.parent_resource._needs_update(
            self.parent_resource.t,
            self.parent_resource.t,
            self.parent_resource.properties,
            self.parent_resource.properties,
            self.parent_resource)

        self.assertTrue(need_update)

    def test_need_update_in_failed_state_for_nested_resource(self):
        """Test the resource with no nested stack should need replacement.

        The resource in failed state and has no nested stack,
        should need update with UpdateReplace.
        """
        self.parent_resource.state_set(self.parent_resource.INIT,
                                       self.parent_resource.FAILED)
        self.parent_resource._nested = None
        self.assertRaises(resource.UpdateReplace,
                          self.parent_resource._needs_update,
                          self.parent_resource.t,
                          self.parent_resource.t,
                          self.parent_resource.properties,
                          self.parent_resource.properties,
                          self.parent_resource)

    def test_need_update_in_init_complete_state_for_nested_resource(self):
        """Test the resource with no nested stack should need replacement.

        The resource in failed state and has no nested stack,
        should need update with UpdateReplace.
        """
        self.parent_resource.state_set(self.parent_resource.INIT,
                                       self.parent_resource.COMPLETE)
        self.parent_resource._nested = None
        self.assertRaises(resource.UpdateReplace,
                          self.parent_resource._needs_update,
                          self.parent_resource.t,
                          self.parent_resource.t,
                          self.parent_resource.properties,
                          self.parent_resource.properties,
                          self.parent_resource)

    def test_need_update_in_check_failed_state_after_stack_check(self):
        self.parent_resource.resource_id = 'fake_id'
        self.parent_resource.state_set(self.parent_resource.CHECK,
                                       self.parent_resource.FAILED)
        self.nested = mock.MagicMock()
        self.nested.name = 'nested-stack'
        self.parent_resource.nested = mock.MagicMock(return_value=self.nested)
        self.parent_resource._nested = self.nested

        self.parent_resource._rpc_client = mock.MagicMock()
        self.parent_resource._rpc_client.show_stack.return_value = [
            {'stack_action': self.parent_resource.CHECK,
             'stack_status': self.parent_resource.FAILED}]

        self.assertTrue(
            self.parent_resource._needs_update(self.parent_resource.t,
                                               self.parent_resource.t,
                                               self.parent_resource.properties,
                                               self.parent_resource.properties,
                                               self.parent_resource))

    def test_need_update_check_failed_state_after_mark_unhealthy(self):
        self.parent_resource.resource_id = 'fake_id'
        self.parent_resource.state_set(self.parent_resource.CHECK,
                                       self.parent_resource.FAILED)
        self.nested = mock.MagicMock()
        self.nested.name = 'nested-stack'
        self.parent_resource.nested = mock.MagicMock(return_value=self.nested)
        self.parent_resource._nested = self.nested

        self.parent_resource._rpc_client = mock.MagicMock()
        self.parent_resource._rpc_client.show_stack.return_value = [
            {'stack_action': self.parent_resource.CREATE,
             'stack_status': self.parent_resource.COMPLETE}]

        self.assertRaises(resource.UpdateReplace,
                          self.parent_resource._needs_update,
                          self.parent_resource.t,
                          self.parent_resource.t,
                          self.parent_resource.properties,
                          self.parent_resource.properties,
                          self.parent_resource)


class StackResourceLimitTest(StackResourceBaseTest):
    scenarios = [
        ('3_4_0', dict(root=3, templ=4, nested=0, max=10, error=False)),
        ('3_8_0', dict(root=3, templ=8, nested=0, max=10, error=True)),
        ('3_8_2', dict(root=3, templ=8, nested=2, max=10, error=True)),
        ('3_5_2', dict(root=3, templ=5, nested=2, max=10, error=False)),
        ('3_6_2', dict(root=3, templ=6, nested=2, max=10, error=True)),
        ('3_12_2', dict(root=3, templ=12, nested=2, max=10, error=True))]

    def setUp(self):
        super(StackResourceLimitTest, self).setUp()
        self.res = self.parent_resource

    def test_resource_limit(self):
        # mock root total_resources
        total_resources = self.root + self.nested
        parser.Stack.total_resources = mock.Mock(return_value=total_resources)

        # setup the config max
        cfg.CONF.set_default('max_resources_per_stack', self.max)

        # fake the template
        templ = mock.MagicMock()
        templ.__getitem__.return_value = range(self.templ)
        templ.RESOURCES = 'Resources'
        if self.error:
            self.assertRaises(exception.RequestLimitExceeded,
                              self.res._validate_nested_resources, templ)
        else:
            self.assertIsNone(self.res._validate_nested_resources(templ))


class StackResourceAttrTest(StackResourceBaseTest):
    def test_get_output_ok(self):
        self.parent_resource.nested_identifier = mock.Mock()
        self.parent_resource.nested_identifier.return_value = {'foo': 'bar'}

        self.parent_resource._rpc_client = mock.MagicMock()
        output = {'outputs': [{'output_key': 'key', 'output_value': 'value'}]}
        self.parent_resource._rpc_client.show_stack.return_value = [output]

        self.assertEqual("value", self.parent_resource.get_output("key"))

    def test_get_output_key_not_found(self):
        self.parent_resource.nested_identifier = mock.Mock()
        self.parent_resource.nested_identifier.return_value = {'foo': 'bar'}

        self.parent_resource._rpc_client = mock.MagicMock()
        output = {'outputs': []}
        self.parent_resource._rpc_client.show_stack.return_value = [output]

        self.assertRaises(exception.NotFound,
                          self.parent_resource.get_output,
                          "key")

    def test_get_output_key_no_outputs_from_rpc(self):
        self.parent_resource.nested_identifier = mock.Mock()
        self.parent_resource.nested_identifier.return_value = {'foo': 'bar'}

        self.parent_resource._rpc_client = mock.MagicMock()
        output = {}
        self.parent_resource._rpc_client.show_stack.return_value = [output]

        self.assertRaises(exception.NotFound,
                          self.parent_resource.get_output,
                          "key")

    def test_resolve_attribute_string(self):
        self.parent_resource.nested_identifier = mock.Mock()
        self.parent_resource.nested_identifier.return_value = {'foo': 'bar'}

        self.parent_resource._rpc_client = mock.MagicMock()
        output = {'outputs': [{'output_key': 'key', 'output_value': 'value'}]}
        self.parent_resource._rpc_client.show_stack.return_value = [output]

        self.assertEqual('value',
                         self.parent_resource._resolve_attribute("key"))

    def test_resolve_attribute_dict(self):
        self.parent_resource.nested_identifier = mock.Mock()
        self.parent_resource.nested_identifier.return_value = {'foo': 'bar'}

        self.parent_resource._rpc_client = mock.MagicMock()
        output = {'outputs': [{'output_key': 'key',
                               'output_value': {'a': 1, 'b': 2}}]}
        self.parent_resource._rpc_client.show_stack.return_value = [output]

        self.assertEqual({'a': 1, 'b': 2},
                         self.parent_resource._resolve_attribute("key"))

    def test_resolve_attribute_list(self):
        self.parent_resource.nested_identifier = mock.Mock()
        self.parent_resource.nested_identifier.return_value = {'foo': 'bar'}

        self.parent_resource._rpc_client = mock.MagicMock()
        output = {'outputs': [{'output_key': 'key',
                               'output_value': [1, 2, 3]}]}
        self.parent_resource._rpc_client.show_stack.return_value = [output]

        self.assertEqual([1, 2, 3],
                         self.parent_resource._resolve_attribute("key"))

    def test_validate_nested_stack(self):
        self.parent_resource.child_template = mock.Mock(return_value='foo')
        self.parent_resource.child_params = mock.Mock(return_value={})
        nested = self.m.CreateMockAnything()
        nested.validate().AndReturn(True)
        self.m.StubOutWithMock(stack_resource.StackResource,
                               '_parse_nested_stack')
        name = '%s-%s' % (self.parent_stack.name, self.parent_resource.name)
        stack_resource.StackResource._parse_nested_stack(
            name, 'foo', {}).AndReturn(nested)

        self.m.ReplayAll()
        self.parent_resource.validate_nested_stack()
        self.assertFalse(nested.strict_validate)
        self.m.VerifyAll()

    def test_validate_assertion_exception_rethrow(self):
        expected_message = 'Expected Assertion Error'
        self.parent_resource.child_template = mock.Mock(return_value='foo')
        self.parent_resource.child_params = mock.Mock(return_value={})
        self.m.StubOutWithMock(stack_resource.StackResource,
                               '_parse_nested_stack')
        name = '%s-%s' % (self.parent_stack.name, self.parent_resource.name)
        stack_resource.StackResource._parse_nested_stack(
            name, 'foo', {}).AndRaise(AssertionError(expected_message))

        self.m.ReplayAll()
        exc = self.assertRaises(AssertionError,
                                self.parent_resource.validate_nested_stack)
        self.assertEqual(expected_message, six.text_type(exc))
        self.m.VerifyAll()


class StackResourceCheckCompleteTest(StackResourceBaseTest):
    scenarios = [
        ('create', dict(action='create')),
        ('update', dict(action='update')),
        ('suspend', dict(action='suspend')),
        ('resume', dict(action='resume')),
        ('delete', dict(action='delete')),
    ]

    def setUp(self):
        super(StackResourceCheckCompleteTest, self).setUp()
        self.status = [self.action.upper(), None, None, None]
        self.mock_status = self.patchobject(stack_object.Stack, 'get_status')
        self.mock_status.return_value = self.status

    def test_state_ok(self):
        """Test case when check_create_complete should return True.

        check_create_complete should return True create task is
        done and the nested stack is in (<action>,COMPLETE) state.
        """
        self.mock_lock = self.patchobject(stack_lock.StackLock,
                                          'get_engine_id')
        self.mock_lock.return_value = None
        self.status[1] = 'COMPLETE'
        complete = getattr(self.parent_resource,
                           'check_%s_complete' % self.action)
        self.assertIs(True, complete(None))
        self.mock_status.assert_called_once_with(
            self.parent_resource.context, self.parent_resource.resource_id)
        self.mock_lock.assert_called_once_with(
            self.parent_resource.context,
            self.parent_resource.resource_id)

    def test_state_err(self):
        """Test case when check_create_complete should raise error.

        check_create_complete should raise error when create task is
        done but the nested stack is not in (<action>,COMPLETE) state
        """
        self.status[1] = 'FAILED'
        reason = ('Resource %s failed: ValueError: '
                  'resources.%s: broken on purpose' % (
                      self.action.upper(),
                      'child_res'))
        exp_path = 'resources.test.resources.child_res'
        exp = 'ValueError: %s: broken on purpose' % exp_path
        self.status[2] = reason
        complete = getattr(self.parent_resource,
                           'check_%s_complete' % self.action)
        exc = self.assertRaises(exception.ResourceFailure, complete, None)
        self.assertEqual(exp, six.text_type(exc))
        self.mock_status.assert_called_once_with(
            self.parent_resource.context, self.parent_resource.resource_id)

    def test_state_unknown(self):
        """Test case when check_create_complete should raise error.

        check_create_complete should raise error when create task is
        done but the nested stack is not in (<action>,COMPLETE) state
        """
        self.status[1] = 'WTF'
        self.status[2] = 'broken on purpose'
        complete = getattr(self.parent_resource,
                           'check_%s_complete' % self.action)
        self.assertRaises(exception.ResourceUnknownStatus, complete, None)
        self.mock_status.assert_called_once_with(
            self.parent_resource.context, self.parent_resource.resource_id)

    def test_in_progress(self):
        self.status[1] = 'IN_PROGRESS'
        complete = getattr(self.parent_resource,
                           'check_%s_complete' % self.action)
        self.assertFalse(complete(None))
        self.mock_status.assert_called_once_with(
            self.parent_resource.context, self.parent_resource.resource_id)

    def test_update_not_started(self):
        if self.action != 'update':
            # only valid for updates at the moment.
            return

        self.status[1] = 'COMPLETE'
        self.status[3] = 'test'
        cookie = {'previous': {'state': ('UPDATE', 'COMPLETE'),
                               'updated_at': 'test'}}

        complete = getattr(self.parent_resource,
                           'check_%s_complete' % self.action)

        self.assertFalse(complete(cookie=cookie))
        self.mock_status.assert_called_once_with(
            self.parent_resource.context, self.parent_resource.resource_id)

    def test_wrong_action(self):
        self.status[0] = 'COMPLETE'
        complete = getattr(self.parent_resource,
                           'check_%s_complete' % self.action)
        self.assertFalse(complete(None))
        self.mock_status.assert_called_once_with(
            self.parent_resource.context, self.parent_resource.resource_id)


class WithTemplateTest(StackResourceBaseTest):

    scenarios = [
        ('basic', dict(params={}, timeout_mins=None, adopt_data=None)),
        ('params', dict(params={'foo': 'fee'},
                        timeout_mins=None, adopt_data=None)),
        ('timeout', dict(params={}, timeout_mins=53, adopt_data=None)),
        ('adopt', dict(params={}, timeout_mins=None,
                       adopt_data={'template': 'foo', 'environment': 'eee'})),
    ]

    class IntegerMatch(object):
        def __eq__(self, other):
            if getattr(self, 'match', None) is not None:
                return other == self.match
            if not isinstance(other, six.integer_types):
                return False

            self.match = other
            return True

        def __ne__(self, other):
            return not self.__eq__(other)

    def test_create_with_template(self):
        child_env = {'parameter_defaults': {},
                     'event_sinks': [],
                     'parameters': self.params,
                     'resource_registry': {'resources': {}}}
        self.parent_resource.child_params = mock.Mock(
            return_value=self.params)
        res_name = self.parent_resource.physical_resource_name()
        rpcc = mock.Mock()
        self.parent_resource.rpc_client = rpcc
        rpcc.return_value._create_stack.return_value = {'stack_id': 'pancakes'}
        self.parent_resource.create_with_template(
            self.empty_temp, user_params=self.params,
            timeout_mins=self.timeout_mins, adopt_data=self.adopt_data)
        if self.adopt_data is not None:
            adopt_data_str = json.dumps(self.adopt_data)
            tmpl_args = {
                'template': self.empty_temp.t,
                'params': child_env,
                'files': {},
            }
        else:
            adopt_data_str = None
            tmpl_args = {
                'template_id': self.IntegerMatch(),
                'template': None,
                'params': None,
                'files': None,
            }
        rpcc.return_value._create_stack.assert_called_once_with(
            self.ctx,
            stack_name=res_name,
            args={rpc_api.PARAM_DISABLE_ROLLBACK: True,
                  rpc_api.PARAM_ADOPT_STACK_DATA: adopt_data_str,
                  rpc_api.PARAM_TIMEOUT: self.timeout_mins},
            environment_files=None,
            stack_user_project_id='aprojectid',
            parent_resource_name='test',
            user_creds_id='uc123',
            owner_id=self.parent_stack.id,
            nested_depth=1,
            **tmpl_args)

    def test_create_with_template_failure(self):
        class StackValidationFailed_Remote(exception.StackValidationFailed):
            pass

        child_env = {'parameter_defaults': {},
                     'event_sinks': [],
                     'parameters': self.params,
                     'resource_registry': {'resources': {}}}
        self.parent_resource.child_params = mock.Mock(
            return_value=self.params)
        res_name = self.parent_resource.physical_resource_name()
        rpcc = mock.Mock()
        self.parent_resource.rpc_client = rpcc
        remote_exc = StackValidationFailed_Remote(message='oops')
        rpcc.return_value._create_stack.side_effect = remote_exc
        self.assertRaises(exception.ResourceFailure,
                          self.parent_resource.create_with_template,
                          self.empty_temp, user_params=self.params,
                          timeout_mins=self.timeout_mins,
                          adopt_data=self.adopt_data)
        if self.adopt_data is not None:
            adopt_data_str = json.dumps(self.adopt_data)
            tmpl_args = {
                'template': self.empty_temp.t,
                'params': child_env,
                'files': {},
            }
        else:
            adopt_data_str = None
            tmpl_args = {
                'template_id': self.IntegerMatch(),
                'template': None,
                'params': None,
                'files': None,
            }
        rpcc.return_value._create_stack.assert_called_once_with(
            self.ctx,
            stack_name=res_name,
            args={rpc_api.PARAM_DISABLE_ROLLBACK: True,
                  rpc_api.PARAM_ADOPT_STACK_DATA: adopt_data_str,
                  rpc_api.PARAM_TIMEOUT: self.timeout_mins},
            environment_files=None,
            stack_user_project_id='aprojectid',
            parent_resource_name='test',
            user_creds_id='uc123',
            owner_id=self.parent_stack.id,
            nested_depth=1,
            **tmpl_args)
        if self.adopt_data is None:
            stored_tmpl_id = tmpl_args['template_id'].match
            self.assertIsNotNone(stored_tmpl_id)
            self.assertRaises(exception.NotFound,
                              raw_template.RawTemplate.get_by_id,
                              self.ctx, stored_tmpl_id)

    def test_update_with_template(self):
        if self.adopt_data is not None:
            return
        ident = identifier.HeatIdentifier(self.ctx.tenant_id, 'fake_name',
                                          'pancakes')
        self.parent_resource.resource_id = ident.stack_id
        self.parent_resource.nested_identifier = mock.Mock(return_value=ident)

        self.parent_resource.child_params = mock.Mock(
            return_value=self.params)
        rpcc = mock.Mock()
        self.parent_resource.rpc_client = rpcc
        rpcc.return_value._update_stack.return_value = dict(ident)

        status = ('CREATE', 'COMPLETE', '', 'now_time')
        with self.patchobject(stack_object.Stack, 'get_status',
                              return_value=status):
            self.parent_resource.update_with_template(
                self.empty_temp, user_params=self.params,
                timeout_mins=self.timeout_mins)

        rpcc.return_value._update_stack.assert_called_once_with(
            self.ctx,
            stack_identity=dict(ident),
            template_id=self.IntegerMatch(),
            template=None,
            params=None,
            files=None,
            args={rpc_api.PARAM_TIMEOUT: self.timeout_mins,
                  rpc_api.PARAM_CONVERGE: False})

    def test_update_with_template_failure(self):
        class StackValidationFailed_Remote(exception.StackValidationFailed):
            pass

        if self.adopt_data is not None:
            return
        ident = identifier.HeatIdentifier(self.ctx.tenant_id, 'fake_name',
                                          'pancakes')
        self.parent_resource.resource_id = ident.stack_id
        self.parent_resource.nested_identifier = mock.Mock(return_value=ident)

        self.parent_resource.child_params = mock.Mock(
            return_value=self.params)
        rpcc = mock.Mock()
        self.parent_resource.rpc_client = rpcc
        remote_exc = StackValidationFailed_Remote(message='oops')
        rpcc.return_value._update_stack.side_effect = remote_exc

        status = ('CREATE', 'COMPLETE', '', 'now_time')
        with self.patchobject(stack_object.Stack, 'get_status',
                              return_value=status):
            self.assertRaises(exception.ResourceFailure,
                              self.parent_resource.update_with_template,
                              self.empty_temp, user_params=self.params,
                              timeout_mins=self.timeout_mins)

        template_id = self.IntegerMatch()
        rpcc.return_value._update_stack.assert_called_once_with(
            self.ctx,
            stack_identity=dict(ident),
            template_id=template_id,
            template=None,
            params=None,
            files=None,
            args={rpc_api.PARAM_TIMEOUT: self.timeout_mins,
                  rpc_api.PARAM_CONVERGE: False})
        self.assertIsNotNone(template_id.match)
        self.assertRaises(exception.NotFound,
                          raw_template.RawTemplate.get_by_id,
                          self.ctx, template_id.match)


class RaiseLocalException(StackResourceBaseTest):

    def test_heat_exception(self):
        local = exception.StackValidationFailed(message='test')
        self.assertRaises(exception.StackValidationFailed,
                          self.parent_resource.translate_remote_exceptions,
                          local)

    def test_messaging_timeout(self):
        local = msg_exceptions.MessagingTimeout('took too long')
        self.assertRaises(msg_exceptions.MessagingTimeout,
                          self.parent_resource.translate_remote_exceptions,
                          local)

    def test_remote_heat_ex(self):
        class StackValidationFailed_Remote(exception.StackValidationFailed):
            pass

        local = StackValidationFailed_Remote(message='test')
        self.assertRaises(exception.ResourceFailure,
                          self.parent_resource.translate_remote_exceptions,
                          local)
