/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.utils.executor;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.IOException;

import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.sun.jna.Platform;

import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.common.TempFileServiceAccess;

/**
 * Tests for {@link ProcessUtils}.
 *
 * @author Tobias Rodehutskors
 */
public class ProcessUtilsTest {

    /**
     * This seems to be a common trick for windows to wait for some time. It pings localhost 100 times and waits between each ping one
     * second.
     */
    static final String WINDOWS_WAIT_COMMAND_TEMPLATE = "ping 127.0.0.1 -n %d > NUL";

//    static final String LINUX_WAIT_COMMAND = "sleep 100";
    
    static final String LINUX_WAIT_COMMAND_TEMPLATE = "sleep %d";

    static final long ONE_SECOND_AS_MILLIS = 1000;
    
    private static final String UNKOWN_PLATFORM = "Unkown platform. Currently only Windows and Linux are supported.";

    private static final int HUNDRED_SECONDS = 100;

    private File workDir;

    /**
     * Static test setup.
     */
    @BeforeClass
    public static void classSetUp() {
        TempFileServiceAccess.setupUnitTestEnvironment();
    }

    /**
     * Setup.
     * 
     * @throws IOException on I/O errors
     */
    @Before
    public void setUp() throws IOException {
        workDir = TempFileServiceAccess.getInstance().createManagedTempDir(
            "-unittest");
    }

    /**
     * Cleanup.
     * 
     * @throws IOException on I/O errors
     */
    @After
    public void tearDown() throws IOException {
        if (workDir != null) {
            TempFileServiceAccess.getInstance().disposeManagedTempDirOrFile(
                workDir);
        }
    }

    /**
     * Starts a short running process, waits for its completion and tries to retrieve the PID of the finished process.
     * 
     * @throws ExecuteException e
     * @throws IOException e
     * @throws InterruptedException e
     * @throws NoSuchFieldException e
     * @throws SecurityException e
     * @throws IllegalArgumentException e
     * @throws IllegalAccessException e
     */
    @Test
    public void testGetPidOfFinishedProcess() throws ExecuteException, IOException, InterruptedException, NoSuchFieldException,
        SecurityException, IllegalArgumentException, IllegalAccessException {

        DefaultExecutor executor = new DefaultExecutor();
        ProcessExtractor processExtractor = new ProcessExtractor();
        executor.setWatchdog(processExtractor);

        CommandLine cl = null;

        if (Platform.isWindows()) {
            cl = ProcessUtils.constructCommandLine("");
        } else if (Platform.isLinux()) {
            cl = ProcessUtils.constructCommandLine(":");
        } else {
            throw new IllegalStateException(UNKOWN_PLATFORM);
        }

        int exitCode = executor.execute(cl);
        assertEquals(0, exitCode);

        Process process = processExtractor.getProcess();
        assertNotNull(process);
        assertFalse(isRunning(process));

        ProcessUtils.getPid(process);
    }

