#    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 copy
import json

from heatclient import exc
import yaml

from heat_integrationtests.functional import functional_base


class ResourceGroupTest(functional_base.FunctionalTestsBase):
    template = '''
heat_template_version: 2013-05-23
resources:
  random_group:
    type: OS::Heat::ResourceGroup
    properties:
      count: 0
      resource_def:
        type: My::RandomString
        properties:
          length: 30
          salt: initial
outputs:
  random1:
    value: {get_attr: [random_group, resource.0.value]}
  random2:
    value: {get_attr: [random_group, resource.1.value]}
  all_values:
    value: {get_attr: [random_group, value]}
'''

    def test_resource_group_zero_novalidate(self):
        # Nested resources should be validated only when size > 0
        # This allows features to be disabled via size=0 without
        # triggering validation of nested resource custom constraints
        # e.g images etc in the nested schema.
        nested_template_fail = '''
heat_template_version: 2013-05-23
parameters:
  length:
    type: string
    default: 50
  salt:
    type: string
    default: initial
resources:
  random:
    type: OS::Heat::RandomString
    properties:
      length: BAD
'''

        files = {'provider.yaml': nested_template_fail}
        env = {'resource_registry':
               {'My::RandomString': 'provider.yaml'}}
        stack_identifier = self.stack_create(
            template=self.template,
            files=files,
            environment=env
        )

        self.assertEqual({'random_group': 'OS::Heat::ResourceGroup'},
                         self.list_resources(stack_identifier))

        # Check we created an empty nested stack
        nested_identifier = self.group_nested_identifier(stack_identifier,
                                                         'random_group')
        self.assertEqual({}, self.list_resources(nested_identifier))

        # Prove validation works for non-zero create/update
        template_two_nested = self.template.replace("count: 0", "count: 2")
        expected_err = ("resources.random_group<nested_stack>.resources."
                        "0<provider.yaml>.resources.random: : "
                        "Value 'BAD' is not an integer")
        ex = self.assertRaises(exc.HTTPBadRequest, self.update_stack,
                               stack_identifier, template_two_nested,
                               environment=env, files=files)
        self.assertIn(expected_err, str(ex))

        ex = self.assertRaises(exc.HTTPBadRequest, self.stack_create,
                               template=template_two_nested,
                               environment=env, files=files)
        self.assertIn(expected_err, str(ex))

    def _validate_resources(self, stack_identifier, expected_count):
        resources = self.list_group_resources(stack_identifier,
                                              'random_group')
        self.assertEqual(expected_count, len(resources))
        expected_resources = dict(
            (str(idx), 'My::RandomString')
            for idx in range(expected_count))

        self.assertEqual(expected_resources, resources)

    def test_create(self):
        def validate_output(stack, output_key, length):
            output_value = self._stack_output(stack, output_key)
            self.assertEqual(length, len(output_value))
            return output_value
        # verify that the resources in resource group are identically
        # configured, resource names and outputs are appropriate.
        env = {'resource_registry':
               {'My::RandomString': 'OS::Heat::RandomString'}}
        create_template = self.template.replace("count: 0", "count: 2")
        stack_identifier = self.stack_create(template=create_template,
                                             environment=env)
        self.assertEqual({'random_group': 'OS::Heat::ResourceGroup'},
                         self.list_resources(stack_identifier))

        # validate count, type and name of resources in a resource group.
        self._validate_resources(stack_identifier, 2)

        # validate outputs
        stack = self.client.stacks.get(stack_identifier)
        outputs = []
        outputs.append(validate_output(stack, 'random1', 30))
        outputs.append(validate_output(stack, 'random2', 30))
        self.assertEqual(outputs, self._stack_output(stack, 'all_values'))

    def test_create_nested_groups_with_timeout(self):
        parent_template = '''
heat_template_version: rocky
resources:
  parent_group:
    type: OS::Heat::ResourceGroup
    update_policy:
      batch_create: { max_batch_size: 1, pause_time: 1 }
    properties:
      count: 2
      resource_def:
        type: child.yaml
'''
        child_template = '''
heat_template_version: rocky
resources:
  child_group:
    type: OS::Heat::ResourceGroup
    update_policy:
      batch_create: { max_batch_size: 1, pause_time: 1 }
    properties:
      count: 2
      resource_def:
        type: value.yaml
'''
        value_template = '''
heat_template_version: rocky
resources:
  value:
    type: OS::Heat::Value
    properties:
      type: string
      value: 'test'
'''
        files = {
            'child.yaml': child_template,
            'value.yaml': value_template,
        }
        stack_identifier = self.stack_create(
            template=parent_template,
            files=files,
            timeout=10,  # in minutes
        )

        resources = self.client.resources.list(
            stack_identifier, nested_depth=2, with_detail=True)
        timeouts = set()
        for res in resources:
            if res.resource_type == "OS::Heat::ResourceGroup":
                nested_stack = self.client.stacks.get(res.physical_resource_id)
                timeouts.add(nested_stack.timeout_mins)

        self.assertEqual({10}, timeouts)

    def test_update_increase_decrease_count(self):
        # create stack with resource group count 2
        env = {'resource_registry':
               {'My::RandomString': 'OS::Heat::RandomString'}}
        create_template = self.template.replace("count: 0", "count: 2")
        stack_identifier = self.stack_create(template=create_template,
                                             environment=env)
        self.assertEqual({'random_group': 'OS::Heat::ResourceGroup'},
                         self.list_resources(stack_identifier))
        # verify that the resource group has 2 resources
        self._validate_resources(stack_identifier, 2)

        # increase the resource group count to 5
        update_template = self.template.replace("count: 0", "count: 5")
        self.update_stack(stack_identifier, update_template, environment=env)
        # verify that the resource group has 5 resources
        self._validate_resources(stack_identifier, 5)

        # decrease the resource group count to 3
        update_template = self.template.replace("count: 0", "count: 3")
        self.update_stack(stack_identifier, update_template, environment=env)
        # verify that the resource group has 3 resources
        self._validate_resources(stack_identifier, 3)

    def test_update_removal_policies(self):
        rp_template = '''
heat_template_version: 2014-10-16
resources:
  random_group:
    type: OS::Heat::ResourceGroup
    properties:
      count: 5
      removal_policies: []
      resource_def:
        type: OS::Heat::RandomString
'''

        # create stack with resource group, initial count 5
        stack_identifier = self.stack_create(template=rp_template)
        self.assertEqual({'random_group': 'OS::Heat::ResourceGroup'},
                         self.list_resources(stack_identifier))
        group_resources = self.list_group_resources(stack_identifier,
                                                    'random_group')
        expected_resources = {'0': 'OS::Heat::RandomString',
                              '1': 'OS::Heat::RandomString',
                              '2': 'OS::Heat::RandomString',
                              '3': 'OS::Heat::RandomString',
                              '4': 'OS::Heat::RandomString'}
        self.assertEqual(expected_resources, group_resources)

        # Remove three, specifying the middle resources to be removed
        update_template = rp_template.replace(
            'removal_policies: []',
            'removal_policies: [{resource_list: [\'1\', \'2\', \'3\']}]')
        self.update_stack(stack_identifier, update_template)
        group_resources = self.list_group_resources(stack_identifier,
                                                    'random_group')
        expected_resources = {'0': 'OS::Heat::RandomString',
                              '4': 'OS::Heat::RandomString',
                              '5': 'OS::Heat::RandomString',
                              '6': 'OS::Heat::RandomString',
                              '7': 'OS::Heat::RandomString'}
        self.assertEqual(expected_resources, group_resources)

    def test_props_update(self):
        """Test update of resource_def properties behaves as expected."""

        env = {'resource_registry':
               {'My::RandomString': 'OS::Heat::RandomString'}}
        template_one = self.template.replace("count: 0", "count: 1")
        stack_identifier = self.stack_create(template=template_one,
                                             environment=env)
        self.assertEqual({'random_group': 'OS::Heat::ResourceGroup'},
                         self.list_resources(stack_identifier))

        initial_nested_ident = self.group_nested_identifier(stack_identifier,
                                                            'random_group')
        self.assertEqual({'0': 'My::RandomString'},
                         self.list_resources(initial_nested_ident))
        # get the resource id
        res = self.client.resources.get(initial_nested_ident, '0')
        initial_res_id = res.physical_resource_id

        # change the salt (this should replace the RandomString but
        # not the nested stack or resource group.
        template_salt = template_one.replace("salt: initial", "salt: more")
        self.update_stack(stack_identifier, template_salt, environment=env)
        updated_nested_ident = self.group_nested_identifier(stack_identifier,
                                                            'random_group')
        self.assertEqual(initial_nested_ident, updated_nested_ident)

        # compare the resource id, we expect a change.
        res = self.client.resources.get(updated_nested_ident, '0')
        updated_res_id = res.physical_resource_id
        self.assertNotEqual(initial_res_id, updated_res_id)

    def test_update_nochange(self):
        """Test update with no properties change."""

        env = {'resource_registry':
               {'My::RandomString': 'OS::Heat::RandomString'}}
        template_one = self.template.replace("count: 0", "count: 2")
        stack_identifier = self.stack_create(template=template_one,
                                             environment=env)
        self.assertEqual({'random_group': 'OS::Heat::ResourceGroup'},
                         self.list_resources(stack_identifier))

        initial_nested_ident = self.group_nested_identifier(stack_identifier,
                                                            'random_group')
        self.assertEqual({'0': 'My::RandomString', '1': 'My::RandomString'},
                         self.list_resources(initial_nested_ident))
        # get the output
        stack0 = self.client.stacks.get(stack_identifier)
        initial_rand = self._stack_output(stack0, 'random1')

        template_copy = copy.deepcopy(template_one)
        self.update_stack(stack_identifier, template_copy, environment=env)
        updated_nested_ident = self.group_nested_identifier(stack_identifier,
                                                            'random_group')
        self.assertEqual(initial_nested_ident, updated_nested_ident)

        # compare the random number, we expect no change.
        stack1 = self.client.stacks.get(stack_identifier)
        updated_rand = self._stack_output(stack1, 'random1')
        self.assertEqual(initial_rand, updated_rand)

    def test_update_nochange_resource_needs_update(self):
        """Test update when the resource definition has changed.

        Test the scenario when the ResourceGroup update happens without
        any changed properties, this can happen if the definition of
        a contained provider resource changes (files map changes), then
        the group and underlying nested stack should end up updated.
        """

        random_templ1 = '''
heat_template_version: 2013-05-23
parameters:
  length:
    type: string
    default: not-used
  salt:
    type: string
    default: not-used
resources:
  random1:
    type: OS::Heat::RandomString
    properties:
      salt: initial
outputs:
  value:
    value: {get_attr: [random1, value]}
'''
        files1 = {'my_random.yaml': random_templ1}

        random_templ2 = random_templ1.replace('salt: initial',
                                              'salt: more')
        files2 = {'my_random.yaml': random_templ2}

        env = {'resource_registry':
               {'My::RandomString': 'my_random.yaml'}}

        template_one = self.template.replace("count: 0", "count: 2")
        stack_identifier = self.stack_create(template=template_one,
                                             environment=env,
                                             files=files1)
        self.assertEqual({'random_group': 'OS::Heat::ResourceGroup'},
                         self.list_resources(stack_identifier))
        self.assertEqual(files1, self.client.stacks.files(stack_identifier))

        initial_nested_ident = self.group_nested_identifier(stack_identifier,
                                                            'random_group')
        self.assertEqual({'0': 'My::RandomString', '1': 'My::RandomString'},
                         self.list_resources(initial_nested_ident))
        # get the output
        stack0 = self.client.stacks.get(stack_identifier)
        initial_rand = self._stack_output(stack0, 'random1')

        # change the environment so we use a different TemplateResource.
        # note "files2".
        self.update_stack(stack_identifier, template_one,
                          environment=env, files=files2)
        updated_nested_ident = self.group_nested_identifier(stack_identifier,
                                                            'random_group')
        self.assertEqual(initial_nested_ident, updated_nested_ident)
        self.assertEqual(files2, self.client.stacks.files(stack_identifier))

        # compare the output, we expect a change.
        stack1 = self.client.stacks.get(stack_identifier)
        updated_rand = self._stack_output(stack1, 'random1')
        self.assertNotEqual(initial_rand, updated_rand)

    def test_validation(self):
        resource_group = '''
heat_template_version: 2016-10-14

parameters:
  the_count:
    type: number

resources:

  the_group:
    type: OS::Heat::ResourceGroup
    properties:
      count: {get_param: the_count}
      resource_def:
        type: OS::Heat::RandomString
'''
        ret = self.client.stacks.validate(template=resource_group)
        expected = {'Description': 'No description',
                    'Environment': {'event_sinks': [],
                                    'parameter_defaults': {},
                                    'parameters': {},
                                    'resource_registry': {'resources': {}}},
                    'Parameters': {
                        'the_count': {'Description': '',
                                      'Label': 'the_count',
                                      'NoEcho': 'false',
                                      'Type': 'Number'}}}

        self.assertEqual(expected, ret)


