/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

package org.apache.cloudstack.cluster;

import com.cloud.api.query.dao.HostJoinDao;
import com.cloud.api.query.vo.HostJoinVO;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventVO;
import com.cloud.event.dao.EventDao;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.offering.ServiceOffering;
import com.cloud.org.Cluster;
import com.cloud.org.Grouping;
import com.cloud.server.ManagementServer;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.db.GlobalLock;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.VMInstanceDao;
import org.apache.cloudstack.api.command.admin.cluster.GenerateClusterDrsPlanCmd;
import org.apache.cloudstack.api.response.ClusterDrsPlanMigrationResponse;
import org.apache.cloudstack.api.response.ClusterDrsPlanResponse;
import org.apache.cloudstack.cluster.dao.ClusterDrsPlanDao;
import org.apache.cloudstack.cluster.dao.ClusterDrsPlanMigrationDao;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;

import javax.naming.ConfigurationException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;

@RunWith(MockitoJUnitRunner.class)
public class ClusterDrsServiceImplTest {

    @Mock
    ClusterDrsAlgorithm condensedAlgorithm;

    @Mock
    ManagementServer managementServer;

    @Mock
    ClusterDrsAlgorithm balancedAlgorithm;

    @Mock
    GenerateClusterDrsPlanCmd cmd;

    AutoCloseable closeable;

    @Mock
    private ClusterDao clusterDao;

    @Mock
    private ClusterDrsPlanDao drsPlanDao;

    @Mock
    private ClusterDrsPlanMigrationDao drsPlanMigrationDao;

    @Mock
    private EventDao eventDao;

    @Mock
    private HostDao hostDao;

    @Mock
    private HostJoinDao hostJoinDao;

    @Mock
    private ServiceOfferingDao serviceOfferingDao;

    @Mock
    private VMInstanceDao vmInstanceDao;

    @Spy
    @InjectMocks
    private ClusterDrsServiceImpl clusterDrsService = new ClusterDrsServiceImpl();

    private MockedStatic<GlobalLock> globalLockMocked;

    @Before
    public void setUp() throws NoSuchFieldException, IllegalAccessException {
        closeable = MockitoAnnotations.openMocks(this);

        HashMap<String, ClusterDrsAlgorithm> drsAlgorithmMap = new HashMap<>();
        drsAlgorithmMap.put("balanced", balancedAlgorithm);
        drsAlgorithmMap.put("condensed", condensedAlgorithm);

        clusterDrsService.setDrsAlgorithms(List.of(new ClusterDrsAlgorithm[]{balancedAlgorithm, condensedAlgorithm}));
        ReflectionTestUtils.setField(clusterDrsService, "drsAlgorithmMap", drsAlgorithmMap);
        Field f = ConfigKey.class.getDeclaredField("_defaultValue");
        f.setAccessible(true);
        f.set(clusterDrsService.ClusterDrsAlgorithm, "balanced");
        Mockito.when(cmd.getId()).thenReturn(1L);

        globalLockMocked = Mockito.mockStatic(GlobalLock.class);
        GlobalLock lock = Mockito.mock(GlobalLock.class);
        Mockito.when(GlobalLock.getInternLock("cluster.drs.1")).thenReturn(lock);
    }

    @After
    public void tearDown() throws Exception {
        globalLockMocked.close();
        closeable.close();
    }

    @Test
    public void testGetCommands() {
        assertFalse(clusterDrsService.getCommands().isEmpty());
    }

    @Test
    public void testGetDrsPlan() throws ConfigurationException {
        ClusterVO cluster = Mockito.mock(ClusterVO.class);
        Mockito.when(cluster.getId()).thenReturn(1L);
        Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);

        HostVO host1 = Mockito.mock(HostVO.class);
        Mockito.when(host1.getId()).thenReturn(1L);

        HostVO host2 = Mockito.mock(HostVO.class);
        Mockito.when(host2.getId()).thenReturn(2L);

        VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
        Mockito.when(vm1.getId()).thenReturn(1L);
        Mockito.when(vm1.getHostId()).thenReturn(1L);

        VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class);
        Mockito.when(vm2.getHostId()).thenReturn(2L);

        List<HostVO> hostList = new ArrayList<>();
        hostList.add(host1);
        hostList.add(host2);

        HostJoinVO hostJoin1 = Mockito.mock(HostJoinVO.class);
        Mockito.when(hostJoin1.getId()).thenReturn(1L);
        Mockito.when(hostJoin1.getCpuUsedCapacity()).thenReturn(1000L);
        Mockito.when(hostJoin1.getCpuReservedCapacity()).thenReturn(0L);
        Mockito.when(hostJoin1.getMemUsedCapacity()).thenReturn(1024L);

        HostJoinVO hostJoin2 = Mockito.mock(HostJoinVO.class);
        Mockito.when(hostJoin2.getId()).thenReturn(2L);
        Mockito.when(hostJoin2.getCpuUsedCapacity()).thenReturn(1000L);
        Mockito.when(hostJoin2.getCpuReservedCapacity()).thenReturn(0L);
        Mockito.when(hostJoin2.getMemUsedCapacity()).thenReturn(1024L);

        List<VMInstanceVO> vmList = new ArrayList<>();
        vmList.add(vm1);
        vmList.add(vm2);

        ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class);
        Mockito.when(serviceOffering.getCpu()).thenReturn(1);
        Mockito.when(serviceOffering.getRamSize()).thenReturn(1024);
        Mockito.when(serviceOffering.getSpeed()).thenReturn(1000);

        Mockito.when(hostDao.findByClusterId(1L)).thenReturn(hostList);
        Mockito.when(vmInstanceDao.listByClusterId(1L)).thenReturn(vmList);
        Mockito.when(balancedAlgorithm.needsDrs(Mockito.any(), Mockito.anyList(), Mockito.anyList())).thenReturn(
                true, false);
        Mockito.when(
                clusterDrsService.getBestMigration(Mockito.any(Cluster.class), Mockito.any(ClusterDrsAlgorithm.class),
                        Mockito.anyList(), Mockito.anyMap(), Mockito.anyMap(), Mockito.anyMap())).thenReturn(
                new Pair<>(vm1, host2));
        Mockito.when(serviceOfferingDao.findByIdIncludingRemoved(Mockito.anyLong(), Mockito.anyLong())).thenReturn(
                serviceOffering);
        Mockito.when(hostJoinDao.searchByIds(host1.getId(), host2.getId())).thenReturn(List.of(hostJoin1, hostJoin2));

        List<Ternary<VirtualMachine, Host, Host>> iterations = clusterDrsService.getDrsPlan(cluster, 5);

        Mockito.verify(hostDao, Mockito.times(1)).findByClusterId(1L);
        Mockito.verify(vmInstanceDao, Mockito.times(1)).listByClusterId(1L);
        Mockito.verify(balancedAlgorithm, Mockito.times(2)).needsDrs(Mockito.any(), Mockito.anyList(),
                Mockito.anyList());

        assertEquals(1, iterations.size());
    }

    @Test(expected = InvalidParameterValueException.class)
    public void testGenerateDrsPlanClusterNotFound() {
        Mockito.when(clusterDao.findById(1L)).thenReturn(null);
        clusterDrsService.generateDrsPlan(cmd);
    }

    @Test(expected = InvalidParameterValueException.class)
    public void testGenerateDrsPlanClusterDisabled() {
        ClusterVO cluster = Mockito.mock(ClusterVO.class);
        Mockito.when(cluster.getName()).thenReturn("testCluster");
        Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Disabled);

        Mockito.when(clusterDao.findById(1L)).thenReturn(cluster);

        clusterDrsService.generateDrsPlan(cmd);
    }

    @Test(expected = InvalidParameterValueException.class)
    public void testGenerateDrsPlanClusterNotCloudManaged() {

        ClusterVO cluster = Mockito.mock(ClusterVO.class);
        Mockito.when(cluster.getName()).thenReturn("testCluster");
        Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);

        Mockito.when(clusterDao.findById(1L)).thenReturn(cluster);

        clusterDrsService.generateDrsPlan(cmd);
    }

    @Test(expected = InvalidParameterValueException.class)
    public void testGenerateDrsPlanInvalidIterations() {
        ClusterVO cluster = Mockito.mock(ClusterVO.class);
        Mockito.when(cluster.getName()).thenReturn("testCluster");
        Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);

        Mockito.when(clusterDao.findById(1L)).thenReturn(cluster);
        Mockito.when(cmd.getMaxMigrations()).thenReturn(0);

        clusterDrsService.generateDrsPlan(cmd);
    }

    @Test(expected = CloudRuntimeException.class)
    public void testGenerateDrsPlanConfigurationException() throws ConfigurationException {
        ClusterVO cluster = Mockito.mock(ClusterVO.class);
        Mockito.when(cluster.getId()).thenReturn(1L);
        Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);
        Mockito.when(clusterDao.findById(1L)).thenReturn(cluster);
        Mockito.when(clusterDrsService.getDrsPlan(cluster, 5)).thenThrow(new ConfigurationException("test"));
        Mockito.when(cmd.getMaxMigrations()).thenReturn(5);

        clusterDrsService.generateDrsPlan(cmd);
    }

    @Test
    public void testGenerateDrsPlan() throws ConfigurationException {
        ClusterVO cluster = Mockito.mock(ClusterVO.class);
        Mockito.when(cluster.getId()).thenReturn(1L);
        Mockito.when(cluster.getAllocationState()).thenReturn(Grouping.AllocationState.Enabled);

        VirtualMachine vm = Mockito.mock(VirtualMachine.class);
        Mockito.when(vm.getId()).thenReturn(1L);

        Host srcHost = Mockito.mock(Host.class);
        Mockito.when(srcHost.getId()).thenReturn(1L);

        Host destHost = Mockito.mock(Host.class);
        Mockito.when(destHost.getId()).thenReturn(2L);

        Mockito.when(clusterDao.findById(1L)).thenReturn(cluster);
        Mockito.when(eventDao.findById(Mockito.anyLong())).thenReturn(Mockito.mock(EventVO.class));
        Mockito.when(cmd.getMaxMigrations()).thenReturn(2);
        Mockito.doReturn(List.of(new Ternary<>(vm, srcHost,
                destHost))).when(clusterDrsService).getDrsPlan(Mockito.any(Cluster.class), Mockito.anyInt());

        ClusterDrsPlanMigrationResponse migrationResponse = Mockito.mock(ClusterDrsPlanMigrationResponse.class);

        Mockito.when(clusterDrsService.getResponseObjectForMigrations(Mockito.anyList())).thenReturn(
                List.of(migrationResponse));

        try (MockedStatic<ActionEventUtils> ignored = Mockito.mockStatic(ActionEventUtils.class)) {
            Mockito.when(ActionEventUtils.onActionEvent(Mockito.anyLong(), Mockito.anyLong(),
                    Mockito.anyLong(),
                    Mockito.anyString(), Mockito.anyString(),
                    Mockito.anyLong(), Mockito.anyString())).thenReturn(1L);

            ClusterDrsPlanResponse response = clusterDrsService.generateDrsPlan(
                    cmd);

            assertEquals(1L, response.getMigrationPlans().size());
            assertEquals(migrationResponse, response.getMigrationPlans().get(0));
        }
    }

    @Test
    public void testPoll() {
        Mockito.doNothing().when(clusterDrsService).updateOldPlanMigrations();
        Mockito.doNothing().when(clusterDrsService).processPlans();
        Mockito.doNothing().when(clusterDrsService).generateDrsPlanForAllClusters();
        Mockito.doNothing().when(clusterDrsService).cleanUpOldDrsPlans();

        GlobalLock lock = Mockito.mock(GlobalLock.class);
        Mockito.when(lock.lock(Mockito.anyInt())).thenReturn(true);

        Mockito.when(GlobalLock.getInternLock(Mockito.anyString())).thenReturn(lock);

        clusterDrsService.poll(new Date());

        Mockito.verify(clusterDrsService, Mockito.times(1)).updateOldPlanMigrations();
        Mockito.verify(clusterDrsService, Mockito.times(2)).processPlans();
        Mockito.verify(clusterDrsService, Mockito.times(1)).generateDrsPlanForAllClusters();
    }

    @Test
    public void testUpdateOldPlanMigrations() {
        ClusterDrsPlanVO drsPlan1 = Mockito.mock(ClusterDrsPlanVO.class);
        ClusterDrsPlanVO drsPlan2 = Mockito.mock(ClusterDrsPlanVO.class);

        Mockito.when(drsPlanDao.listByStatus(ClusterDrsPlan.Status.IN_PROGRESS)).thenReturn(
                List.of(drsPlan1, drsPlan2));

        Mockito.doNothing().when(clusterDrsService).updateDrsPlanMigrations(drsPlan1);
        Mockito.doNothing().when(clusterDrsService).updateDrsPlanMigrations(drsPlan2);

        clusterDrsService.updateOldPlanMigrations();

        Mockito.verify(clusterDrsService, Mockito.times(2)).updateDrsPlanMigrations(
                Mockito.any(ClusterDrsPlanVO.class));
    }

    @Test
    public void testGetBestMigration() throws ConfigurationException {
        ClusterVO cluster = Mockito.mock(ClusterVO.class);
        Mockito.when(cluster.getId()).thenReturn(1L);

        HostVO destHost = Mockito.mock(HostVO.class);
        Mockito.when(destHost.getClusterId()).thenReturn(1L);

        HostVO host = Mockito.mock(HostVO.class);
        Mockito.when(host.getId()).thenReturn(2L);

        VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
        Mockito.when(vm1.getId()).thenReturn(1L);
        Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
        Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
        Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap());

        VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class);
        Mockito.when(vm2.getId()).thenReturn(2L);
        Mockito.when(vm2.getType()).thenReturn(VirtualMachine.Type.User);
        Mockito.when(vm2.getState()).thenReturn(VirtualMachine.State.Running);
        Mockito.when(vm2.getDetails()).thenReturn(Collections.emptyMap());

        List<VirtualMachine> vmList = new ArrayList<>();
        vmList.add(vm1);
        vmList.add(vm2);

        Map<Long, List<VirtualMachine>> hostVmMap = new HashMap<>();
        hostVmMap.put(host.getId(), new ArrayList<>());
        hostVmMap.get(host.getId()).add(vm1);
        hostVmMap.get(host.getId()).add(vm2);

        Map<Long, ServiceOffering> vmIdServiceOfferingMap = new HashMap<>();

        ServiceOffering serviceOffering = Mockito.mock(ServiceOffering.class);
        for (VirtualMachine vm : vmList) {
            vmIdServiceOfferingMap.put(vm.getId(), serviceOffering);
        }

        Mockito.when(managementServer.listHostsForMigrationOfVM(vm1, 0L, 500L, null, vmList)).thenReturn(
                new Ternary<>(new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, false)));
        Mockito.when(managementServer.listHostsForMigrationOfVM(vm2, 0L, 500L, null, vmList)).thenReturn(
                new Ternary<>(new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, false)));
        Mockito.when(balancedAlgorithm.getMetrics(cluster, vm1, serviceOffering, destHost, new HashMap<>(),
                new HashMap<>(), false)).thenReturn(new Ternary<>(1.0, 0.5, 1.5));

        Mockito.when(balancedAlgorithm.getMetrics(cluster, vm2, serviceOffering, destHost, new HashMap<>(),
                new HashMap<>(), false)).thenReturn(new Ternary<>(1.0, 2.5, 1.5));

        Pair<VirtualMachine, Host> bestMigration = clusterDrsService.getBestMigration(cluster, balancedAlgorithm,
                vmList, vmIdServiceOfferingMap, new HashMap<>(), new HashMap<>());

        assertEquals(destHost, bestMigration.second());
        assertEquals(vm1, bestMigration.first());
    }

    @Test
    public void testGetBestMigrationDifferentCluster() throws ConfigurationException {
        ClusterVO cluster = Mockito.mock(ClusterVO.class);
        Mockito.when(cluster.getId()).thenReturn(1L);

        HostVO destHost = Mockito.mock(HostVO.class);
        Mockito.when(destHost.getClusterId()).thenReturn(2L);

        HostVO host = Mockito.mock(HostVO.class);
        Mockito.when(host.getId()).thenReturn(2L);

        VMInstanceVO vm1 = Mockito.mock(VMInstanceVO.class);
        Mockito.when(vm1.getId()).thenReturn(1L);
        Mockito.when(vm1.getType()).thenReturn(VirtualMachine.Type.User);
        Mockito.when(vm1.getState()).thenReturn(VirtualMachine.State.Running);
        Mockito.when(vm1.getDetails()).thenReturn(Collections.emptyMap());

        VMInstanceVO vm2 = Mockito.mock(VMInstanceVO.class);
        Mockito.when(vm2.getId()).thenReturn(2L);
        Mockito.when(vm2.getType()).thenReturn(VirtualMachine.Type.User);
        Mockito.when(vm2.getState()).thenReturn(VirtualMachine.State.Running);
        Mockito.when(vm2.getDetails()).thenReturn(Collections.emptyMap());

        List<VirtualMachine> vmList = new ArrayList<>();
        vmList.add(vm1);
        vmList.add(vm2);

        Map<Long, List<VirtualMachine>> hostVmMap = new HashMap<>();
        hostVmMap.put(host.getId(), new ArrayList<>());
        hostVmMap.get(host.getId()).add(vm1);
        hostVmMap.get(host.getId()).add(vm2);

        Map<Long, ServiceOffering> vmIdServiceOfferingMap = new HashMap<>();

        ServiceOffering serviceOffering = Mockito.mock(ServiceOffering.class);
        for (VirtualMachine vm : vmList) {
            vmIdServiceOfferingMap.put(vm.getId(), serviceOffering);
        }

        Mockito.when(managementServer.listHostsForMigrationOfVM(vm1, 0L, 500L, null, vmList)).thenReturn(
                new Ternary<>(new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, false)));
        Mockito.when(managementServer.listHostsForMigrationOfVM(vm2, 0L, 500L, null, vmList)).thenReturn(
                new Ternary<>(new Pair<>(List.of(destHost), 1), List.of(destHost), Map.of(destHost, false)));
        Pair<VirtualMachine, Host> bestMigration = clusterDrsService.getBestMigration(cluster, balancedAlgorithm,
                vmList, vmIdServiceOfferingMap, new HashMap<>(), new HashMap<>());

        assertNull(bestMigration.second());
        assertNull(bestMigration.first());
    }

    @Test
    public void testSavePlan() {
        Mockito.when(drsPlanDao.persist(Mockito.any(ClusterDrsPlanVO.class))).thenReturn(
                Mockito.mock(ClusterDrsPlanVO.class));
        Mockito.when(drsPlanMigrationDao.persist(Mockito.any(ClusterDrsPlanMigrationVO.class))).thenReturn(
                Mockito.mock(ClusterDrsPlanMigrationVO.class));

        clusterDrsService.savePlan(1L,
                List.of(new Ternary<>(Mockito.mock(VirtualMachine.class), Mockito.mock(Host.class),
                                Mockito.mock(Host.class)),
                        new Ternary<>(Mockito.mock(VirtualMachine.class), Mockito.mock(Host.class),
                                Mockito.mock(Host.class))), 1L, ClusterDrsPlan.Type.AUTOMATED,
                ClusterDrsPlan.Status.READY);

        Mockito.verify(drsPlanDao, Mockito.times(1)).persist(Mockito.any(ClusterDrsPlanVO.class));
        Mockito.verify(drsPlanMigrationDao, Mockito.times(2)).persist(Mockito.any(ClusterDrsPlanMigrationVO.class));
    }

    @Test
    public void testProcessPlans() {
        Mockito.when(drsPlanDao.listByStatus(ClusterDrsPlan.Status.READY)).thenReturn(
                List.of(Mockito.mock(ClusterDrsPlanVO.class), Mockito.mock(ClusterDrsPlanVO.class)));

        Mockito.doNothing().when(clusterDrsService).executeDrsPlan(Mockito.any(ClusterDrsPlanVO.class));

        clusterDrsService.processPlans();

        Mockito.verify(clusterDrsService, Mockito.times(2)).executeDrsPlan(Mockito.any(ClusterDrsPlanVO.class));
    }
}