    /**
     * 
     * Starts a long running process, retrieves the process id of the started process and kills it afterwards.
     * 
     * @throws ExecuteException e
     * @throws IOException e
     * @throws InterruptedException e
     * @throws NoSuchFieldException e
     * @throws SecurityException e
     * @throws IllegalArgumentException e
     * @throws IllegalAccessException e
     */
    @Test
    public void testGetPidOfRunningProcessAndKillProcess() throws ExecuteException, IOException, InterruptedException,
        NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {

        DefaultExecutor executor = new DefaultExecutor();
        ProcessExtractor processExtractor = new ProcessExtractor();
        executor.setWatchdog(processExtractor);

        // start a long running process
        CommandLine cl;
        if (Platform.isWindows()) {
            cl = ProcessUtils.constructCommandLine(StringUtils.format(WINDOWS_WAIT_COMMAND_TEMPLATE, HUNDRED_SECONDS));
        } else if (Platform.isLinux()) {
            cl = ProcessUtils.constructCommandLine(StringUtils.format(ProcessUtilsTest.LINUX_WAIT_COMMAND_TEMPLATE, HUNDRED_SECONDS));
        } else {
            throw new IllegalStateException(UNKOWN_PLATFORM);
        }

        DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
        executor.execute(cl, resultHandler);

        // wait for some time to make sure that the process has started
        Thread.sleep(ONE_SECOND_AS_MILLIS);

        // extract the process and its process id
        Process process = processExtractor.getProcess();
        assertNotNull(process);
        assertTrue(isRunning(process));
        int pid = ProcessUtils.getPid(process);
        assertTrue(isRunning(process));

        // kill the process tree
        assertTrue(ProcessUtils.killProcessTree(pid));

        // verify that the process has died
        resultHandler.waitFor(10 * ONE_SECOND_AS_MILLIS);
        if (!resultHandler.hasResult()) {
            fail("The process was not killed within 10 seconds");
        }

        int exitCode = resultHandler.getExitValue();
        int expectedExitCode;
        if (Platform.isWindows()) {
            expectedExitCode = ProcessUtils.WINDOWS_EXIT_CODE_SIGTERM;
        } else {
            expectedExitCode = ProcessUtils.LINUX_EXIT_CODE_SIGTERM;
        }

        assertEquals(expectedExitCode, exitCode);
        assertFalse(isRunning(process));
    }

    /**
     * Starts a process, retrieves the process id of the started process and waits for its termination. Afterwards it tries to kills it. No
     * exception should be thrown.
     * 
     * @throws ExecuteException e
     * @throws IOException e
     * @throws InterruptedException e
     * @throws NoSuchFieldException e
     * @throws SecurityException e
     * @throws IllegalArgumentException e
     * @throws IllegalAccessException e
     */
    @Test
    public void testGetPidOfRunningProcessAndKillProcessWhenFinished() throws ExecuteException, IOException, InterruptedException,
        NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {

        DefaultExecutor executor = new DefaultExecutor();
        ProcessExtractor processExtractor = new ProcessExtractor();
        executor.setWatchdog(processExtractor);

        // start a long running process
        CommandLine cl;
        if (Platform.isWindows()) {
            cl = ProcessUtils.constructCommandLine(StringUtils.format(WINDOWS_WAIT_COMMAND_TEMPLATE, 5));
        } else if (Platform.isLinux()) {
            cl = ProcessUtils.constructCommandLine(StringUtils.format(ProcessUtilsTest.LINUX_WAIT_COMMAND_TEMPLATE, 5));
        } else {
            throw new IllegalStateException(UNKOWN_PLATFORM);
        }

        DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
        executor.execute(cl, resultHandler);

        // wait for some time to make sure that the process has started
        Thread.sleep(ONE_SECOND_AS_MILLIS);

        // extract the process and its process id
        Process process = processExtractor.getProcess();
        assertNotNull(process);
        assertTrue(isRunning(process));
        int pid = ProcessUtils.getPid(process);
        assertTrue(isRunning(process));

        // wait for the process to finish
        resultHandler.waitFor(10 * ONE_SECOND_AS_MILLIS);

        // verify that the process has finished
        if (!resultHandler.hasResult()) {
            fail("The process has not finished within 10 seconds");
        }

        int exitCode = resultHandler.getExitValue();
        int expectedExitCode;
        if (Platform.isWindows()) {
            expectedExitCode = ProcessUtils.WINDOWS_EXIT_CODE_SUCCESS;
        } else {
            expectedExitCode = ProcessUtils.LINUX_EXIT_CODE_SUCCESS;
        }

        assertEquals(expectedExitCode, exitCode);
        assertFalse(isRunning(process));

        // try to kill the process tree
        assertTrue(ProcessUtils.killProcessTree(pid));
    }

    /**
     * TODO JAVA8 As soon as the code base is converted to Java 8 this call should be replaced with {@link java.lang.Process#isAlive()}.
     */
    boolean isRunning(Process process) {
        try {
            process.exitValue();
            return false;
        } catch (java.lang.IllegalThreadStateException e) {
            return true;
        }
    }
}
