//
// 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.hypervisor.external.provisioner;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.naming.ConfigurationException;

import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.extension.Extension;
import org.apache.cloudstack.framework.extensions.manager.ExtensionsManager;
import org.apache.logging.log4j.Logger;
import org.junit.After;
import org.junit.Assume;
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.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;

import com.cloud.agent.api.GetExternalConsoleAnswer;
import com.cloud.agent.api.GetExternalConsoleCommand;
import com.cloud.agent.api.HostVmStateReportEntry;
import com.cloud.agent.api.PrepareExternalProvisioningAnswer;
import com.cloud.agent.api.PrepareExternalProvisioningCommand;
import com.cloud.agent.api.RebootAnswer;
import com.cloud.agent.api.RebootCommand;
import com.cloud.agent.api.RunCustomActionAnswer;
import com.cloud.agent.api.RunCustomActionCommand;
import com.cloud.agent.api.StartAnswer;
import com.cloud.agent.api.StartCommand;
import com.cloud.agent.api.StopAnswer;
import com.cloud.agent.api.StopCommand;
import com.cloud.agent.api.to.VirtualMachineTO;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.hypervisor.HypervisorGuru;
import com.cloud.hypervisor.HypervisorGuruManager;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.utils.FileUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.script.Script;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.UserVmDao;

@RunWith(MockitoJUnitRunner.class)
public class ExternalPathPayloadProvisionerTest {

    @Spy
    @InjectMocks
    private ExternalPathPayloadProvisioner provisioner;

    @Mock
    private UserVmDao userVmDao;

    @Mock
    private HostDao hostDao;

    @Mock
    private HypervisorGuruManager hypervisorGuruManager;

    @Mock
    private HypervisorGuru hypervisorGuru;

    @Mock
    private Logger logger;

    @Mock
    private ExtensionsManager extensionsManager;

    private File tempDir;
    private File tempDataDir;
    private Properties testProperties;
    private File testScript;

    @Before
    public void setUp() throws IOException {
        tempDir = Files.createTempDirectory("extensions-test").toFile();
        tempDataDir = Files.createTempDirectory("extensions-data-test").toFile();

        testScript = new File(tempDir, "test-extension.sh");
        testScript.createNewFile();
        resetTestScript();

        testProperties = new Properties();
        testProperties.setProperty("extensions.deployment.mode", "developer");

        ReflectionTestUtils.setField(provisioner, "extensionsDirectory", tempDir.getAbsolutePath());
        ReflectionTestUtils.setField(provisioner, "extensionsDataDirectory", tempDataDir.getAbsolutePath());

        try (MockedStatic<PropertiesUtil> propertiesUtilMock = Mockito.mockStatic(PropertiesUtil.class)) {
            File mockPropsFile = mock(File.class);
            propertiesUtilMock.when(() -> PropertiesUtil.findConfigFile(anyString())).thenReturn(mockPropsFile);
        }
    }

    @After
    public void tearDown() {
        if (tempDir != null && tempDir.exists()) {
            deleteDirectory(tempDir);
        }
        if (tempDataDir != null && tempDataDir.exists()) {
            deleteDirectory(tempDataDir);
        }
    }

    private void deleteDirectory(File dir) {
        if (dir.isDirectory()) {
            File[] files = dir.listFiles();
            if (files != null) {
                for (File file : files) {
                    deleteDirectory(file);
                }
            }
        }
        dir.delete();
    }

    private void resetTestScript() {
        testScript.setExecutable(true);
        testScript.setReadable(true);
        testScript.setWritable(true);
    }

    @Test
    public void testLoadAccessDetails() {
        Map<String, Map<String, String>> externalDetails = new HashMap<>();
        externalDetails.put(ApiConstants.EXTENSION, Map.of("key1", "value1"));

        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(vmTO.getUuid()).thenReturn("test-uuid");
        when(vmTO.getName()).thenReturn("test-vm");

        Map<String, Object> result = provisioner.loadAccessDetails(externalDetails, vmTO);

        assertNotNull(result);
        assertEquals(externalDetails, result.get(ApiConstants.EXTERNAL_DETAILS));
        assertEquals("test-uuid", result.get(ApiConstants.VIRTUAL_MACHINE_ID));
        assertEquals("test-vm", result.get(ApiConstants.VIRTUAL_MACHINE_NAME));
        assertEquals(vmTO, result.get("cloudstack.vm.details"));
    }

