# Copyright 2015 IBM 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.

import copy
from unittest import mock

from oslo_config import cfg

from heat.common import exception
from heat.common import template_format
from heat.engine.clients.os import senlin
from heat.engine.resources.openstack.senlin import cluster as sc
from heat.engine import scheduler
from heat.engine import template
from heat.tests import common
from heat.tests import utils
from openstack import exceptions


cluster_stack_template = """
heat_template_version: 2016-04-08
description: Senlin Cluster Template
resources:
  senlin-cluster:
    type: OS::Senlin::Cluster
    properties:
      name: SenlinCluster
      profile: fake_profile
      policies:
        - policy: fake_policy
          enabled: true
      min_size: 0
      max_size: -1
      desired_capacity: 1
      timeout: 3600
      metadata:
        foo: bar
"""


class FakeCluster(object):
    def __init__(self, id='some_id', status='ACTIVE'):
        self.status = status
        self.status_reason = 'Unknown'
        self.id = id
        self.name = "SenlinCluster"
        self.metadata = {}
        self.nodes = ['node1']
        self.desired_capacity = 1
        self.metadata = {'foo': 'bar'}
        self.timeout = 3600
        self.max_size = -1
        self.min_size = 0
        self.location = 'actions/fake-action'
        self.profile_name = 'fake_profile'
        self.profile_id = 'fake_profile_id'

    def to_dict(self):
        return {
            'id': self.id,
            'status': self.status,
            'status_reason': self.status_reason,
            'name': self.name,
            'metadata': self.metadata,
            'timeout': self.timeout,
            'desired_capacity': self.desired_capacity,
            'max_size': self.max_size,
            'min_size': self.min_size,
            'nodes': self.nodes,
            'profile_name': self.profile_name,
            'profile_id': self.profile_id
        }


