# Copyright 2016 Cloudbase Solutions Srl
# All Rights Reserved.
#
#    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.

from unittest import mock

import ddt
from nova.compute import vm_states
from nova import exception
from nova import test as nova_test
from os_win import constants as os_win_const

from compute_hyperv.nova.cluster import livemigrationops
from compute_hyperv.nova import livemigrationops as base_livemigrationops
from compute_hyperv.tests import fake_instance
from compute_hyperv.tests.unit import test_base


@ddt.ddt
class ClusterLiveMigrationOpsTestCase(test_base.HyperVBaseTestCase):
    """Unit tests for the Hyper-V Cluster LivemigrationOps class."""

    _autospec_classes = [
        base_livemigrationops.volumeops.VolumeOps,
    ]

    def setUp(self):
        super(ClusterLiveMigrationOpsTestCase, self).setUp()
        self._fake_context = 'fake_context'
        self.livemigrops = livemigrationops.ClusterLiveMigrationOps()
        self._clustutils = self.livemigrops._clustutils

    def test_is_instance_clustered(self):
        ret = self.livemigrops.is_instance_clustered(
            mock.sentinel.instance)

        self.assertEqual(
            self.livemigrops._clustutils.vm_exists.return_value, ret)

    def test_live_migration_in_cluster(self):
        migr_timeout = 10
        self.flags(instance_live_migration_timeout=migr_timeout,
                   group='hyperv')

        mock_instance = fake_instance.fake_instance_obj(self._fake_context)
        self.livemigrops._clustutils.vm_exists.return_value = True
        post_method = mock.MagicMock()
        dest = 'fake_dest'
        node_names = [dest, 'fake_node2']
        get_nodes = self.livemigrops._clustutils.get_cluster_node_names
        get_nodes.return_value = node_names

        self.livemigrops.live_migration(
            self._fake_context, mock_instance, dest, post_method,
            mock.sentinel.recover_method,
            block_migration=mock.sentinel.block_migration,
            migrate_data=mock.sentinel.migrate_data)

        clustutils = self.livemigrops._clustutils
        clustutils.live_migrate_vm.assert_called_once_with(
            mock_instance.name, dest, migr_timeout)
        post_method.assert_called_once_with(
            self._fake_context, mock_instance, dest,
            mock.sentinel.block_migration, mock.sentinel.migrate_data)

    @mock.patch.object(livemigrationops.ClusterLiveMigrationOps,
                       '_check_failed_instance_migration')
    def test_live_migration_in_cluster_exception(self, mock_check_migr):
        mock_instance = fake_instance.fake_instance_obj(self._fake_context)
        self.livemigrops._clustutils.vm_exists.return_value = True
        recover_method = mock.MagicMock()
        dest = 'fake_dest'
        node_names = [dest, 'fake_node2']
        get_nodes = self.livemigrops._clustutils.get_cluster_node_names
        get_nodes.return_value = node_names
        clustutils = self.livemigrops._clustutils
        clustutils.live_migrate_vm.side_effect = nova_test.TestingException

        self.livemigrops.live_migration(
            self._fake_context, mock_instance, dest, mock.sentinel.post_method,
            recover_method,
            block_migration=mock.sentinel.block_migration,
            migrate_data=mock.sentinel.migrate_data)

        mock_check_migr.assert_called_once_with(
            mock_instance,
            expected_state=os_win_const.CLUSTER_GROUP_ONLINE)

        recover_method.assert_called_once_with(
            self._fake_context, mock_instance, dest,
            mock.sentinel.migrate_data)

    @mock.patch.object(base_livemigrationops.LiveMigrationOps,
                       'live_migration')
    def test_live_migration_outside_cluster(self, mock_super_live_migration):
        mock_instance = fake_instance.fake_instance_obj(self._fake_context)
        self.livemigrops._clustutils.vm_exists.return_value = True
        dest = 'fake_dest'
        node_names = ['fake_node1', 'fake_node2']
        get_nodes = self.livemigrops._clustutils.get_cluster_node_names
        get_nodes.return_value = node_names

        self.livemigrops.live_migration(
            self._fake_context, mock_instance, dest, mock.sentinel.post_method,
            mock.sentinel.recover_method, block_migration=False,
            migrate_data=None)

        mock_super_live_migration.assert_called_once_with(
            self._fake_context, mock_instance, dest, mock.sentinel.post_method,
            mock.sentinel.recover_method, False, None)

    @ddt.data({},
              {'state': os_win_const.CLUSTER_GROUP_PENDING,
               'expected_invalid_state': True},
              {'migration_queued': True,
               'expected_invalid_state': True},
              {'owner_node': 'some_other_node',
               'expected_invalid_state': True})
    @ddt.unpack
    def test_check_failed_instance_migration(
            self, state=os_win_const.CLUSTER_GROUP_ONLINE,
            owner_node='source_node', migration_queued=False,
            expected_invalid_state=False):
        state_info = dict(owner_node=owner_node.upper(),
                          state=state,
                          migration_queued=migration_queued)
        self._clustutils.get_cluster_group_state_info.return_value = (
            state_info)
        self._clustutils.get_node_name.return_value = 'source_node'

        mock_instance = mock.Mock()

        if expected_invalid_state:
            self.assertRaises(
                exception.InstanceInvalidState,
                self.livemigrops._check_failed_instance_migration,
                mock_instance,
                os_win_const.CLUSTER_GROUP_ONLINE)
            self.assertEqual(vm_states.ERROR, mock_instance.vm_state)
        else:
            self.livemigrops._check_failed_instance_migration(
                mock_instance, os_win_const.CLUSTER_GROUP_ONLINE)

        self._clustutils.get_cluster_group_state_info.assert_called_once_with(
            mock_instance.name)
        self._clustutils.get_node_name.assert_called_once_with()

    def test_pre_live_migration_clustered(self):
        self.livemigrops.pre_live_migration(self._fake_context,
                                            mock.sentinel.fake_instance,
                                            mock.sentinel.bdi,
                                            mock.sentinel.network_info)

        fake_conn_vol = self.livemigrops._volumeops.connect_volumes
        fake_conn_vol.assert_called_once_with(mock.sentinel.bdi)

    @mock.patch.object(base_livemigrationops.LiveMigrationOps,
                       'pre_live_migration')
    def test_pre_live_migration_not_clustered(self, mock_pre_live_migration):
        self.livemigrops._clustutils.vm_exists.return_value = False
        self.livemigrops.pre_live_migration(self._fake_context,
                                            mock.sentinel.fake_instance,
                                            mock.sentinel.bdi,
                                            mock.sentinel.network_info)

        mock_pre_live_migration.assert_called_once_with(
            self._fake_context, mock.sentinel.fake_instance,
            mock.sentinel.bdi, mock.sentinel.network_info)

    @mock.patch.object(base_livemigrationops.LiveMigrationOps,
                       'post_live_migration')
    def test_post_live_migration_clustered(self, mock_post_live_migration):
        self.livemigrops.post_live_migration(self._fake_context,
                                             mock.sentinel.fake_instance,
                                             mock.sentinel.bdi,
                                             mock.sentinel.migrate_data)

        self.assertFalse(mock_post_live_migration.called)

    @mock.patch.object(base_livemigrationops.LiveMigrationOps,
                       'post_live_migration')
    def test_post_live_migration_not_clustered(self, mock_post_live_migration):
        self.livemigrops._clustutils.vm_exists.return_value = False
        self.livemigrops.post_live_migration(self._fake_context,
                                             mock.sentinel.fake_instance,
                                             mock.sentinel.bdi,
                                             mock.sentinel.migrate_data)

        mock_post_live_migration.assert_called_once_with(
            self._fake_context, mock.sentinel.fake_instance,
            mock.sentinel.bdi,
            mock.sentinel.migrate_data)
