/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.rhq.core.system;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

/**
 * Tests the Java-only non-native layer.
 *
 * @author John Mazzitelli
 */
@Test(groups = "native-system")
public class JavaTest {
    @AfterMethod
    @BeforeMethod
    public void terminateNativeLibrary() {
        try {
            SystemInfoFactory.shutdown();
        } catch (Throwable ignore) {
        }
    }

    /**
     * Will return <code>true</code> if this is on Windows platform - this test needs Windows because it will want to
     * execute Windows executables (albeit using Java API).
     *
     * @param  testName the name of the test that is asking to be performed
     *
     * @return <code>true</code> if the platform is the correct one that our tests need
     */
    private boolean isCorrectTestPlatform(String testName) {
        if (System.getProperty("os.name").indexOf("Windows") == -1) {
            System.out.println("~~~ [" + testName + "] Test machine is not the correct platform for this test "
                + "- cannot test the Java non-native API");
            return false;
        }

        System.out.println("Running non-native java test: " + testName);
        return true;
    }

    /**
     * Tests getting network adapter information.
     */
    // since the move to SIGAR, Java-only sysinfo doesn't support network adapters
/*
    public void testGetNetworkAdapterInfo() {
        SystemInfo sysinfo = SystemInfoFactory.createJavaSystemInfo();

        List<NetworkAdapterInfo> adapters = sysinfo.getAllNetworkAdapters();
        assert adapters != null;
        assert adapters.size() > 0 : "You can only run this on a machine with at least one detectable network interface";

        for (NetworkAdapterInfo adapter : adapters) {
            assert adapter.getName() != null : adapter;
            assert adapter.getDisplayName() != null : adapter;
            assert adapter.getDescription() != null : adapter;
            assert adapter.getUnicastAddresses() != null : adapter;
            assert adapter.getMacAddressString() == null : "How did Java find the Mac address? " + adapter;
            assert adapter.getType().equals("unknown") : "How did Java find the type? " + adapter;
            assert adapter.getOperationalStatus() == NetworkAdapterInfo.OperationState.UNKNOWN : "How did Java find the status? "
                + adapter;
            assert adapter.isDhcpEnabled() == null : "How did Java find the DHCP enabled flag? " + adapter;
            assert adapter.getDnsServers() == null : "How did Java find the DNS servers? " + adapter;
            assert adapter.getMulticastAddresses() == null : "How did Java find the multicast addresses? " + adapter;

            for (InetAddress addr : adapter.getUnicastAddresses()) {
                assert addr.getAddress() != null;
                assert addr.getHostAddress() != null;
            }

            System.out.println("Network adapter found: " + adapter);
        }
    }
*/