class ResourceGroupTestNullParams(functional_base.FunctionalTestsBase):
    template = '''
heat_template_version: 2013-05-23
parameters:
  param:
    type: empty
resources:
  random_group:
    type: OS::Heat::ResourceGroup
    properties:
      count: 1
      resource_def:
        type: My::RandomString
        properties:
          param: {get_param: param}
outputs:
  val:
    value: {get_attr: [random_group, val]}
'''

    nested_template_file = '''
heat_template_version: 2013-05-23
parameters:
  param:
    type: empty
outputs:
  val:
    value: {get_param: param}
'''

    scenarios = [
        ('string_empty', dict(
            param='',
            p_type='string',
        )),
        ('boolean_false', dict(
            param=False,
            p_type='boolean',
        )),
        ('number_zero', dict(
            param=0,
            p_type='number',
        )),
        ('comma_delimited_list', dict(
            param=[],
            p_type='comma_delimited_list',
        )),
        ('json_empty', dict(
            param={},
            p_type='json',
        )),
    ]

    def test_create_pass_zero_parameter(self):
        templ = self.template.replace('type: empty',
                                      'type: %s' % self.p_type)
        n_t_f = self.nested_template_file.replace('type: empty',
                                                  'type: %s' % self.p_type)
        files = {'provider.yaml': n_t_f}
        env = {'resource_registry':
               {'My::RandomString': 'provider.yaml'}}
        stack_identifier = self.stack_create(
            template=templ,
            files=files,
            environment=env,
            parameters={'param': self.param}
        )
        stack = self.client.stacks.get(stack_identifier)
        self.assertEqual(self.param, self._stack_output(stack, 'val')[0])