    @Test
    public void testLoadAccessDetailsWithNullExternalDetails() {
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(vmTO.getUuid()).thenReturn("test-uuid");
        when(vmTO.getName()).thenReturn("test-vm");

        Map<String, Object> result = provisioner.loadAccessDetails(null, vmTO);

        assertNotNull(result);
        assertNull(result.get(ApiConstants.EXTERNAL_DETAILS));
        assertEquals("test-uuid", result.get(ApiConstants.VIRTUAL_MACHINE_ID));
        assertEquals("test-vm", result.get(ApiConstants.VIRTUAL_MACHINE_NAME));
    }

    @Test
    public void testLoadAccessDetails_WithCaller() {
        Map<String, Map<String, String>> externalDetails = new HashMap<>();
        externalDetails.put(ApiConstants.EXTENSION, Map.of("key1", "value1"));
        externalDetails.put(ApiConstants.CALLER, Map.of("key2", "value2"));
        Map<String, Object> result = provisioner.loadAccessDetails(externalDetails, null);

        assertNotNull(result);
        assertNotNull(result.get(ApiConstants.EXTERNAL_DETAILS));
        assertNotNull(((Map<String, String>) result.get(ApiConstants.EXTERNAL_DETAILS)).get(ApiConstants.EXTENSION));
        assertNotNull(result.get(ApiConstants.CALLER));
        assertNull(result.get(VmDetailConstants.CLOUDSTACK_VM_DETAILS));
    }

    @Test
    public void testGetExtensionCheckedPathValidFile() {
        String result = provisioner.getExtensionCheckedPath("test-extension", "test-extension.sh");

        assertEquals(testScript.getAbsolutePath(), result);
    }

    @Test
    public void testGetExtensionCheckedPathFileNotExists() {
        String result = provisioner.getExtensionCheckedPath("test-extension", "nonexistent.sh");

        assertNull(result);
    }

    @Test
    public void testGetExtensionCheckedPathNoExecutePermissions() {
        testScript.setExecutable(false);
        String result = provisioner.getExtensionCheckedPath("test-extension", "test-extension.sh");
        assertNull(result);
        Mockito.verify(logger).error("{} is not executable", "Entry point [" + testScript.getAbsolutePath() + "] for extension: test-extension");
    }

    @Test
    public void testGetExtensionCheckedPathNoReadPermissions() {
        testScript.setWritable(false);
        testScript.setReadable(false);
        Assume.assumeFalse("Skipping test as file can not be marked unreadable", testScript.canRead());
        String result = provisioner.getExtensionCheckedPath("test-extension", "test-extension.sh");
        assertNull(result);
        Mockito.verify(logger).error("{} is not readable", "Entry point [" + testScript.getAbsolutePath() + "] for extension: test-extension");
    }

    @Test
    public void testCheckExtensionsDirectoryValid() {
        boolean result = provisioner.checkExtensionsDirectory();
        assertTrue(result);
    }

    @Test
    public void testCheckExtensionsDirectoryInvalid() {
        ReflectionTestUtils.setField(provisioner, "extensionsDirectory", "/nonexistent/path");

        boolean result = provisioner.checkExtensionsDirectory();
        assertFalse(result);
    }

    @Test
    public void testCreateOrCheckExtensionsDataDirectory() throws ConfigurationException {
        provisioner.createOrCheckExtensionsDataDirectory();
        Mockito.verify(logger).info("Extensions data directory path: {}", tempDataDir.getAbsolutePath());
    }

    @Test(expected = ConfigurationException.class)
    public void testCreateOrCheckExtensionsDataDirectoryCreateThrowsExceptionFail() throws ConfigurationException {
        ReflectionTestUtils.setField(provisioner, "extensionsDataDirectory", "/nonexistent/path");
        try(MockedStatic<Files> filesMock = Mockito.mockStatic(Files.class)) {
            filesMock.when(() -> Files.createDirectories(any())).thenThrow(new IOException("fail"));
            provisioner.createOrCheckExtensionsDataDirectory();
        }
    }

    @Test(expected = ConfigurationException.class)
    public void testCreateOrCheckExtensionsDataDirectoryNoCreateFail() throws ConfigurationException {
        ReflectionTestUtils.setField(provisioner, "extensionsDataDirectory", "/nonexistent/path");
        try(MockedStatic<Files> filesMock = Mockito.mockStatic(Files.class)) {
            filesMock.when(() -> Files.createDirectories(any())).thenReturn(mock(Path.class));
            provisioner.createOrCheckExtensionsDataDirectory();
        }
    }

    @Test
    public void testGetExtensionPath() {
        String result = provisioner.getExtensionPath("test-extension.sh");
        String expected = tempDir.getAbsolutePath() + File.separator + "test-extension.sh";
        assertEquals(expected, result);
    }

    @Test
    public void testGetChecksumForExtensionPath() {
        String result = provisioner.getChecksumForExtensionPath("test-extension", "test-extension.sh");

        assertNotNull(result);
    }