    /**
     * Just a test to run concurrent requests through the java layer.
     *
     * @throws Exception
     */
    public void testConcurrency() throws Exception {
        final SystemInfo sysinfo = SystemInfoFactory.createJavaSystemInfo();
//        final int hostname = sysinfo.getHostname().hashCode();
        final int osname = sysinfo.getOperatingSystemName().hashCode();
        final int osver = sysinfo.getOperatingSystemVersion().hashCode();
        final List<Throwable> errors = new Vector<Throwable>();
        final AtomicInteger count = new AtomicInteger(0);

        class TestConcurrencyTask implements Callable<Object> {
            public Object call() {
                try {
                    count.incrementAndGet();

//                    assert sysinfo.getHostname().hashCode() == hostname;
                    assert sysinfo.getOperatingSystemName().hashCode() == osname;
                    assert sysinfo.getOperatingSystemVersion().hashCode() == osver;
                } catch (Throwable t) {
                    errors.add(t);
                }

                return null;
            }
        }

        // run numTasks total tasks, with numConcurrency being the number we run concurrently at any one time
        int numTasks = 100;
        int numConcurrency = 50;

        Collection<Callable<Object>> tasks = new ArrayList<Callable<Object>>(numTasks);
        for (int i = 0; i < numTasks; i++) {
            tasks.add(new TestConcurrencyTask());
        }

        ExecutorService pool = Executors.newFixedThreadPool(numConcurrency);

        System.gc();
        long beforeFree = Runtime.getRuntime().freeMemory();
        long beforeTotal = Runtime.getRuntime().totalMemory();
        System.gc();
        List<Future<Object>> futures = pool.invokeAll(tasks, 120L, TimeUnit.SECONDS);
        for (Future<Object> future : futures) {
            future.get(30L, TimeUnit.SECONDS);
            assert future.isDone() : "A future failed to complete";
        }

        System.gc();
        long afterFree = Runtime.getRuntime().freeMemory();
        long afterTotal = Runtime.getRuntime().totalMemory();

        pool.shutdown();
        assert pool.awaitTermination(120, TimeUnit.SECONDS) : "Wow - needed to wait longer than 120 seconds to finish";
        assert count.get() == numTasks : "For some reason, we didn't run [" + numTasks + "] tasks: " + count.get();
        assert errors.size() == 0 : "Got some errors: " + errors;

        long beforeUsed = beforeTotal - beforeFree;
        long afterUsed = afterTotal - afterFree;

        System.out.println("1 native concurrency mem usage: before(free/total/used):after(free/total/used)\n"
            + beforeFree + '/' + beforeTotal + '/' + beforeUsed + ':' + afterFree + '/' + afterTotal + '/' + afterUsed);

        if (!((afterUsed - beforeUsed) < 500000)) {
            System.out.println("Hmm.. why did the concurrency test leave 500KB memory still used?");
        }

        long firstTestUsed = afterUsed;

        // run it a second time
        count.set(0);
        pool = Executors.newFixedThreadPool(numConcurrency);
        System.gc();
        beforeFree = Runtime.getRuntime().freeMemory();
        beforeTotal = Runtime.getRuntime().totalMemory();
        System.gc();
        futures = pool.invokeAll(tasks, 120L, TimeUnit.SECONDS);
        for (Future<Object> future : futures) {
            future.get(30L, TimeUnit.SECONDS);
            assert future.isDone() : "A future failed to complete";
        }

        System.gc();
        afterFree = Runtime.getRuntime().freeMemory();
        afterTotal = Runtime.getRuntime().totalMemory();

        pool.shutdown();
        assert pool.awaitTermination(120, TimeUnit.SECONDS) : "Wow - needed to wait longer than 120 seconds to finish";
        assert count.get() == numTasks : "For some reason, we didn't run [" + numTasks + "] tasks: " + count.get();
        assert errors.size() == 0 : "Got some errors: " + errors;

        beforeUsed = beforeTotal - beforeFree;
        afterUsed = afterTotal - afterFree;

        System.out.println("2 native concurrency mem usage: before(free/total/used):after(free/total/used)\n"
            + beforeFree + '/' + beforeTotal + '/' + beforeUsed + ':' + afterFree + '/' + afterTotal + '/' + afterUsed);

        if (!((afterUsed - beforeUsed) < 2500000)) {
            System.out.println("Hmm.. why did the concurrency test leave 250KB memory still used?");
        }

        long secondTestUsed = afterUsed;

        if (!(firstTestUsed >= (secondTestUsed - 50000))) {
            System.out.println("Hmm.. why did the second concurrency test leak more than 50K mem?");
        }
    }

    /**
     * Test waiting for a process and killing it if we timeout.
     */
    public void testExecuteWithWaitKill() {
        if (!isCorrectTestPlatform("testExecuteWithWaitKill")) {
            return;
        }

        SystemInfo platform = SystemInfoFactory.createJavaSystemInfo();

        ProcessExecution pe = new ProcessExecution("C:\\WINDOWS\\system32\\sol.exe");
        pe.setKillOnTimeout(true);
        pe.setWaitForCompletion(3000L);

        long before = System.currentTimeMillis();
        ProcessExecutionResults results = platform.executeProcess(pe);
        long after = System.currentTimeMillis();

        assert (after - before) >= 3000L : "Did not wait for 3000ms: Waited=" + (after - before);
        assert results.getError() == null : "Failed to exec process: " + results;
    }