class ResourceGroupAdoptTest(functional_base.FunctionalTestsBase):
    """Prove that we can do resource group adopt."""

    main_template = '''
heat_template_version: "2013-05-23"
resources:
  group1:
    type: OS::Heat::ResourceGroup
    properties:
      count: 2
      resource_def:
        type: OS::Heat::RandomString
outputs:
  test0:
    value: {get_attr: [group1, resource.0.value]}
  test1:
    value: {get_attr: [group1, resource.1.value]}
'''

    def _yaml_to_json(self, yaml_templ):
        return yaml.safe_load(yaml_templ)

    def test_adopt(self):
        data = {
            "resources": {
                "group1": {
                    "status": "COMPLETE",
                    "name": "group1",
                    "resource_data": {},
                    "metadata": {},
                    "resource_id": "test-group1-id",
                    "action": "CREATE",
                    "type": "OS::Heat::ResourceGroup",
                    "resources": {
                        "0": {
                            "status": "COMPLETE",
                            "name": "0",
                            "resource_data": {"value": "goopie"},
                            "resource_id": "ID-0",
                            "action": "CREATE",
                            "type": "OS::Heat::RandomString",
                            "metadata": {}
                        },
                        "1": {
                            "status": "COMPLETE",
                            "name": "1",
                            "resource_data": {"value": "different"},
                            "resource_id": "ID-1",
                            "action": "CREATE",
                            "type": "OS::Heat::RandomString",
                            "metadata": {}
                        }
                    }
                }
            },
            "environment": {"parameters": {}},
            "template": yaml.safe_load(self.main_template)
        }
        stack_identifier = self.stack_adopt(
            adopt_data=json.dumps(data))

        self.assert_resource_is_a_stack(stack_identifier, 'group1')
        stack = self.client.stacks.get(stack_identifier)
        self.assertEqual('goopie', self._stack_output(stack, 'test0'))
        self.assertEqual('different', self._stack_output(stack, 'test1'))