    @Test
    public void testGetChecksumForExtensionPath_InvalidFile() {
        String result = provisioner.getChecksumForExtensionPath("test-extension", "nonexistent.sh");

        assertNull(result);
    }

    @Test
    public void testPrepareExternalProvisioning() {
        try (MockedStatic<Script> scriptMock = Mockito.mockStatic(Script.class)) {
            scriptMock.when(() -> Script.findScript(anyString(), anyString())).thenReturn(testScript.getAbsolutePath());

            PrepareExternalProvisioningCommand cmd = mock(PrepareExternalProvisioningCommand.class);
            VirtualMachineTO vmTO = new VirtualMachineTO(1, "test-vm", VirtualMachine.Type.User, 1, 1000, 256,
                    512, VirtualMachineTemplate.BootloaderType.HVM, "OS", false, false, "Pass");
            vmTO.setUuid("test-uuid");

            when(cmd.getVirtualMachineTO()).thenReturn(vmTO);
            when(cmd.getExternalDetails()).thenReturn(new HashMap<>());
            when(cmd.getWait()).thenReturn(30);

            doReturn(new Pair<>(true, "{\"nics\":[{\"uuid\":\"test-net-uuid\",\"mac\":\"00:00:00:01:02:03\"}]}")).when(provisioner)
                .executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());

            PrepareExternalProvisioningAnswer answer = provisioner.prepareExternalProvisioning(
                "host-name", "test-extension", "test-extension.sh", cmd);

            assertTrue(answer.getResult());
            assertEquals("test-net-uuid", answer.getVirtualMachineTO().getNics()[0].getNetworkUuid());
            assertEquals("00:00:00:01:02:03", answer.getVirtualMachineTO().getNics()[0].getMac());
        }
    }

    @Test
    public void testPrepareExternalProvisioning_ExtensionNotConfigured() {
        PrepareExternalProvisioningCommand cmd = mock(PrepareExternalProvisioningCommand.class);

        String extensionName = "test-extension";
        String hostName = "host-name";
        PrepareExternalProvisioningAnswer answer = provisioner.prepareExternalProvisioning(
            hostName, extensionName, "nonexistent.sh", cmd);

        assertFalse(answer.getResult());
        assertNotNull(answer);
        assertEquals(String.format("Extension: %s not configured for host: %s", extensionName, hostName), answer.getDetails());
    }

    @Test
    public void testStartInstance() {
        StartCommand cmd = mock(StartCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(vmTO.getUuid()).thenReturn("test-uuid");
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(cmd.getExternalDetails()).thenReturn(new HashMap<>());
        when(cmd.getWait()).thenReturn(30);

        doReturn(new Pair<>(true, "{\"status\": \"success\", \"message\": \"Instance started\"}")).when(provisioner)
            .executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());

        StartAnswer answer = provisioner.startInstance("host-name", "test-extension", "test-extension.sh", cmd);

        assertTrue(answer.getResult());
        Mockito.verify(logger).debug("Starting VM test-uuid on the external system");
    }

    @Test
    public void testStartInstanceDeploy() {
        StartCommand cmd = mock(StartCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(vmTO.getUuid()).thenReturn("test-uuid");
        Map<String, String> details = new HashMap<>();
        details.put("deployvm", "true");
        when(vmTO.getDetails()).thenReturn(details);
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(cmd.getExternalDetails()).thenReturn(new HashMap<>());
        when(cmd.getWait()).thenReturn(30);

        doReturn(new Pair<>(true, "{\"status\": \"success\", \"message\": \"Instance started\"}")).when(provisioner)
                .executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());

        StartAnswer answer = provisioner.startInstance("host-name", "test-extension", "test-extension.sh", cmd);

        assertTrue(answer.getResult());
        Mockito.verify(logger).debug("Deploying VM test-uuid on the external system");
    }

    @Test
    public void testStartInstanceError() {
        StartCommand cmd = mock(StartCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(vmTO.getUuid()).thenReturn("test-uuid");
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(cmd.getExternalDetails()).thenReturn(new HashMap<>());
        when(cmd.getWait()).thenReturn(30);

        doReturn(new Pair<>(false, "{\"error\": \"Instance failed to start\"}")).when(provisioner)
                .executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());

        StartAnswer answer = provisioner.startInstance("host-name", "test-extension", "test-extension.sh", cmd);

        assertFalse(answer.getResult());
        assertEquals("{\"error\": \"Instance failed to start\"}", answer.getDetails());
        Mockito.verify(logger).debug("Starting VM test-uuid on the external system failed: {\"error\": \"Instance failed to start\"}");
    }

    @Test
    public void testStopInstance() {
        StopCommand cmd = mock(StopCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(vmTO.getUuid()).thenReturn("test-uuid");
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(cmd.getExternalDetails()).thenReturn(new HashMap<>());
        when(cmd.getWait()).thenReturn(30);

        doReturn(new Pair<>(true, "success")).when(provisioner)
            .executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());

        StopAnswer answer = provisioner.stopInstance("host-name", "test-extension", "test-extension.sh", cmd);

        assertTrue(answer.getResult());
    }

    @Test
    public void testRebootInstance() {
        RebootCommand cmd = mock(RebootCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(vmTO.getUuid()).thenReturn("test-uuid");
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(cmd.getExternalDetails()).thenReturn(new HashMap<>());
        when(cmd.getWait()).thenReturn(30);

        doReturn(new Pair<>(true, "success")).when(provisioner)
            .executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());

        RebootAnswer answer = provisioner.rebootInstance("host-name", "test-extension", "test-extension.sh", cmd);

        assertTrue(answer.getResult());
    }

    @Test
    public void testExpungeInstance() {
        StopCommand cmd = mock(StopCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(vmTO.getUuid()).thenReturn("test-uuid");
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(cmd.getExternalDetails()).thenReturn(new HashMap<>());
        when(cmd.getWait()).thenReturn(30);

        doReturn(new Pair<>(true, "success")).when(provisioner)
            .executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());

        StopAnswer answer = provisioner.expungeInstance("host-name", "test-extension", "test-extension.sh", cmd);

        assertTrue(answer.getResult());
    }

    @Test
    public void testGetHostVmStateReport() {
        HostVO host = mock(HostVO.class);
        when(hostDao.findById(anyLong())).thenReturn(host);

        UserVmVO vm = mock(UserVmVO.class);
        when(vm.getUuid()).thenReturn("test-uuid");
        when(vm.getInstanceName()).thenReturn("test-instance");

        List<UserVmVO> vms = new ArrayList<>();
        vms.add(vm);
        when(userVmDao.listByHostId(anyLong())).thenReturn(vms);
        when(userVmDao.listByLastHostId(anyLong())).thenReturn(new ArrayList<>());

        when(hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External)).thenReturn(hypervisorGuru);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(hypervisorGuru.implement(any(VirtualMachineProfile.class))).thenReturn(vmTO);

        doReturn(new Pair<>(true, "PowerOn")).when(provisioner)
            .executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());

        Map<String, HostVmStateReportEntry> result = provisioner.getHostVmStateReport(1L, "test-extension", "test-extension.sh");

        assertNotNull(result);
        assertEquals(1, result.size());
        assertTrue(result.containsKey("test-instance"));
    }

    @Test
    public void testGetHostVmStateReportHostNotFound() {
        Map<String, HostVmStateReportEntry> result = provisioner.getHostVmStateReport(1L, "test-extension", "test-extension.sh");

        assertNotNull(result);
        assertTrue(result.isEmpty());
        Mockito.verify(logger).error("Host with ID: {} not found", 1L);
    }

    @Test
    public void testRunCustomAction() {
        RunCustomActionCommand cmd = mock(RunCustomActionCommand.class);
        when(cmd.getActionName()).thenReturn("test-action");
        when(cmd.getParameters()).thenReturn(new HashMap<>());
        when(cmd.getExternalDetails()).thenReturn(new HashMap<>());
        when(cmd.getWait()).thenReturn(30);

        doReturn(new Pair<>(true, "success")).when(provisioner)
            .executeExternalCommand(anyString(), anyString(), anyMap(), anyInt(), anyString(), anyString());

        RunCustomActionAnswer answer = provisioner.runCustomAction("host-name", "test-extension", "test-extension.sh", cmd);

        assertTrue(answer.getResult());
        Mockito.verify(logger).debug("Executing custom action '{}' in the external system", "test-action");
    }

    @Test
    public void testPrepareExtensionPath() throws IOException {
        try (MockedStatic<Script> scriptMock = Mockito.mockStatic(Script.class)) {
            File mockScript = new File(tempDir, "provisioner.sh");
            mockScript.createNewFile();
            scriptMock.when(() -> Script.findScript(anyString(), anyString())).thenReturn(mockScript.getAbsolutePath());

            provisioner.prepareExtensionPath("test-extension", true, "test-extension-new.sh");

            File createdFile = new File(tempDir, "test-extension-new.sh");
            assertTrue(createdFile.exists());
        }
    }

    @Test
    public void testPrepareExtensionPathNotUserDefined() {
        provisioner.prepareExtensionPath("test-extension", false, "test-extension-builtin.sh");
        File createdFile = new File(tempDir, "test-extension-builtin.sh");
        assertFalse(createdFile.exists());
    }

    @Test
    public void testPrepareExtensionPathNotBashScript() {
        provisioner.prepareExtensionPath("test-extension", true, "test-extension.txt");

        File createdFile = new File(tempDir, "test-extension.txt");
        assertFalse(createdFile.exists());
    }

    @Test
    public void testPrepareExtensionPathFileAlreadyExists() {
        File existingFile = new File(tempDir, "test-extension.sh");

        provisioner.prepareExtensionPath("test-extension", true, "test-extension.sh");

        assertTrue(existingFile.exists());
    }

    @Test
    public void testCleanupExtensionPath() throws IOException {
        String extensionDirName = Extension.getDirectoryName("test-extension");
        File extensionDir = new File(tempDir, extensionDirName);
        extensionDir.mkdirs();
        File testFile = new File(extensionDir, "test-file.txt");
        testFile.createNewFile();

        provisioner.cleanupExtensionPath("test-extension", extensionDirName + "/test-file.txt");

        assertFalse(testFile.exists());
    }

    @Test
    public void testCleanupExtensionData() throws IOException {
        File extensionDataDir = new File(tempDataDir, "test-extension");
        extensionDataDir.mkdirs();
        File testFile = new File(extensionDataDir, "test-file.txt");
        testFile.createNewFile();

        provisioner.cleanupExtensionData("test-extension", 1, true);

        assertFalse(extensionDataDir.exists());
    }

    @Test
    public void testExecuteExternalCommand() throws IOException {
        String scriptContent = "#!/bin/bash\nif grep -q '{\"test\":\"value\"}' \"$2\"; then\necho 'success'\nexit 0\nelse\necho 'fail'\nexit 1\nfi";
        Files.write(testScript.toPath(), scriptContent.getBytes());

        Map<String, Object> accessDetails = new HashMap<>();
        accessDetails.put("test", "value");

        Pair<Boolean, String> result = provisioner.executeExternalCommand(
            "test-extension", "test-action", accessDetails, 30, "Test error", testScript.getAbsolutePath());

        assertTrue(result.first());
        assertTrue(result.second().contains("success"));
    }

    @Test
    public void testExecuteExternalCommandFileNotExecutable() {
        Map<String, Object> accessDetails = new HashMap<>();

        Pair<Boolean, String> result = provisioner.executeExternalCommand(
            "test-extension", "test-action", accessDetails, 30, "Test error", "/nonexistent/path");

        assertFalse(result.first());
        assertEquals("File is not executable", result.second());
    }

    @Test
    public void deleteExtensionPayloadFile_DeletesFile_WhenActionIsTrivial() {
        String extensionName = "test-extension";
        String action = "status"; // in TRIVIAL_ACTIONS
        String payloadFileName = "/tmp/test-payload.json";

        try (MockedStatic<FileUtil> fileUtilMock = Mockito.mockStatic(FileUtil.class)) {
            provisioner.deleteExtensionPayloadFile(extensionName, action, payloadFileName);

            fileUtilMock.verify(() -> FileUtil.deletePath(payloadFileName));
            Mockito.verify(logger).trace(
                    "Deleting payload file: {} for extension: {}, action: {}, file: {}",
                    payloadFileName, extensionName, action
            );
        }
    }

    @Test
    public void deleteExtensionPayloadFile_DoesNothing_WhenActionIsNotTrivial() {
        String extensionName = "test-extension";
        String action = "start";
        String payloadFileName = "/tmp/test-payload.json";

        try (MockedStatic<FileUtil> fileUtilMock = Mockito.mockStatic(FileUtil.class)) {
            provisioner.deleteExtensionPayloadFile(extensionName, action, payloadFileName);

            fileUtilMock.verifyNoInteractions();
            Mockito.verify(logger, Mockito.never()).trace(anyString(), any(), any(), any(), any());
        }
    }

    @Test
    public void prepareExternalPayloadCreatesFileInNewDirectory() throws IOException {
        String extensionName = "test-extension";
        Map<String, Object> details = Map.of("key", "value");
        String result = provisioner.prepareExternalPayload(extensionName, details);
        assertNotNull(result);
        Path payloadFile = Paths.get(result);
        assertTrue(Files.exists(payloadFile));
        assertTrue(Files.isRegularFile(payloadFile));
        assertEquals("{\"key\":\"value\"}", Files.readString(payloadFile));
    }

    @Test
    public void prepareExternalPayloadCreatesFileInExistingDirectory() throws IOException {
        String extensionName = "test-extension";
        Map<String, Object> details = Map.of("key", "value");
        Path existingDir = Paths.get(tempDataDir.getAbsolutePath(), extensionName);
        Files.createDirectories(existingDir);
        doNothing().when(provisioner).scheduleExtensionPayloadDirectoryCleanup(extensionName);
        String result = provisioner.prepareExternalPayload(extensionName, details);
        assertNotNull(result);
        Path payloadFile = Paths.get(result);
        assertTrue(Files.exists(payloadFile));
        assertTrue(Files.isRegularFile(payloadFile));
        assertEquals("{\"key\":\"value\"}", Files.readString(payloadFile));
    }

    @Test
    public void prepareExternalPayloadSchedulesCleanupForExistingDirectory() throws IOException {
        String extensionName = "test-extension";
        Map<String, Object> details = Map.of("key", "value");
        Path existingDir = Paths.get(tempDataDir.getAbsolutePath(), extensionName);
        Files.createDirectories(existingDir);
        doNothing().when(provisioner).scheduleExtensionPayloadDirectoryCleanup(extensionName);
        provisioner.prepareExternalPayload(extensionName, details);
    }

    @Test(expected = IOException.class)
    public void prepareExternalPayloadThrowsExceptionWhenFileCannotBeWritten() throws IOException {
        String extensionName = "test-extension";
        Map<String, Object> details = Map.of("key", "value");
        Path existingDir = Paths.get(tempDataDir.getAbsolutePath(), extensionName);
        Files.createDirectories(existingDir);
        try (MockedStatic<Files> filesMock = Mockito.mockStatic(Files.class)) {
            filesMock.when(() -> Files.writeString(any(Path.class), anyString(), eq(StandardOpenOption.CREATE_NEW))).thenThrow(IOException.class);
            provisioner.prepareExternalPayload(extensionName, details);
        }
    }

    @Test
    public void getPowerStateFromStringReturnsPowerOnForValidInput() {
        VirtualMachine.PowerState result = provisioner.getPowerStateFromString("PowerOn");
        assertEquals(VirtualMachine.PowerState.PowerOn, result);
    }

    @Test
    public void getPowerStateFromStringReturnsPowerOffForValidInput() {
        VirtualMachine.PowerState result = provisioner.getPowerStateFromString("PowerOff");
        assertEquals(VirtualMachine.PowerState.PowerOff, result);
    }

    @Test
    public void getPowerStateFromStringReturnsPowerUnknownForInvalidInput() {
        VirtualMachine.PowerState result = provisioner.getPowerStateFromString("InvalidState");
        assertEquals(VirtualMachine.PowerState.PowerUnknown, result);
    }

    @Test
    public void getPowerStateFromStringReturnsPowerUnknownForBlankInput() {
        VirtualMachine.PowerState result = provisioner.getPowerStateFromString("");
        assertEquals(VirtualMachine.PowerState.PowerUnknown, result);
    }

    @Test
    public void parsePowerStateFromResponseReturnsPowerOnForValidJson() {
        String response = "{\"power_state\":\"PowerOn\"}";
        UserVmVO vm = mock(UserVmVO.class);
        VirtualMachine.PowerState result = provisioner.parsePowerStateFromResponse(vm, response);
        assertEquals(VirtualMachine.PowerState.PowerOn, result);
    }

    @Test
    public void parsePowerStateFromResponseReturnsPowerOffForValidJson() {
        String response = "{\"power_state\":\"PowerOff\"}";
        UserVmVO vm = mock(UserVmVO.class);
        VirtualMachine.PowerState result = provisioner.parsePowerStateFromResponse(vm, response);
        assertEquals(VirtualMachine.PowerState.PowerOff, result);
    }

    @Test
    public void parsePowerStateFromResponseReturnsPowerUnknownForInvalidJson() {
        String response = "{\"invalid_key\":\"value\"}";
        UserVmVO vm = mock(UserVmVO.class);
        VirtualMachine.PowerState result = provisioner.parsePowerStateFromResponse(vm, response);
        assertEquals(VirtualMachine.PowerState.PowerUnknown, result);
    }

    @Test
    public void parsePowerStateFromResponseReturnsPowerUnknownForMalformedJson() {
        String response = "{power_state:PowerOn";
        UserVmVO vm = mock(UserVmVO.class);
        VirtualMachine.PowerState result = provisioner.parsePowerStateFromResponse(vm, response);
        assertEquals(VirtualMachine.PowerState.PowerUnknown, result);
    }

    @Test
    public void parsePowerStateFromResponseReturnsPowerUnknownForBlankResponse() {
        String response = "";
        UserVmVO vm = mock(UserVmVO.class);
        VirtualMachine.PowerState result = provisioner.parsePowerStateFromResponse(vm, response);
        assertEquals(VirtualMachine.PowerState.PowerUnknown, result);
    }

    @Test
    public void parsePowerStateFromResponseReturnsPowerStateForPlainTextResponse() {
        String response = "PowerOn";
        UserVmVO vm = mock(UserVmVO.class);
        VirtualMachine.PowerState result = provisioner.parsePowerStateFromResponse(vm, response);
        assertEquals(VirtualMachine.PowerState.PowerOn, result);
    }

    @Test
    public void getVirtualMachineTOReturnsNullWhenVmIsNull() {
        VirtualMachineTO result = provisioner.getVirtualMachineTO(null);
        assertNull(result);
    }

    @Test
    public void getVirtualMachineTOReturnsValidTOWhenVmIsNotNull() {
        VirtualMachine vm = mock(VirtualMachine.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(hypervisorGuruManager.getGuru(Hypervisor.HypervisorType.External)).thenReturn(hypervisorGuru);
        when(hypervisorGuru.implement(any(VirtualMachineProfile.class))).thenReturn(vmTO);
        VirtualMachineTO result = provisioner.getVirtualMachineTO(vm);
        assertNotNull(result);
        assertEquals(vmTO, result);
        Mockito.verify(hypervisorGuruManager).getGuru(Hypervisor.HypervisorType.External);
        Mockito.verify(hypervisorGuru).implement(any(VirtualMachineProfile.class));
    }

    @Test
    public void getInstanceConsoleReturnsAnswerWhenConsoleDetailsAreValid() {
        GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(vmTO.getUuid()).thenReturn("test-uuid");

        Map<String, Object> accessDetails = new HashMap<>();
        when(provisioner.loadAccessDetails(any(), eq(vmTO))).thenReturn(accessDetails);

        String validOutput = "{\"console\":{\"host\":\"127.0.0.1\",\"port\":5900,\"password\":\"pass\",\"protocol\":\"vnc\"}}";
        doReturn(new Pair<>(true, validOutput)).when(provisioner)
                .getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());

        GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);

        assertNotNull(result);
        assertEquals("127.0.0.1", result.getHost());
        Integer port = 5900;
        assertEquals(port, result.getPort());
        assertEquals("pass", result.getPassword());
        assertEquals("vnc", result.getProtocol());
    }

    @Test
    public void getInstanceConsoleReturnsErrorWhenExtensionNotConfigured() {
        GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
        when(provisioner.getExtensionCheckedPath(anyString(), anyString())).thenReturn(null);

        String extensionName = "test-extension";
        String hostName = "host-name";
        GetExternalConsoleAnswer result = provisioner.getInstanceConsole(hostName,
                extensionName, "test-extension.sh", cmd);

        assertNotNull(result);
        assertEquals(String.format("Extension: %s not configured for host: %s", extensionName, hostName), result.getDetails());
    }

    @Test
    public void getInstanceConsoleReturnsErrorWhenExternalSystemFails() {
        GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(vmTO.getUuid()).thenReturn("test-uuid");

        doReturn(new Pair<>(false, "External system error")).when(provisioner)
                .getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());

        GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);

        assertNotNull(result);
        assertEquals("External system error", result.getDetails());
    }

    @Test
    public void getInstanceConsoleReturnsErrorWhenConsoleObjectIsMissing() {
        GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(vmTO.getUuid()).thenReturn("test-uuid");

        String invalidOutput = "{\"invalid_key\":\"value\"}";
        doReturn(new Pair<>(true, invalidOutput)).when(provisioner)
                .getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());

        GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);

        assertNotNull(result);
        assertEquals("Missing console object in output", result.getDetails());
    }

    @Test
    public void getInstanceConsoleReturnsErrorWhenRequiredFieldsAreMissing() {
        GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(vmTO.getUuid()).thenReturn("test-uuid");

        String incompleteOutput = "{\"console\":{\"host\":\"127.0.0.1\"}}";
        doReturn(new Pair<>(true, incompleteOutput)).when(provisioner)
                .getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());

        GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);

        assertNotNull(result);
        assertEquals("Missing required fields in output", result.getDetails());
    }

    @Test
    public void getInstanceConsoleReturnsErrorWhenOutputParsingFails() {
        GetExternalConsoleCommand cmd = mock(GetExternalConsoleCommand.class);
        VirtualMachineTO vmTO = mock(VirtualMachineTO.class);
        when(cmd.getVirtualMachine()).thenReturn(vmTO);
        when(vmTO.getUuid()).thenReturn("test-uuid");

        String malformedOutput = "{console:invalid}";
        doReturn(new Pair<>(true, malformedOutput)).when(provisioner)
                .getInstanceConsoleOnExternalSystem(anyString(), anyString(), anyString(), anyMap(), anyInt());

        GetExternalConsoleAnswer result = provisioner.getInstanceConsole("host-name", "test-extension", "test-extension.sh", cmd);

        assertNotNull(result);
        assertEquals("Failed to parse output", result.getDetails());
    }

    @Test
    public void getInstanceConsoleOnExternalSystemReturnsSuccessWhenCommandExecutesSuccessfully() {
        String extensionName = "test-extension";
        String filename = "test-script.sh";
        String vmUUID = "test-vm-uuid";
        Map<String, Object> accessDetails = new HashMap<>();
        int wait = 30;

        doReturn(new Pair<>(true, "Console details")).when(provisioner)
            .executeExternalCommand(eq(extensionName), eq("getconsole"), eq(accessDetails), eq(wait), anyString(), eq(filename));

        Pair<Boolean, String> result = provisioner.getInstanceConsoleOnExternalSystem(extensionName, filename, vmUUID, accessDetails, wait);

        assertTrue(result.first());
        assertEquals("Console details", result.second());
    }

    @Test
    public void getInstanceConsoleOnExternalSystemReturnsFailureWhenCommandFails() {
        String extensionName = "test-extension";
        String filename = "test-script.sh";
        String vmUUID = "test-vm-uuid";
        Map<String, Object> accessDetails = new HashMap<>();
        int wait = 30;

        doReturn(new Pair<>(false, "Failed to get console")).when(provisioner)
            .executeExternalCommand(eq(extensionName), eq("getconsole"), eq(accessDetails), eq(wait), anyString(), eq(filename));

        Pair<Boolean, String> result = provisioner.getInstanceConsoleOnExternalSystem(extensionName, filename, vmUUID, accessDetails, wait);

        assertFalse(result.first());
        assertEquals("Failed to get console", result.second());
    }

    @Test
    public void getInstanceConsoleOnExternalSystemHandlesNullResponseGracefully() {
        String extensionName = "test-extension";
        String filename = "test-script.sh";
        String vmUUID = "test-vm-uuid";
        Map<String, Object> accessDetails = new HashMap<>();
        int wait = 30;

        doReturn(null).when(provisioner)
            .executeExternalCommand(eq(extensionName), eq("getconsole"), eq(accessDetails), eq(wait), anyString(), eq(filename));

        Pair<Boolean, String> result = provisioner.getInstanceConsoleOnExternalSystem(extensionName, filename, vmUUID, accessDetails, wait);

        assertNull(result);
    }

    @Test
    public void getSanitizedJsonStringForLogReturnsNullWhenInputIsNull() {
        String result = provisioner.getSanitizedJsonStringForLog(null);
        assertNull(result);
    }

    @Test
    public void getSanitizedJsonStringForLogReturnsEmptyWhenInputIsEmpty() {
        String result = provisioner.getSanitizedJsonStringForLog("");
        assertEquals("", result);
    }

    @Test
    public void getSanitizedJsonStringForLogReturnsSameStringWhenNoPasswordField() {
        String json = "{\"key\":\"value\"}";
        String result = provisioner.getSanitizedJsonStringForLog(json);
        assertEquals(json, result);
    }

    @Test
    public void getSanitizedJsonStringForLogMasksPasswordField() {
        String json = "{\"password\":\"secret\"}";
        String result = provisioner.getSanitizedJsonStringForLog(json);
        assertEquals("{\"password\":\"****\"}", result);
    }

    @Test
    public void getSanitizedJsonStringForLogHandlesMultiplePasswordFields() {
        String json = "{\"password\":\"secret\",\"nested\":{\"password\":\"anotherSecret\"}}";
        String result = provisioner.getSanitizedJsonStringForLog(json);
        assertEquals("{\"password\":\"****\",\"nested\":{\"password\":\"****\"}}", result);
    }

    @Test
    public void getSanitizedJsonStringForLogHandlesMalformedJsonGracefully() {
        String json = "{password:\"secret\"";
        String result = provisioner.getSanitizedJsonStringForLog(json);
        assertEquals("{password:\"secret\"", result);
    }

    @Test
    public void getExtensionConfigureErrorReturnsMessageWhenHostNameIsNotBlank() {
        String result = provisioner.getExtensionConfigureError("test-extension", "test-host");
        assertEquals("Extension: test-extension not configured for host: test-host", result);
    }

    @Test
    public void getExtensionConfigureErrorReturnsMessageWhenHostNameIsBlank() {
        String result = provisioner.getExtensionConfigureError("test-extension", "");
        assertEquals("Extension: test-extension not configured", result);
    }

    @Test
    public void getExtensionConfigureErrorReturnsMessageWhenHostNameIsNull() {
        String result = provisioner.getExtensionConfigureError("test-extension", null);
        assertEquals("Extension: test-extension not configured", result);
    }
}