    /**
     * Test executing a process.
     */
    public void testExecute() {
        if (!isCorrectTestPlatform("testExecute")) {
            return;
        }

        SystemInfo platform = SystemInfoFactory.createJavaSystemInfo();
        ProcessExecution pe = new ProcessExecution("C:\\WINDOWS\\system32\\cmd.exe");
        pe.setWorkingDirectory("C:\\");
        pe.setCaptureOutput(true);
        pe.setArguments(new String[] { "/C", "dir" });
        ProcessExecutionResults results = platform.executeProcess(pe);
        assert results.getError() == null : "Failed to exec process: " + results;
        assert results.getExitCode().intValue() == 0 : "Failed to get a 0 exit code: " + results;
        assert results.getCapturedOutput().length() > 0 : "Missing output";

        Map envVars = new HashMap();
        envVars.put("WINTESTVAR", "hello there");
        pe.setEnvironmentVariables(envVars);
        pe.setArguments(new String[] { "/C", "echo", "%WINTESTVAR%" });
        results = platform.executeProcess(pe);
        assert results.getError() == null : "Failed to exec process: " + results;
        assert results.getExitCode().intValue() == 0 : "Failed to get a 0 exit code: " + results;
        assert results.getCapturedOutput().length() > 0 : "Missing output";
        assert results.getCapturedOutput().indexOf("hello there") > -1 : "Not the expected output: "
            + results.getCapturedOutput();
    }

    public void testJBNATIVE16() {
        if (!isCorrectTestPlatform("testJBNATIVE16")) {
            return;
        }

        SystemInfo platform = SystemInfoFactory.createJavaSystemInfo();
        assert platform != null;

        boolean isNative = platform.isNative();
        String hostname = platform.getHostname();
        String operatingSystemName = platform.getOperatingSystemName();

        assert !isNative : "Should not have been native";
        assert platform instanceof JavaSystemInfo : "Should have be given a JavaSystemInfo object";
        assert hostname != null;
        assert operatingSystemName != null;

        System.out.println("is-native=" + isNative);
        System.out.println("hostname=" + hostname);
        System.out.println("os name=" + operatingSystemName);

        platform = SystemInfoFactory.createJavaSystemInfo();
        ProcessExecution pe = new ProcessExecution("C:\\WINDOWS\\system32\\cmd.exe");
        pe.setWorkingDirectory("C:\\");
        pe.setCaptureOutput(true);
        pe.setArguments(new String[] { "/C", "dir" });
        ProcessExecutionResults results = platform.executeProcess(pe);
        assert results.getError() == null : "Failed to exec process: " + results;
        assert results.getExitCode().intValue() == 0 : "Failed to get a 0 exit code: " + results;
        assert results.getCapturedOutput().length() > 0 : "Missing output";

        Map envVars = new HashMap();
        envVars.put("WINTESTVAR", "hello there");
        pe.setEnvironmentVariables(envVars);
        pe.setArguments(new String[] { "/C", "echo", "%WINTESTVAR%" });
        results = platform.executeProcess(pe);
        assert results.getError() == null : "Failed to exec process: " + results;
        assert results.getExitCode().intValue() == 0 : "Failed to get a 0 exit code: " + results;
        assert results.getCapturedOutput().length() > 0 : "Missing output";
        assert results.getCapturedOutput().indexOf("hello there") > -1 : "Not the expected output: "
            + results.getCapturedOutput();
    }