class ResourceGroupErrorResourceTest(functional_base.FunctionalTestsBase):
    template = '''
heat_template_version: "2013-05-23"
resources:
  group1:
    type: OS::Heat::ResourceGroup
    properties:
      count: 2
      resource_def:
        type: fail.yaml
'''
    nested_templ = '''
heat_template_version: "2013-05-23"
resources:
  oops:
    type: OS::Heat::TestResource
    properties:
      fail: true
      wait_secs: 2
'''

    def test_fail(self):
        stack_identifier = self.stack_create(
            template=self.template,
            files={'fail.yaml': self.nested_templ},
            expected_status='CREATE_FAILED',
            enable_cleanup=False)
        stack = self.client.stacks.get(stack_identifier)

        self.assertEqual('CREATE_FAILED', stack.stack_status)
        self.client.stacks.delete(stack_identifier)
        self._wait_for_stack_status(
            stack_identifier, 'DELETE_COMPLETE',
            success_on_not_found=True)


class ResourceGroupUpdatePolicyTest(functional_base.FunctionalTestsBase):

    template = '''
heat_template_version: '2015-04-30'
resources:
  random_group:
    type: OS::Heat::ResourceGroup
    update_policy:
      rolling_update:
        min_in_service: 1
        max_batch_size: 2
        pause_time: 1
    properties:
      count: 10
      resource_def:
        type: OS::Heat::TestResource
        properties:
          value: initial
          update_replace: False
'''

    def update_resource_group(self, update_template,
                              updated, created, deleted):
        stack_identifier = self.stack_create(template=self.template)
        group_resources = self.list_group_resources(stack_identifier,
                                                    'random_group',
                                                    minimal=False)

        init_names = [res.physical_resource_id for res in group_resources]

        self.update_stack(stack_identifier, update_template)
        group_resources = self.list_group_resources(stack_identifier,
                                                    'random_group',
                                                    minimal=False)

        updt_names = [res.physical_resource_id for res in group_resources]

        matched_names = set(updt_names) & set(init_names)

        self.assertEqual(updated, len(matched_names))

        self.assertEqual(created, len(set(updt_names) - set(init_names)))

        self.assertEqual(deleted, len(set(init_names) - set(updt_names)))

    def test_resource_group_update(self):
        """Test rolling update with no conflict.

        Simple rolling update with no conflict in batch size
        and minimum instances in service.
        """
        updt_template = yaml.safe_load(copy.deepcopy(self.template))
        grp = updt_template['resources']['random_group']
        policy = grp['update_policy']['rolling_update']
        policy['min_in_service'] = '1'
        policy['max_batch_size'] = '3'
        res_def = grp['properties']['resource_def']
        res_def['properties']['value'] = 'updated'

        self.update_resource_group(updt_template,
                                   updated=10,
                                   created=0,
                                   deleted=0)

    def test_resource_group_update_replace(self):
        """Test rolling update(replace)with no conflict.

        Simple rolling update replace with no conflict in batch size
        and minimum instances in service.
        """
        updt_template = yaml.safe_load(copy.deepcopy(self.template))
        grp = updt_template['resources']['random_group']
        policy = grp['update_policy']['rolling_update']
        policy['min_in_service'] = '1'
        policy['max_batch_size'] = '3'
        res_def = grp['properties']['resource_def']
        res_def['properties']['value'] = 'updated'
        res_def['properties']['update_replace'] = True

        self.update_resource_group(updt_template,
                                   updated=0,
                                   created=10,
                                   deleted=10)

    def test_resource_group_update_replace_template_changed(self):
        """Test rolling update(replace)with child template path changed.

        Simple rolling update replace with child template path changed.
        """

        nested_templ = '''
heat_template_version: "2013-05-23"
resources:
  oops:
    type: OS::Heat::TestResource
'''

        create_template = yaml.safe_load(copy.deepcopy(self.template))
        grp = create_template['resources']['random_group']
        grp['properties']['resource_def'] = {'type': '/opt/provider.yaml'}
        files = {'/opt/provider.yaml': nested_templ}

        policy = grp['update_policy']['rolling_update']
        policy['min_in_service'] = '1'
        policy['max_batch_size'] = '3'
        stack_identifier = self.stack_create(template=create_template,
                                             files=files)
        update_template = create_template.copy()
        grp = update_template['resources']['random_group']
        grp['properties']['resource_def'] = {'type': '/opt1/provider.yaml'}
        files = {'/opt1/provider.yaml': nested_templ}

        self.update_stack(stack_identifier, update_template, files=files)

    def test_resource_group_update_scaledown(self):
        """Test rolling update with scaledown.

        Simple rolling update with reduced size.
        """
        updt_template = yaml.safe_load(copy.deepcopy(self.template))
        grp = updt_template['resources']['random_group']
        policy = grp['update_policy']['rolling_update']
        policy['min_in_service'] = '1'
        policy['max_batch_size'] = '3'
        grp['properties']['count'] = 6
        res_def = grp['properties']['resource_def']
        res_def['properties']['value'] = 'updated'

        self.update_resource_group(updt_template,
                                   updated=6,
                                   created=0,
                                   deleted=4)

    def test_resource_group_update_scaleup(self):
        """Test rolling update with scaleup.

        Simple rolling update with increased size.
        """
        updt_template = yaml.safe_load(copy.deepcopy(self.template))
        grp = updt_template['resources']['random_group']
        policy = grp['update_policy']['rolling_update']
        policy['min_in_service'] = '1'
        policy['max_batch_size'] = '3'
        grp['properties']['count'] = 12
        res_def = grp['properties']['resource_def']
        res_def['properties']['value'] = 'updated'

        self.update_resource_group(updt_template,
                                   updated=10,
                                   created=2,
                                   deleted=0)

    def test_resource_group_update_adjusted(self):
        """Test rolling update with enough available resources

        Update  with capacity adjustment with enough resources.
        """
        updt_template = yaml.safe_load(copy.deepcopy(self.template))
        grp = updt_template['resources']['random_group']
        policy = grp['update_policy']['rolling_update']
        policy['min_in_service'] = '8'
        policy['max_batch_size'] = '4'
        grp['properties']['count'] = 6
        res_def = grp['properties']['resource_def']
        res_def['properties']['value'] = 'updated'

        self.update_resource_group(updt_template,
                                   updated=6,
                                   created=0,
                                   deleted=4)

    def test_resource_group_update_with_adjusted_capacity(self):
        """Test rolling update with capacity adjustment.

        Rolling update with capacity adjustment due to conflict in
        batch size and minimum instances in service.
        """
        updt_template = yaml.safe_load(copy.deepcopy(self.template))
        grp = updt_template['resources']['random_group']
        policy = grp['update_policy']['rolling_update']
        policy['min_in_service'] = '8'
        policy['max_batch_size'] = '4'
        res_def = grp['properties']['resource_def']
        res_def['properties']['value'] = 'updated'

        self.update_resource_group(updt_template,
                                   updated=10,
                                   created=0,
                                   deleted=0)

    def test_resource_group_update_huge_batch_size(self):
        """Test rolling update with huge batch size.

        Rolling Update with a huge batch size(more than
        current size).
        """
        updt_template = yaml.safe_load(copy.deepcopy(self.template))
        grp = updt_template['resources']['random_group']
        policy = grp['update_policy']['rolling_update']
        policy['min_in_service'] = '0'
        policy['max_batch_size'] = '20'
        res_def = grp['properties']['resource_def']
        res_def['properties']['value'] = 'updated'
        self.update_resource_group(updt_template,
                                   updated=10,
                                   created=0,
                                   deleted=0)

    def test_resource_group_update_huge_min_in_service(self):
        """Test rolling update with huge minimum capacity.

        Rolling Update with a huge number of minimum instances
        in service.
        """
        updt_template = yaml.safe_load(copy.deepcopy(self.template))
        grp = updt_template['resources']['random_group']
        policy = grp['update_policy']['rolling_update']
        policy['min_in_service'] = '20'
        policy['max_batch_size'] = '1'
        res_def = grp['properties']['resource_def']
        res_def['properties']['value'] = 'updated'

        self.update_resource_group(updt_template,
                                   updated=10,
                                   created=0,
                                   deleted=0)
