# 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 senlin.common import consts
from senlin.common import scaleutils as su
from senlin.policies import deletion_policy as dp
from senlin.tests.unit.common import base
from senlin.tests.unit.common import utils


class TestDeletionPolicy(base.SenlinTestCase):

    def setUp(self):
        super(TestDeletionPolicy, self).setUp()
        self.context = utils.dummy_context()
        self.spec = {
            'type': 'senlin.policy.deletion',
            'version': '1.0',
            'properties': {
                'criteria': 'OLDEST_FIRST',
                'destroy_after_deletion': True,
                'grace_period': 60,
                'reduce_desired_capacity': False
            }
        }

    def test_policy_init(self):
        policy = dp.DeletionPolicy('test-policy', self.spec)

        self.assertIsNone(policy.id)
        self.assertEqual('test-policy', policy.name)
        self.assertEqual('senlin.policy.deletion-1.0', policy.type)
        self.assertEqual('OLDEST_FIRST', policy.criteria)
        self.assertTrue(policy.destroy_after_deletion)
        self.assertEqual(60, policy.grace_period)
        self.assertFalse(policy.reduce_desired_capacity)

    @mock.patch.object(su, 'nodes_by_random')
    def test_victims_by_regions_random(self, mock_select):
        cluster = mock.Mock()
        node1 = mock.Mock(id=1)
        node2 = mock.Mock(id=2)
        node3 = mock.Mock(id=3)
        cluster.nodes_by_region.side_effect = [
            [node1], [node2, node3]
        ]

        mock_select.side_effect = [['1'], ['2', '3']]

        self.spec['properties']['criteria'] = 'RANDOM'
        policy = dp.DeletionPolicy('test-policy', self.spec)

        res = policy._victims_by_regions(cluster, {'R1': 1, 'R2': 2})
        self.assertEqual(['1', '2', '3'], res)
        mock_select.assert_has_calls([
            mock.call([node1], 1),
            mock.call([node2, node3], 2)
        ])
        cluster.nodes_by_region.assert_has_calls([
            mock.call('R1'), mock.call('R2')])

    @mock.patch.object(su, 'nodes_by_profile_age')
    def test_victims_by_regions_profile_age(self, mock_select):
        cluster = mock.Mock()
        node1 = mock.Mock(id=1)
        node2 = mock.Mock(id=2)
        node3 = mock.Mock(id=3)
        cluster.nodes_by_region.side_effect = [
            [node1], [node2, node3]
        ]

        mock_select.side_effect = [['1'], ['2', '3']]

        self.spec['properties']['criteria'] = 'OLDEST_PROFILE_FIRST'
        policy = dp.DeletionPolicy('test-policy', self.spec)

        res = policy._victims_by_regions(cluster, {'R1': 1, 'R2': 2})
        self.assertEqual(['1', '2', '3'], res)
        mock_select.assert_has_calls([
            mock.call([node1], 1),
            mock.call([node2, node3], 2)
        ])
        cluster.nodes_by_region.assert_has_calls([
            mock.call('R1'), mock.call('R2')])

    @mock.patch.object(su, 'nodes_by_age')
    def test_victims_by_regions_age_oldest(self, mock_select):
        cluster = mock.Mock()
        node1 = mock.Mock(id=1)
        node2 = mock.Mock(id=2)
        node3 = mock.Mock(id=3)
        cluster.nodes_by_region.side_effect = [
            [node1], [node2, node3]
        ]

        mock_select.side_effect = [['1'], ['2', '3']]

        self.spec['properties']['criteria'] = 'OLDEST_FIRST'
        policy = dp.DeletionPolicy('test-policy', self.spec)

        res = policy._victims_by_regions(cluster, {'R1': 1, 'R2': 2})
        self.assertEqual(['1', '2', '3'], res)
        mock_select.assert_has_calls([
            mock.call([node1], 1, True),
            mock.call([node2, node3], 2, True)
        ])
        cluster.nodes_by_region.assert_has_calls([
            mock.call('R1'), mock.call('R2')])

    @mock.patch.object(su, 'nodes_by_age')
    def test_victims_by_regions_age_youngest(self, mock_select):
        cluster = mock.Mock()
        node1 = mock.Mock(id=1)
        node2 = mock.Mock(id=2)
        node3 = mock.Mock(id=3)
        cluster.nodes_by_region.side_effect = [
            [node1], [node2, node3]
        ]

        mock_select.side_effect = [['1'], ['2', '3']]

        self.spec['properties']['criteria'] = 'YOUNGEST_FIRST'
        policy = dp.DeletionPolicy('test-policy', self.spec)

        res = policy._victims_by_regions(cluster, {'R1': 1, 'R2': 2})
        self.assertEqual(['1', '2', '3'], res)
        mock_select.assert_has_calls([
            mock.call([node1], 1, False),
            mock.call([node2, node3], 2, False)
        ])
        cluster.nodes_by_region.assert_has_calls([
            mock.call('R1'), mock.call('R2')])

    @mock.patch.object(su, 'nodes_by_random')
    def test_victims_by_zones_random(self, mock_select):
        cluster = mock.Mock()
        node1 = mock.Mock(id=1)
        node2 = mock.Mock(id=2)
        node3 = mock.Mock(id=3)
        cluster.nodes_by_zone.side_effect = [
            [node1], [node2, node3]
        ]

        mock_select.side_effect = [['1'], ['3']]

        self.spec['properties']['criteria'] = 'RANDOM'
        policy = dp.DeletionPolicy('test-policy', self.spec)

        res = policy._victims_by_zones(cluster, {'AZ1': 1, 'AZ2': 1})
        self.assertEqual(['1', '3'], res)
        mock_select.assert_has_calls([
            mock.call([node1], 1),
            mock.call([node2, node3], 1)
        ])
        cluster.nodes_by_zone.assert_has_calls(
            [mock.call('AZ1'), mock.call('AZ2')],
        )

    @mock.patch.object(su, 'nodes_by_profile_age')
    def test_victims_by_zones_profile_age(self, mock_select):
        cluster = mock.Mock()
        node1 = mock.Mock(id=1)
        node2 = mock.Mock(id=2)
        node3 = mock.Mock(id=3)
        cluster.nodes_by_zone.side_effect = [
            [node1], [node2, node3]
        ]

        mock_select.side_effect = [['1'], ['2']]

        self.spec['properties']['criteria'] = 'OLDEST_PROFILE_FIRST'
        policy = dp.DeletionPolicy('test-policy', self.spec)

        res = policy._victims_by_zones(cluster, {'AZ1': 1, 'AZ2': 1})
        self.assertEqual(['1', '2'], res)
        mock_select.assert_has_calls(
            [
                mock.call([node1], 1),
                mock.call([node2, node3], 1)
            ],
        )
        cluster.nodes_by_zone.assert_has_calls(
            [mock.call('AZ1'), mock.call('AZ2')],
        )

    @mock.patch.object(su, 'nodes_by_age')
    def test_victims_by_zones_age_oldest(self, mock_select):
        cluster = mock.Mock()
        node1 = mock.Mock(id=1)
        node2 = mock.Mock(id=2)
        node3 = mock.Mock(id=3)
        cluster.nodes_by_zone.side_effect = [
            [node1], [node2, node3]
        ]

        mock_select.side_effect = [['1'], ['3']]

        self.spec['properties']['criteria'] = 'OLDEST_FIRST'
        policy = dp.DeletionPolicy('test-policy', self.spec)

        res = policy._victims_by_zones(cluster, {'AZ1': 1, 'AZ8': 1})
        self.assertEqual(['1', '3'], res)
        mock_select.assert_has_calls([
            mock.call([node1], 1, True),
            mock.call([node2, node3], 1, True)
        ])
        cluster.nodes_by_zone.assert_has_calls(
            [mock.call('AZ1'), mock.call('AZ8')],
        )

    @mock.patch.object(su, 'nodes_by_age')
    def test_victims_by_zones_age_youngest(self, mock_select):
        cluster = mock.Mock()
        node1 = mock.Mock(id=1)
        node2 = mock.Mock(id=3)
        node3 = mock.Mock(id=5)
        cluster.nodes_by_zone.side_effect = [
            [node1], [node2, node3]
        ]

        mock_select.side_effect = [['1'], ['3', '5']]

        self.spec['properties']['criteria'] = 'YOUNGEST_FIRST'
        policy = dp.DeletionPolicy('test-policy', self.spec)

        res = policy._victims_by_zones(cluster, {'AZ5': 1, 'AZ6': 2})
        self.assertEqual(['1', '3', '5'], res)
        mock_select.assert_has_calls(
            [
                mock.call([node1], 1, False),
                mock.call([node2, node3], 2, False)
            ],
        )
        cluster.nodes_by_zone.assert_has_calls(
            [mock.call('AZ5'), mock.call('AZ6')],
        )

    def test_update_action_clean(self):
        action = mock.Mock()
        action.data = {}

        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy._update_action(action, ['N1', 'N2'])

        pd = {
            'status': 'OK',
            'reason': 'Candidates generated',
            'deletion': {
                'count': 2,
                'candidates': ['N1', 'N2'],
                'destroy_after_deletion': True,
                'grace_period': 60,
                'reduce_desired_capacity': False,
            }
        }
        self.assertEqual(pd, action.data)
        action.store.assert_called_with(action.context)

    def test_update_action_override(self):
        action = mock.Mock()
        action.data = {
            'deletion': {
                'count': 3,
            }
        }

        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy._update_action(action, ['N1', 'N2'])

        pd = {
            'status': 'OK',
            'reason': 'Candidates generated',
            'deletion': {
                'count': 2,
                'candidates': ['N1', 'N2'],
                'destroy_after_deletion': True,
                'grace_period': 60,
                'reduce_desired_capacity': False,
            }
        }
        self.assertEqual(pd, action.data)
        action.store.assert_called_with(action.context)

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    def test_pre_op_del_nodes(self, mock_update):
        action = mock.Mock()
        action.context = self.context
        action.inputs = {
            'count': 2,
            'candidates': ['N1', 'N2'],
        }
        action.data = {}
        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy.pre_op('FAKE_ID', action)

        mock_update.assert_called_once_with(action, ['N1', 'N2'])

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    def test_pre_op_node_delete(self, mock_update):
        action = mock.Mock(action=consts.NODE_DELETE, context=self.context,
                           inputs={}, data={}, entity=mock.Mock(id='NODE_ID'))
        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy.pre_op('FAKE_ID', action)

        mock_update.assert_called_once_with(action, ['NODE_ID'])

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'nodes_by_age')
    def test_pre_op_with_count_decisions(self, mock_select, mock_update):
        action = mock.Mock(context=self.context, inputs={},
                           data={'deletion': {'count': 2}})
        cluster = mock.Mock(nodes=['a', 'b', 'c'])
        action.entity = cluster
        mock_select.return_value = ['NODE1', 'NODE2']
        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy.pre_op('FAKE_ID', action)

        mock_update.assert_called_once_with(action, ['NODE1', 'NODE2'])
        mock_select.assert_called_once_with(cluster.nodes, 2, True)

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(dp.DeletionPolicy, '_victims_by_regions')
    def test_pre_op_with_region_decisions(self, mock_select, mock_update):
        action = mock.Mock(context=self.context, inputs={})
        action.data = {
            'deletion': {
                'count': 2,
                'regions': {
                    'R1': 1,
                    'R2': 1
                }
            }
        }
        cluster = mock.Mock(nodes=['a', 'b', 'c'])
        action.entity = cluster
        mock_select.return_value = ['NODE1', 'NODE2']
        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy.pre_op('FAKE_ID', action)

        mock_update.assert_called_once_with(action, ['NODE1', 'NODE2'])
        mock_select.assert_called_once_with(cluster, {'R1': 1, 'R2': 1})

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(dp.DeletionPolicy, '_victims_by_zones')
    def test_pre_op_with_zone_decisions(self, mock_select, mock_update):
        action = mock.Mock(context=self.context, inputs={})
        action.data = {
            'deletion': {
                'count': 2,
                'zones': {
                    'AZ1': 1,
                    'AZ2': 1
                }
            }
        }
        cluster = mock.Mock(nodes=['a', 'b', 'c'])
        action.entity = cluster
        mock_select.return_value = ['NODE1', 'NODE2']
        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy.pre_op('FAKE_ID', action)

        mock_update.assert_called_once_with(action, ['NODE1', 'NODE2'])
        mock_select.assert_called_once_with(cluster, {'AZ1': 1, 'AZ2': 1})

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'nodes_by_age')
    def test_pre_op_scale_in_with_count(self, mock_select, mock_update):
        action = mock.Mock(context=self.context, data={}, inputs={'count': 2},
                           action=consts.CLUSTER_SCALE_IN)
        cluster = mock.Mock(nodes=[mock.Mock()])
        action.entity = cluster
        mock_select.return_value = ['NODE_ID']
        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy.pre_op('FAKE_ID', action)

        mock_update.assert_called_once_with(action, ['NODE_ID'])
        # the following was invoked with 1 because the input count is
        # greater than the cluster size
        mock_select.assert_called_once_with(cluster.nodes, 1, True)

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'nodes_by_age')
    def test_pre_op_scale_in_without_count(self, mock_select, mock_update):
        action = mock.Mock(context=self.context, data={}, inputs={},
                           action=consts.CLUSTER_SCALE_IN)
        cluster = mock.Mock(nodes=[mock.Mock()])
        action.entity = cluster
        mock_select.return_value = ['NODE_ID']
        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy.pre_op('FAKE_ID', action)

        mock_update.assert_called_once_with(action, ['NODE_ID'])
        # the following was invoked with 1 because the input count is
        # not specified so 1 becomes the default
        mock_select.assert_called_once_with(cluster.nodes, 1, True)

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'parse_resize_params')
    def test_pre_op_resize_failed_parse(self, mock_parse, mock_update):
        action = mock.Mock(context=self.context, inputs={}, data={},
                           action=consts.CLUSTER_RESIZE)
        cluster = mock.Mock(nodes=[mock.Mock(), mock.Mock()])
        action.entity = cluster
        mock_parse.return_value = 'ERROR', 'Failed parsing.'
        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy.pre_op('FAKE_ID', action)

        self.assertEqual('ERROR', action.data['status'])
        self.assertEqual('Failed parsing.', action.data['reason'])
        mock_parse.assert_called_once_with(action, cluster, 2)
        self.assertEqual(0, mock_update.call_count)

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'parse_resize_params')
    def test_pre_op_resize_not_deletion(self, mock_parse, mock_update):
        def fake_parse(action, cluster, current):
            action.data = {}
            return 'OK', 'cool'

        action = mock.Mock(context=self.context, inputs={},
                           action=consts.CLUSTER_RESIZE)
        cluster = mock.Mock(nodes=[mock.Mock(), mock.Mock()])
        action.entity = cluster
        mock_parse.side_effect = fake_parse
        policy = dp.DeletionPolicy('test-policy', self.spec)
        # a simulation of non-deletion RESZIE
        action.data = {}

        policy.pre_op('FAKE_ID', action)

        mock_parse.assert_called_once_with(action, cluster, 2)
        self.assertEqual(0, mock_update.call_count)

    @mock.patch.object(su, 'parse_resize_params')
    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'nodes_by_age')
    def test_pre_op_resize_with_count(self, mock_select, mock_update,
                                      mock_parse):
        def fake_parse(a, cluster, current):
            a.data = {
                'deletion': {
                    'count': 2
                }
            }
            return 'OK', 'cool'

        action = mock.Mock(context=self.context, inputs={}, data={},
                           action=consts.CLUSTER_RESIZE)
        cluster = mock.Mock(nodes=[mock.Mock(), mock.Mock()])
        action.entity = cluster
        mock_parse.side_effect = fake_parse
        mock_select.return_value = ['NID']
        policy = dp.DeletionPolicy('test-policy', self.spec)

        policy.pre_op('FAKE_ID', action)

        mock_parse.assert_called_once_with(action, cluster, 2)
        mock_update.assert_called_once_with(action, ['NID'])

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'nodes_by_random')
    def test_pre_op_do_random(self, mock_select, mock_update):
        action = mock.Mock(context=self.context, inputs={},
                           data={'deletion': {'count': 2}})
        cluster = mock.Mock(nodes=['a', 'b', 'c'])
        action.entity = cluster
        mock_select.return_value = ['NODE1', 'NODE2']
        spec = copy.deepcopy(self.spec)
        spec['properties']['criteria'] = 'RANDOM'
        policy = dp.DeletionPolicy('test-policy', spec)

        policy.pre_op('FAKE_ID', action)

        mock_select.assert_called_once_with(cluster.nodes, 2)
        mock_update.assert_called_once_with(action, ['NODE1', 'NODE2'])

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'nodes_by_profile_age')
    def test_pre_op_do_oldest_profile(self, mock_select, mock_update):
        action = mock.Mock(context=self.context, inputs={},
                           data={'deletion': {'count': 2}})
        mock_select.return_value = ['NODE1', 'NODE2']
        cluster = mock.Mock(nodes=['a', 'b', 'c'])
        action.entity = cluster
        spec = copy.deepcopy(self.spec)
        spec['properties']['criteria'] = 'OLDEST_PROFILE_FIRST'
        policy = dp.DeletionPolicy('test-policy', spec)

        policy.pre_op('FAKE_ID', action)

        mock_select.assert_called_once_with(cluster.nodes, 2)
        mock_update.assert_called_once_with(action, ['NODE1', 'NODE2'])

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'nodes_by_age')
    def test_pre_op_do_oldest_first(self, mock_select, mock_update):
        action = mock.Mock(context=self.context, inputs={},
                           data={'deletion': {'count': 2}})
        cluster = mock.Mock(nodes=['a', 'b', 'c'])
        action.entity = cluster
        mock_select.return_value = ['NODE1', 'NODE2']
        spec = copy.deepcopy(self.spec)
        spec['properties']['criteria'] = 'OLDEST_FIRST'
        policy = dp.DeletionPolicy('test-policy', spec)

        policy.pre_op('FAKE_ID', action)

        mock_select.assert_called_once_with(cluster.nodes, 2, True)
        mock_update.assert_called_once_with(action, ['NODE1', 'NODE2'])

    @mock.patch.object(dp.DeletionPolicy, '_update_action')
    @mock.patch.object(su, 'nodes_by_age')
    def test_pre_op_do_youngest_first(self, mock_select, mock_update):
        action = mock.Mock(context=self.context, inputs={},
                           data={'deletion': {'count': 2}})
        cluster = mock.Mock(nodes=['a', 'b', 'c'])
        action.entity = cluster
        mock_select.return_value = ['NODE1', 'NODE2']
        spec = copy.deepcopy(self.spec)
        spec['properties']['criteria'] = 'YOUNGEST_FIRST'
        policy = dp.DeletionPolicy('test-policy', spec)

        policy.pre_op('FAKE_ID', action)

        mock_select.assert_called_once_with(cluster.nodes, 2, False)
        mock_update.assert_called_once_with(action, ['NODE1', 'NODE2'])