    /**
     * Just a test to run concurrent requests through the java layer that executes processes.
     *
     * @throws Exception
     */
    public void testConcurrencyExec() throws Exception {
        final SystemInfo sysinfo = SystemInfoFactory.createJavaSystemInfo();
        final List<Throwable> errors = new Vector<Throwable>();
        final int[] count = new int[] { 0 };
        final ProcessExecution start = new ProcessExecution("dummy");

        setupProgram(start);

        class TestExecConcurrencyTask implements Callable<Object> {
            public Object call() {
                try {
                    synchronized (count) {
                        count[0]++;
                    }

                    ProcessExecutionResults results = sysinfo.executeProcess(start);
                    if (results.getError() != null) {
                        throw results.getError();
                    }

                    if (start.isCaptureOutput()) {
                        assert results.getCapturedOutput() != null : "no captured output";

                        for (int i = 0; i < 10; i++) {
                            if (results.getCapturedOutput().length() > 0) {
                                break;
                            }

                            Thread.sleep(500L); // give it a chance to slurp the output
                        }

                        assert results.getCapturedOutput().length() > 0 : "captured output is empty";
                    }
                } catch (Throwable t) {
                    errors.add(t);
                }

                return null;
            }
        }

        // run numTasks total tasks, with numConcurrency being the number we run concurrently at any one time
        int numTasks = 25;
        int numConcurrency = 15;

        Collection<Callable<Object>> tasks = new ArrayList<Callable<Object>>(numTasks);
        for (int i = 0; i < numTasks; i++) {
            tasks.add(new TestExecConcurrencyTask());
        }

        ExecutorService pool = Executors.newFixedThreadPool(numConcurrency);

        System.gc();
        long beforeFree = Runtime.getRuntime().freeMemory();
        long beforeTotal = Runtime.getRuntime().totalMemory();
        System.gc();
        pool.invokeAll(tasks, 30L, TimeUnit.SECONDS);
        System.gc();
        long afterFree = Runtime.getRuntime().freeMemory();
        long afterTotal = Runtime.getRuntime().totalMemory();

        pool.shutdown();
        assert pool.awaitTermination(120, TimeUnit.SECONDS) : "Wow - needed to wait longer than 120 seconds to finish";
        assert count[0] == numTasks : "For some reason, we didn't run [" + numTasks + "] tasks: " + count[0];
        assert errors.size() == 0 : "Got some errors: " + errors;

        long beforeUsed = beforeTotal - beforeFree;
        long afterUsed = afterTotal - afterFree;

        System.out.println("1 native concurrency mem usage: before(free/total/used):after(free/total/used)\n"
            + beforeFree + '/' + beforeTotal + '/' + beforeUsed + ':' + afterFree + '/' + afterTotal + '/' + afterUsed);

        if (!((afterUsed - beforeUsed) < 500000)) {
            System.out.println("Hmm.. why did the concurrency test leave 500KB memory still used?");
        }

        long firstTestUsed = afterUsed;

        // run it a second time
        start.setCaptureOutput(true); // see if this causes any problems
        start.setWaitForCompletion(1000L);

        count[0] = 0;
        pool = Executors.newFixedThreadPool(numConcurrency);
        System.gc();
        beforeFree = Runtime.getRuntime().freeMemory();
        beforeTotal = Runtime.getRuntime().totalMemory();
        System.gc();
        pool.invokeAll(tasks, 30L, TimeUnit.SECONDS);
        System.gc();
        afterFree = Runtime.getRuntime().freeMemory();
        afterTotal = Runtime.getRuntime().totalMemory();

        pool.shutdown();
        assert pool.awaitTermination(120, TimeUnit.SECONDS) : "Wow - needed to wait longer than 120 seconds to finish";
        assert count[0] == numTasks : "For some reason, we didn't run [" + numTasks + "] tasks: " + count[0];
        assert errors.size() == 0 : "Got some errors: " + errors;

        beforeUsed = beforeTotal - beforeFree;
        afterUsed = afterTotal - afterFree;

        System.out.println("2 native concurrency mem usage: before(free/total/used):after(free/total/used)\n"
            + beforeFree + '/' + beforeTotal + '/' + beforeUsed + ':' + afterFree + '/' + afterTotal + '/' + afterUsed);

        if (!((afterUsed - beforeUsed) < 2500000)) {
            System.out.println("Hmm.. why did the concurrency test leave 250KB memory still used?");
        }

        long secondTestUsed = afterUsed;

        if (!(firstTestUsed >= (secondTestUsed - 50000))) {
            System.out.println("Hmm.. why did the second concurrency test leak more than 50K mem?");
        }
    }

    /**
     * Picks a program that can be executed on the test platform. This is not fool proof, might need to tweek this in
     * case, for example, some platforms do not have "/bin/ls".
     *
     * @param start
     */
    private void setupProgram(ProcessExecution start) {
        // just pick some short-lived executable
        if (File.separatorChar == '\\') {
            start.setExecutable("C:\\WINDOWS\\system32\\find.exe");
            start.setArguments(new String[] { "/C", "\"yaddayadda\"", "C:\\WINDOWS\\WIN.INI" });
        } else {
            start.setExecutable("/bin/ls");
            start.setArguments(new String[] { "/bin" });
        }

        start.setCaptureOutput(false);
        start.setWaitForCompletion(-1);

        return;
    }
}