/*
 * 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.
 */

package com.cloud.hypervisor.kvm.resource.wrapper;

import com.cloud.agent.api.Answer;
import com.cloud.agent.api.ScaleVmCommand;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.VirtualMachine;
import junit.framework.TestCase;
import org.apache.cloudstack.utils.bytescale.ByteScaleUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.libvirt.Connect;
import org.libvirt.Domain;
import org.libvirt.LibvirtException;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class LibvirtScaleVmCommandWrapperTest extends TestCase {

    @Spy
    LibvirtScaleVmCommandWrapper libvirtScaleVmCommandWrapperSpy = Mockito.spy(LibvirtScaleVmCommandWrapper.class);

    @Mock
    LibvirtComputingResource libvirtComputingResourceMock;

    @Mock
    ScaleVmCommand scaleVmCommandMock;

    @Mock
    LibvirtUtilitiesHelper libvirtUtilitiesHelperMock;

    @Mock
    Domain domainMock;

    @Mock
    Connect connectMock;

    @Mock
    LibvirtException libvirtException;

    @Mock
    Exception exception;

    LibvirtRequestWrapper wrapper;
    VirtualMachineTO vmTo;

    String scalingDetails;

    MockedStatic<LibvirtComputingResource> libvirtComputingResourceMocked;

    @Before
    public void init() {
        wrapper = LibvirtRequestWrapper.getInstance();
        assertNotNull(wrapper);

        vmTo = new VirtualMachineTO(1, "Test 1", VirtualMachine.Type.User, 2, 1000, 67108864, 67108864, VirtualMachineTemplate.BootloaderType.External, "Other Linux (64x)", true, true, "test123");

        long memory = ByteScaleUtils.bytesToKibibytes(vmTo.getMaxRam());
        int vcpus = vmTo.getCpus();
        int cpuShares = vcpus * vmTo.getSpeed();
        scalingDetails = String.format("%s memory to [%s KiB], CPU cores to [%s] and cpu_shares to [%s]", vmTo.toString(), memory, vcpus, cpuShares);

        libvirtComputingResourceMocked = Mockito.mockStatic(LibvirtComputingResource.class);
    }

    @Override
    @After
    public void tearDown() throws Exception {
        libvirtComputingResourceMocked.close();
    }

    @Test
    public void validateScaleVcpusRunningVcpusLessThanNewVcpusSetNewVcpu() throws LibvirtException{
        long runningVcpus = 1;
        int newVcpus = 2;

        Mockito.when(LibvirtComputingResource.countDomainRunningVcpus(Mockito.any())).thenReturn(runningVcpus);
        Mockito.doNothing().when(domainMock).setVcpus(Mockito.anyInt());

        libvirtScaleVmCommandWrapperSpy.scaleVcpus(domainMock, newVcpus, scalingDetails);

        Mockito.verify(domainMock).setVcpus(Mockito.anyInt());
    }

    @Test
    public void validateScaleVcpusRunningVcpusEqualThanNewVcpusDoNothing() throws LibvirtException{
        long runningVcpus = 2;
        int newVcpus = 2;

        Mockito.when(LibvirtComputingResource.countDomainRunningVcpus(Mockito.any())).thenReturn(runningVcpus);

        libvirtScaleVmCommandWrapperSpy.scaleVcpus(domainMock, newVcpus, scalingDetails);

        Mockito.verify(domainMock, Mockito.never()).setVcpus(Mockito.anyInt());
    }

    @Test
    public void validateScaleVcpusRunningVcpusHigherThanNewVcpusDoNothing() throws LibvirtException{
        long runningVcpus = 2;
        int newVcpus = 1;

        Mockito.when(LibvirtComputingResource.countDomainRunningVcpus(Mockito.any())).thenReturn(runningVcpus);

        libvirtScaleVmCommandWrapperSpy.scaleVcpus(domainMock, newVcpus, scalingDetails);

        Mockito.verify(domainMock, Mockito.never()).setVcpus(Mockito.anyInt());
    }

    @Test (expected = LibvirtException.class)
    public void validateScaleVcpusSetVcpusThrowLibvirtException() throws LibvirtException{
        long runningVcpus = 1;
        int newVcpus = 2;

        Mockito.when(LibvirtComputingResource.countDomainRunningVcpus(Mockito.any())).thenReturn(runningVcpus);
        Mockito.doThrow(LibvirtException.class).when(domainMock).setVcpus(Mockito.anyInt());

        libvirtScaleVmCommandWrapperSpy.scaleVcpus(domainMock, newVcpus, scalingDetails);

        Mockito.verify(domainMock, Mockito.never()).setVcpus(Mockito.anyInt());
    }

    @Test
    public void validateScaleMemoryMemoryLessThanZeroDoNothing() throws LibvirtException {
        long currentMemory = 1l;
        long newMemory = 0l;

        Mockito.when(LibvirtComputingResource.getDomainMemory(Mockito.any())).thenReturn(currentMemory);

        libvirtScaleVmCommandWrapperSpy.scaleMemory(domainMock, newMemory, scalingDetails);

        Mockito.verify(domainMock, Mockito.never()).getXMLDesc(Mockito.anyInt());
        Mockito.verify(domainMock, Mockito.never()).attachDevice(Mockito.anyString());
    }

    @Test
    public void validateScaleMemoryMemoryEqualToZeroDoNothing() throws LibvirtException {
        long currentMemory = 1l;
        long newMemory = 1l;

        Mockito.when(LibvirtComputingResource.getDomainMemory(Mockito.any())).thenReturn(currentMemory);

        libvirtScaleVmCommandWrapperSpy.scaleMemory(domainMock, newMemory, scalingDetails);

        Mockito.verify(domainMock, Mockito.never()).getXMLDesc(Mockito.anyInt());
        Mockito.verify(domainMock, Mockito.never()).attachDevice(Mockito.anyString());
    }

    @Test (expected = CloudRuntimeException.class)
    public void validateScaleMemoryDomainXmlDoesNotContainsMaxMemory() throws LibvirtException {
        long currentMemory = 1l;
        long newMemory = 2l;

        Mockito.when(LibvirtComputingResource.getDomainMemory(Mockito.any())).thenReturn(currentMemory);
        Mockito.doReturn("").when(domainMock).getXMLDesc(Mockito.anyInt());

        libvirtScaleVmCommandWrapperSpy.scaleMemory(domainMock, newMemory, scalingDetails);

        Mockito.verify(domainMock).getXMLDesc(Mockito.anyInt());
        Mockito.verify(domainMock, Mockito.never()).attachDevice(Mockito.anyString());
    }

    @Test (expected = LibvirtException.class)
    public void validateScaleMemoryAttachDeviceThrowsLibvirtException() throws LibvirtException {
        long currentMemory = 1l;
        long newMemory = 2l;

        Mockito.when(LibvirtComputingResource.getDomainMemory(Mockito.any())).thenReturn(currentMemory);
        Mockito.doReturn("<maxMemory slots='16' unit='KiB'>").when(domainMock).getXMLDesc(Mockito.anyInt());
        Mockito.doThrow(LibvirtException.class).when(domainMock).attachDevice(Mockito.anyString());

        libvirtScaleVmCommandWrapperSpy.scaleMemory(domainMock, newMemory, scalingDetails);

        Mockito.verify(domainMock).getXMLDesc(Mockito.anyInt());
        Mockito.verify(domainMock).attachDevice(Mockito.anyString());
    }

    @Test
    public void validateScaleMemory() throws LibvirtException {
        long currentMemory = 1l;
        long newMemory = 2l;

        Mockito.when(LibvirtComputingResource.getDomainMemory(Mockito.any())).thenReturn(currentMemory);
        Mockito.doReturn("<maxMemory slots='16' unit='KiB'>").when(domainMock).getXMLDesc(Mockito.anyInt());
        Mockito.doNothing().when(domainMock).attachDevice(Mockito.anyString());

        libvirtScaleVmCommandWrapperSpy.scaleMemory(domainMock, newMemory, scalingDetails);

        Mockito.verify(domainMock).getXMLDesc(Mockito.anyInt());
        Mockito.verify(domainMock).attachDevice(Mockito.anyString());
    }

    @Test
    public void validateExecuteHandleLibvirtException() throws LibvirtException {
        String errorMessage = "";
        int shares = vmTo.getCpus() * vmTo.getSpeed();

        Mockito.doReturn(vmTo).when(scaleVmCommandMock).getVirtualMachine();
        Mockito.doReturn(libvirtUtilitiesHelperMock).when(libvirtComputingResourceMock).getLibvirtUtilitiesHelper();
        Mockito.doReturn(shares).when(libvirtComputingResourceMock).calculateCpuShares(vmTo);
        Mockito.doThrow(libvirtException).when(libvirtUtilitiesHelperMock).getConnectionByVmName(Mockito.anyString());
        Mockito.doReturn(errorMessage).when(libvirtException).getMessage();

        Answer answer = libvirtScaleVmCommandWrapperSpy.execute(scaleVmCommandMock, libvirtComputingResourceMock);

        String details = String.format("Unable to scale %s due to [%s].", scalingDetails, errorMessage);
        assertFalse(answer.getResult());
        assertEquals(details, answer.getDetails());
    }

    @Test
    public void validateExecuteSuccessfully() throws LibvirtException {
        int shares = vmTo.getCpus() * vmTo.getSpeed();

        Mockito.doReturn(vmTo).when(scaleVmCommandMock).getVirtualMachine();
        Mockito.doReturn(libvirtUtilitiesHelperMock).when(libvirtComputingResourceMock).getLibvirtUtilitiesHelper();
        Mockito.doReturn(connectMock).when(libvirtUtilitiesHelperMock).getConnectionByVmName(Mockito.anyString());
        Mockito.doReturn(shares).when(libvirtComputingResourceMock).calculateCpuShares(vmTo);
        Mockito.doReturn(domainMock).when(connectMock).domainLookupByName(Mockito.anyString());
        Mockito.doNothing().when(libvirtScaleVmCommandWrapperSpy).scaleMemory(Mockito.any(), Mockito.anyLong(), Mockito.anyString());
        Mockito.doNothing().when(libvirtScaleVmCommandWrapperSpy).scaleVcpus(Mockito.any(), Mockito.anyInt(), Mockito.anyString());

        Answer answer = libvirtScaleVmCommandWrapperSpy.execute(scaleVmCommandMock, libvirtComputingResourceMock);

        String details = String.format("Successfully scaled %s.", scalingDetails);
        assertTrue(answer.getResult());
        assertEquals(details, answer.getDetails());
    }

    @Test(expected = Exception.class)
    public void validateExecuteThrowAnyOtherException() {
        Mockito.doThrow(Exception.class).when(libvirtComputingResourceMock).getLibvirtUtilitiesHelper();

        libvirtScaleVmCommandWrapperSpy.execute(scaleVmCommandMock, libvirtComputingResourceMock);
    }

    @Test
    public void updateCpuSharesTestOldSharesLessThanNewSharesUpdateShares() throws LibvirtException {
        int oldShares = 2000;
        int newShares = 3000;

        Mockito.when(LibvirtComputingResource.getCpuShares(Mockito.any())).thenReturn(oldShares);
        libvirtScaleVmCommandWrapperSpy.updateCpuShares(domainMock, newShares);

        libvirtComputingResourceMocked.verify(() -> LibvirtComputingResource.setCpuShares(domainMock, newShares),
                Mockito.times(1));
        ;
    }

    @Test
    public void updateCpuSharesTestOldSharesHigherThanNewSharesDoNothing() throws LibvirtException {
        int oldShares = 3000;
        int newShares = 2000;

        Mockito.when(LibvirtComputingResource.getCpuShares(Mockito.any())).thenReturn(oldShares);
        libvirtScaleVmCommandWrapperSpy.updateCpuShares(domainMock, newShares);


        libvirtComputingResourceMocked.verify(() -> LibvirtComputingResource.setCpuShares(domainMock, newShares),
                Mockito.times(0));
    }

    @Test
    public void updateCpuSharesTestOldSharesEqualsNewSharesDoNothing() throws LibvirtException {
        int oldShares = 2000;
        int newShares = 2000;

        Mockito.when(LibvirtComputingResource.getCpuShares(Mockito.any())).thenReturn(oldShares);
        libvirtScaleVmCommandWrapperSpy.updateCpuShares(domainMock, newShares);

        libvirtComputingResourceMocked.verify(() -> LibvirtComputingResource.setCpuShares(domainMock, newShares),
                Mockito.times(0));
    }
}
