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

from senlin.common import consts
from senlin.objects import node as no
from senlin.policies import base as pb
from senlin.policies import scaling_policy as sp
from senlin.tests.unit.common import base
from senlin.tests.unit.common import utils

PROFILE_ID = 'aa5f86b8-e52b-4f2b-828a-4c14c770938d'
CLUSTER_ID = '2c5139a6-24ba-4a6f-bd53-a268f61536de'


class TestScalingPolicy(base.SenlinTestCase):

    def setUp(self):
        super(TestScalingPolicy, self).setUp()
        self.context = utils.dummy_context()
        self.spec = {
            'type': 'senlin.policy.scaling',
            'version': '1.0',
            'properties': {
                'event': 'CLUSTER_SCALE_IN',
                'adjustment': {
                    'type': 'CHANGE_IN_CAPACITY',
                    'number': 1,
                    'min_step': 1,
                    'best_effort': False,
                    'cooldown': 3,
                }
            }
        }
        self.profile = utils.create_profile(self.context, PROFILE_ID)
        self.cluster = utils.create_cluster(self.context, CLUSTER_ID,
                                            PROFILE_ID)

    def _create_nodes(self, count):
        NODE_IDS = [
            '6eaa45fa-bd2e-426d-ae49-f75db1a4bd73',
            '8bf73953-b57b-4e6b-bdef-83fa9420befb',
            'c3058ea0-5241-466b-89bc-6a85f6050a11',
        ]
        PHYSICAL_IDS = [
            '2417c5d6-9a89-4637-9ba6-82c00b180cb7',
            '374bf2b9-30ba-4a9b-822b-1196f6d4a368',
            '2a1b7e37-de18-4b22-9489-a7a413fdfe48',
        ]

        nodes = []
        for i in range(count):
            utils.create_node(self.context, NODE_IDS[i], PROFILE_ID,
                              CLUSTER_ID, PHYSICAL_IDS[i])
            nodes.append(NODE_IDS[i])
        return nodes

    def test_policy_init(self):
        policy = sp.ScalingPolicy('p1', self.spec)
        self.assertFalse(policy.singleton)

        self.assertIsNone(policy.id)
        self.assertEqual('p1', policy.name)
        self.assertEqual('senlin.policy.scaling-1.0', policy.type)
        self.assertEqual('CLUSTER_SCALE_IN', policy.event)
        adjustment = self.spec['properties']['adjustment']
        self.assertEqual(adjustment['type'], policy.adjustment_type)
        self.assertEqual(adjustment['number'], policy.adjustment_number)
        self.assertEqual(adjustment['min_step'], policy.adjustment_min_step)
        self.assertEqual(adjustment['best_effort'], policy.best_effort)
        self.assertEqual(adjustment['cooldown'], policy.cooldown)

    def test_policy_init_default_value(self):
        self.spec['properties']['adjustment'] = {}
        policy = sp.ScalingPolicy('p1', self.spec)

        self.assertIsNone(policy.id)
        self.assertEqual('senlin.policy.scaling-1.0', policy.type)
        self.assertEqual('p1', policy.name)
        self.assertEqual(consts.CHANGE_IN_CAPACITY, policy.adjustment_type)
        self.assertEqual(1, policy.adjustment_number)
        self.assertEqual(1, policy.adjustment_min_step)
        self.assertFalse(policy.best_effort)
        self.assertEqual(0, policy.cooldown)

    def test_calculate_adjustment_count(self):
        adjustment = self.spec['properties']['adjustment']
        # adjustment_type as EXACT_CAPACITY and event as cluster_scale_in
        current_size = 3
        adjustment['type'] = consts.EXACT_CAPACITY
        adjustment['number'] = 1
        policy = sp.ScalingPolicy('test-policy', self.spec)
        policy.event = consts.CLUSTER_SCALE_IN
        count = policy._calculate_adjustment_count(current_size)
        self.assertEqual(2, count)

        # adjustment_type as EXACT_CAPACITY and event as cluster_scale_out
        current_size = 3
        adjustment['type'] = consts.EXACT_CAPACITY
        adjustment['number'] = 1
        policy = sp.ScalingPolicy('test-policy', self.spec)
        policy.event = consts.CLUSTER_SCALE_OUT
        count = policy._calculate_adjustment_count(current_size)
        self.assertEqual(-2, count)

        # adjustment_type is CHANGE_IN_CAPACITY
        adjustment['type'] = consts.CHANGE_IN_CAPACITY
        adjustment['number'] = 1
        policy = sp.ScalingPolicy('test-policy', self.spec)
        count = policy._calculate_adjustment_count(current_size)
        self.assertEqual(1, count)

        # adjustment_type is CHANGE_IN_PERCENTAGE
        current_size = 10
        adjustment['type'] = consts.CHANGE_IN_PERCENTAGE
        adjustment['number'] = 50
        policy = sp.ScalingPolicy('test-policy', self.spec)
        count = policy._calculate_adjustment_count(current_size)
        self.assertEqual(5, count)

        # adjustment_type is CHANGE_IN_PERCENTAGE and min_step is 2
        adjustment['type'] = consts.CHANGE_IN_PERCENTAGE
        adjustment['number'] = 1
        adjustment['min_step'] = 2
        policy = sp.ScalingPolicy('test-policy', self.spec)
        count = policy._calculate_adjustment_count(current_size)
        self.assertEqual(2, count)

    def test_pre_op_pass_without_input(self):
        self._create_nodes(3)
        action = mock.Mock()
        action.context = self.context
        action.action = consts.CLUSTER_SCALE_IN
        action.inputs = {}
        adjustment = self.spec['properties']['adjustment']
        adjustment['type'] = consts.EXACT_CAPACITY
        adjustment['number'] = 1
        policy = sp.ScalingPolicy('test-policy', self.spec)

        policy.pre_op(self.cluster['id'], action)
        pd = {
            'deletion': {
                'count': 2,
            },
            'reason': 'Scaling request validated.',
            'status': pb.CHECK_OK,
        }
        action.data.update.assert_called_with(pd)
        action.store.assert_called_with(self.context)

    def test_pre_op_pass_with_input(self):
        self._create_nodes(3)
        action = mock.Mock()
        action.context = self.context
        action.action = consts.CLUSTER_SCALE_IN
        action.inputs = {'count': 1}
        adjustment = self.spec['properties']['adjustment']
        adjustment['type'] = consts.CHANGE_IN_CAPACITY
        adjustment['number'] = 2
        policy = sp.ScalingPolicy('p1', self.spec)

        policy.pre_op(self.cluster['id'], action)
        pd = {
            'deletion': {
                'count': 1,
            },
            'reason': 'Scaling request validated.',
            'status': pb.CHECK_OK,
        }
        action.data.update.assert_called_with(pd)
        action.store.assert_called_with(self.context)

        # count value is string rather than integer
        action.inputs = {'count': '1'}
        policy.pre_op(self.cluster['id'], action)
        pd = {
            'deletion': {
                'count': 1,
            },
            'reason': 'Scaling request validated.',
            'status': pb.CHECK_OK,
        }
        action.data.update.assert_called_with(pd)

    def test_pre_op_fail_negative_count(self):
        self._create_nodes(3)
        action = mock.Mock()
        action.context = self.context
        action.action = consts.CLUSTER_SCALE_IN
        action.inputs = {}
        adjustment = self.spec['properties']['adjustment']
        adjustment['type'] = consts.EXACT_CAPACITY
        adjustment['number'] = 5
        policy = sp.ScalingPolicy('test-policy', self.spec)

        policy.pre_op(self.cluster['id'], action)

        pd = {
            'status': pb.CHECK_ERROR,
            'reason': "Invalid count (-2) for action 'CLUSTER_SCALE_IN'.",
        }
        action.data.update.assert_called_with(pd)
        action.store.assert_called_with(self.context)

    def test_pre_op_fail_below_min_size(self):
        self._create_nodes(3)
        action = mock.Mock()
        action.action = consts.CLUSTER_SCALE_IN
        action.context = self.context
        action.inputs = {}
        adjustment = self.spec['properties']['adjustment']
        adjustment['type'] = consts.CHANGE_IN_CAPACITY
        adjustment['number'] = 3
        policy = sp.ScalingPolicy('test-policy', self.spec)

        policy.pre_op(self.cluster['id'], action)

        pd = {
            'status': pb.CHECK_ERROR,
            'reason': ("The target capacity (0) is less than the cluster's "
                       "min_size (1)."),
        }
        action.data.update.assert_called_with(pd)
        action.store.assert_called_with(self.context)

    def test_pre_op_pass_best_effort(self):
        self._create_nodes(3)
        action = mock.Mock()
        action.context = self.context
        action.action = consts.CLUSTER_SCALE_IN
        action.inputs = {}
        adjustment = self.spec['properties']['adjustment']
        adjustment['best_effort'] = True
        adjustment['type'] = consts.CHANGE_IN_CAPACITY
        adjustment['number'] = 3
        policy = sp.ScalingPolicy('test-policy', self.spec)

        policy.pre_op(self.cluster['id'], action)

        pd = {
            'deletion': {
                'count': 2,
            },
            'status': pb.CHECK_OK,
            'reason': 'Scaling request validated.',
        }
        action.data.update.assert_called_with(pd)
        action.store.assert_called_with(self.context)

    def test_pre_op_with_bad_nodes(self):
        self.nodes = self._create_nodes(3)
        no.Node.update(self.context, self.nodes[0], {'status': 'ERROR'})

        action = mock.Mock()
        action.context = self.context
        action.action = consts.CLUSTER_SCALE_IN
        action.inputs = {}
        adjustment = self.spec['properties']['adjustment']
        adjustment['type'] = consts.EXACT_CAPACITY
        adjustment['number'] = 1
        policy = sp.ScalingPolicy('test-policy', self.spec)

        policy.pre_op(self.cluster['id'], action)
        pd = {
            'deletion': {
                'count': 2,
            },
            'reason': 'Scaling request validated.',
            'status': pb.CHECK_OK,
        }
        action.data.update.assert_called_with(pd)
        action.store.assert_called_with(self.context)

    def test_need_check_in_event(self):
        action = mock.Mock()
        action.context = self.context
        action.action = consts.CLUSTER_SCALE_IN
        action.data = {}

        policy = sp.ScalingPolicy('test-policy', self.spec)
        res = policy.need_check('BEFORE', action)
        self.assertTrue(res)

    def test_need_check_not_in_event(self):
        action = mock.Mock()
        action.context = self.context
        action.action = consts.CLUSTER_SCALE_OUT
        action.data = {}

        policy = sp.ScalingPolicy('test-policy', self.spec)
        res = policy.need_check('BEFORE', action)
        self.assertFalse(res)