class SenlinClusterTest(common.HeatTestCase):
    def setUp(self):
        super(SenlinClusterTest, self).setUp()
        self.senlin_mock = mock.MagicMock()
        self.senlin_mock.get_profile.return_value = mock.Mock(
            id='fake_profile_id'
        )
        self.patchobject(sc.Cluster, 'client', return_value=self.senlin_mock)
        self.patchobject(senlin.SenlinClientPlugin, 'client',
                         return_value=self.senlin_mock)
        self.patchobject(senlin.ProfileConstraint, 'validate',
                         return_value=True)
        self.patchobject(senlin.PolicyConstraint, 'validate',
                         return_value=True)
        self.fake_cl = FakeCluster()
        self.t = template_format.parse(cluster_stack_template)

    def _init_cluster(self, template):
        self.stack = utils.parse_stack(template)
        cluster = self.stack['senlin-cluster']
        return cluster

    def _create_cluster(self, template):
        cluster = self._init_cluster(template)
        self.senlin_mock.create_cluster.return_value = self.fake_cl
        self.senlin_mock.get_cluster.return_value = self.fake_cl
        self.senlin_mock.get_action.return_value = mock.Mock(
            status='SUCCEEDED')
        self.senlin_mock.get_policy.return_value = mock.Mock(
            id='fake_policy_id'
        )
        self.senlin_mock.cluster_policies.return_value = [
            {'policy_id': 'fake_policy_id', 'enabled': True}
        ]
        scheduler.TaskRunner(cluster.create)()
        self.assertEqual((cluster.CREATE, cluster.COMPLETE),
                         cluster.state)
        self.assertEqual(self.fake_cl.id, cluster.resource_id)
        self.assertEqual(1, self.senlin_mock.get_action.call_count)
        self.assertEqual(1, self.senlin_mock.get_cluster.call_count)
        return cluster

    def test_cluster_create_success(self):
        self._create_cluster(self.t)
        create_cluster_kwargs = {
            'name': 'SenlinCluster',
            'profile_id': 'fake_profile_id',
            'desired_capacity': 1,
            'min_size': 0,
            'max_size': -1,
            'metadata': {'foo': 'bar'},
            'timeout': 3600,
        }
        attach_policy_kwargs = {
            'cluster': self.fake_cl.id,
            'policy': 'fake_policy_id',
            'enabled': True
        }
        self.senlin_mock.create_cluster.assert_called_once_with(
            **create_cluster_kwargs)
        self.senlin_mock.cluster_attach_policy.assert_called_once_with(
            **attach_policy_kwargs)

    def test_cluster_create_error(self):
        cfg.CONF.set_override('action_retry_limit', 0)
        cluster = self._init_cluster(self.t)
        self.senlin_mock.create_cluster.return_value = self.fake_cl
        mock_cluster = mock.MagicMock()
        mock_cluster.status = 'ERROR'
        mock_cluster.status_reason = 'oops'
        self.senlin_mock.get_policy.return_value = mock.Mock(
            id='fake_policy_id'
        )
        self.senlin_mock.get_cluster.return_value = mock_cluster
        create_task = scheduler.TaskRunner(cluster.create)
        ex = self.assertRaises(exception.ResourceFailure, create_task)
        expected = ('ResourceInError: resources.senlin-cluster: '
                    'Went to status ERROR due to "oops"')
        self.assertEqual(expected, str(ex))

    def test_cluster_delete_success(self):
        cluster = self._create_cluster(self.t)
        self.senlin_mock.get_cluster.side_effect = [
            exceptions.ResourceNotFound('SenlinCluster'),
        ]
        scheduler.TaskRunner(cluster.delete)()
        self.senlin_mock.delete_cluster.assert_called_once_with(
            cluster.resource_id)

    def test_cluster_delete_error(self):
        cluster = self._create_cluster(self.t)
        self.senlin_mock.get_cluster.side_effect = exception.Error('oops')
        delete_task = scheduler.TaskRunner(cluster.delete)
        ex = self.assertRaises(exception.ResourceFailure, delete_task)
        expected = 'Error: resources.senlin-cluster: oops'
        self.assertEqual(expected, str(ex))

    def test_cluster_update_profile(self):
        cluster = self._create_cluster(self.t)
        # Mock translate rules
        self.senlin_mock.get_profile.side_effect = [
            mock.Mock(id='new_profile_id'),
            mock.Mock(id='fake_profile_id'),
            mock.Mock(id='new_profile_id'),
        ]
        new_t = copy.deepcopy(self.t)
        props = new_t['resources']['senlin-cluster']['properties']
        props['profile'] = 'new_profile'
        props['name'] = 'new_name'
        rsrc_defns = template.Template(new_t).resource_definitions(self.stack)
        new_cluster = rsrc_defns['senlin-cluster']
        self.senlin_mock.update_cluster.return_value = mock.Mock(
            location='/actions/fake-action')
        self.senlin_mock.get_action.return_value = mock.Mock(
            status='SUCCEEDED')
        scheduler.TaskRunner(cluster.update, new_cluster)()
        self.assertEqual((cluster.UPDATE, cluster.COMPLETE), cluster.state)
        cluster_update_kwargs = {
            'profile_id': 'new_profile_id',
            'name': 'new_name'
        }
        self.senlin_mock.update_cluster.assert_called_once_with(
            cluster=self.fake_cl, **cluster_update_kwargs)
        self.assertEqual(2, self.senlin_mock.get_action.call_count)

    def test_cluster_update_desire_capacity(self):
        cluster = self._create_cluster(self.t)
        new_t = copy.deepcopy(self.t)
        props = new_t['resources']['senlin-cluster']['properties']
        props['desired_capacity'] = 10
        rsrc_defns = template.Template(new_t).resource_definitions(self.stack)
        new_cluster = rsrc_defns['senlin-cluster']
        self.senlin_mock.cluster_resize.return_value = {
            'action': 'fake-action'}
        self.senlin_mock.get_action.return_value = mock.Mock(
            status='SUCCEEDED')
        scheduler.TaskRunner(cluster.update, new_cluster)()
        self.assertEqual((cluster.UPDATE, cluster.COMPLETE), cluster.state)
        cluster_resize_kwargs = {
            'adjustment_type': 'EXACT_CAPACITY',
            'number': 10
        }
        self.senlin_mock.cluster_resize.assert_called_once_with(
            cluster=cluster.resource_id, **cluster_resize_kwargs)
        self.assertEqual(2, self.senlin_mock.get_action.call_count)

    def test_cluster_update_policy_add_remove(self):
        cluster = self._create_cluster(self.t)
        # Mock translate rules
        self.senlin_mock.get_policy.side_effect = [
            mock.Mock(id='new_policy_id'),
            mock.Mock(id='fake_policy_id'),
            mock.Mock(id='new_policy_id'),
        ]
        new_t = copy.deepcopy(self.t)
        props = new_t['resources']['senlin-cluster']['properties']
        props['policies'] = [{'policy': 'new_policy'}]
        rsrc_defns = template.Template(new_t).resource_definitions(self.stack)
        new_cluster = rsrc_defns['senlin-cluster']
        self.senlin_mock.cluster_detach_policy.return_value = {
            'action': 'fake-action'}
        self.senlin_mock.cluster_attach_policy.return_value = {
            'action': 'fake-action'}
        self.senlin_mock.get_action.return_value = mock.Mock(
            status='SUCCEEDED')
        scheduler.TaskRunner(cluster.update, new_cluster)()
        self.assertEqual((cluster.UPDATE, cluster.COMPLETE), cluster.state)
        detach_policy_kwargs = {
            'policy': 'fake_policy_id',
            'cluster': cluster.resource_id,
            'enabled': True,
        }
        self.assertEqual(2,
                         self.senlin_mock.cluster_attach_policy.call_count)
        self.senlin_mock.cluster_detach_policy.assert_called_once_with(
            **detach_policy_kwargs)
        self.assertEqual(0, self.senlin_mock.cluster_update_policy.call_count)
        self.assertEqual(3, self.senlin_mock.get_action.call_count)

    def test_cluster_update_policy_exists(self):
        cluster = self._create_cluster(self.t)
        new_t = copy.deepcopy(self.t)
        props = new_t['resources']['senlin-cluster']['properties']
        props['policies'] = [{'policy': 'fake_policy', 'enabled': False}]
        rsrc_defns = template.Template(new_t).resource_definitions(self.stack)
        new_cluster = rsrc_defns['senlin-cluster']
        self.senlin_mock.cluster_update_policy.return_value = {
            'action': 'fake-action'}
        self.senlin_mock.get_action.return_value = mock.Mock(
            status='SUCCEEDED')
        scheduler.TaskRunner(cluster.update, new_cluster)()
        self.assertEqual((cluster.UPDATE, cluster.COMPLETE), cluster.state)
        update_policy_kwargs = {
            'policy': 'fake_policy_id',
            'cluster': cluster.resource_id,
            'enabled': False,
        }
        self.senlin_mock.cluster_update_policy.assert_called_once_with(
            **update_policy_kwargs)
        self.assertEqual(1, self.senlin_mock.cluster_attach_policy.call_count)
        self.assertEqual(0, self.senlin_mock.cluster_detach_policy.call_count)

    def test_cluster_update_failed(self):
        cluster = self._create_cluster(self.t)
        new_t = copy.deepcopy(self.t)
        props = new_t['resources']['senlin-cluster']['properties']
        props['desired_capacity'] = 3
        rsrc_defns = template.Template(new_t).resource_definitions(self.stack)
        update_snippet = rsrc_defns['senlin-cluster']
        self.senlin_mock.cluster_resize.return_value = {
            'action': 'fake-action'}
        self.senlin_mock.get_action.return_value = mock.Mock(
            status='FAILED', status_reason='Unknown')
        exc = self.assertRaises(
            exception.ResourceFailure,
            scheduler.TaskRunner(cluster.update, update_snippet))
        self.assertEqual('ResourceInError: resources.senlin-cluster: '
                         'Went to status FAILED due to "Unknown"',
                         str(exc))

    def test_cluster_get_attr_collect(self):
        cluster = self._create_cluster(self.t)
        self.senlin_mock.collect_cluster_attrs.return_value = [
            mock.Mock(attr_value='ip1')]
        attr_path1 = ['details.addresses.private[0].addr']
        self.assertEqual(
            ['ip1'], cluster.get_attribute(cluster.ATTR_COLLECT, *attr_path1))
        attr_path2 = ['details.addresses.private[0].addr', 0]
        self.assertEqual(
            'ip1', cluster.get_attribute(cluster.ATTR_COLLECT, *attr_path2))
        self.senlin_mock.collect_cluster_attrs.assert_called_with(
            cluster.resource_id, attr_path2[0])

    def test_cluster_resolve_attribute(self):
        excepted_show = {
            'id': 'some_id',
            'status': 'ACTIVE',
            'status_reason': 'Unknown',
            'name': 'SenlinCluster',
            'metadata': {'foo': 'bar'},
            'timeout': 3600,
            'desired_capacity': 1,
            'max_size': -1,
            'min_size': 0,
            'nodes': ['node1'],
            'profile_name': 'fake_profile',
            'profile_id': 'fake_profile_id',
            'policies': [{'policy_id': 'fake_policy_id', 'enabled': True}]
        }
        cluster = self._create_cluster(self.t)
        self.assertEqual(self.fake_cl.desired_capacity,
                         cluster._resolve_attribute('desired_capacity'))
        self.assertEqual(['node1'],
                         cluster._resolve_attribute('nodes'))
        self.assertEqual(excepted_show,
                         cluster._show_resource())

    def test_cluster_get_live_state(self):
        expected_reality = {
            'name': 'SenlinCluster',
            'metadata': {'foo': 'bar'},
            'timeout': 3600,
            'desired_capacity': 1,
            'max_size': -1,
            'min_size': 0,
            'profile': 'fake_profile_id',
            'policies': [{'policy': 'fake_policy_id', 'enabled': True}]
        }
        cluster = self._create_cluster(self.t)
        self.senlin_mock.get_cluster.return_value = self.fake_cl

        reality = cluster.get_live_state(cluster.properties)

        self.assertEqual(expected_reality, reality)


class TestSenlinClusterValidation(common.HeatTestCase):
    def setUp(self):
        super(TestSenlinClusterValidation, self).setUp()
        self.t = template_format.parse(cluster_stack_template)

    def test_invalid_min_max_size(self):
        self.t['resources']['senlin-cluster']['properties']['min_size'] = 2
        self.t['resources']['senlin-cluster']['properties']['max_size'] = 1
        stack = utils.parse_stack(self.t)
        ex = self.assertRaises(exception.StackValidationFailed,
                               stack['senlin-cluster'].validate)
        self.assertEqual('min_size can not be greater than max_size',
                         str(ex))

    def test_invalid_desired_capacity(self):
        self.t['resources']['senlin-cluster']['properties']['min_size'] = 1
        self.t['resources']['senlin-cluster']['properties']['max_size'] = 2
        self.t['resources']['senlin-cluster']['properties'][
            'desired_capacity'] = 3
        stack = utils.parse_stack(self.t)
        ex = self.assertRaises(exception.StackValidationFailed,
                               stack['senlin-cluster'].validate)
        self.assertEqual(
            'desired_capacity must be between min_size and max_size',
            str(ex)
        )
