/*
 *  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
 *
 *      https://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.tools.ant.taskdefs;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildFileRule;
import org.apache.tools.ant.input.DefaultInputHandler;
import org.apache.tools.ant.taskdefs.condition.JavaVersion;
import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.TeeOutputStream;
import org.junit.Assume;
import org.junit.AssumptionViolatedException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;

/**
 * stress out java task
 * */
public class JavaTest {

    @Rule
    public BuildFileRule buildRule = new BuildFileRule();

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    private static final int TIME_TO_WAIT = 1;
    // wait 1 second extra to allow for java to start ...
    // this time was OK on a Win NT machine and on nagoya
    private static final int SECURITY_MARGIN = 2000;

    /** Utilities used for file operations */
    private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();

    private boolean runFatalTests = false;


    /**
     * configure the project.
     * if the property junit.run.fatal.tests is set we run
     * the fatal tests
     */
    @Before
    public void setUp() {
        buildRule.configureProject("src/etc/testcases/taskdefs/java.xml");
        buildRule.executeTarget("setUp");

        //final String propname="tests-classpath.value";
        //String testClasspath=System.getProperty(propname);
        //System.out.println("Test cp=" + testClasspath);
        String runFatal = System.getProperty("junit.run.fatal.tests");
        if (runFatal != null) {
            runFatalTests = true;
        }
    }

    @Test
    public void testNoJarNoClassname() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Classname must not be null.");
        buildRule.executeTarget("testNoJarNoClassname");
    }

    @Test
    public void testJarNoFork() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Cannot execute a jar in non-forked mode. Please set fork='true'. ");
        buildRule.executeTarget("testJarNoFork");
    }

    @Test
    public void testJarAndClassName() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Cannot use combination of ");
        buildRule.executeTarget("testJarAndClassName");
    }

    @Test
    public void testClassnameAndJar() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Cannot use combination of ");
        buildRule.executeTarget("testClassnameAndJar");
    }

    @Test
    public void testJarAndModule() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Cannot use combination of ");
        buildRule.executeTarget("testJarAndModule");
    }

    @Test
    public void testModuleAndJar() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Cannot use combination of ");
        buildRule.executeTarget("testModuleAndJar");
    }

    @Test
    public void testClassnameAndModule() {
        buildRule.executeTarget("testClassnameAndModule");
    }

    @Test
    public void testModuleAndClassname() {
        buildRule.executeTarget("testModuleAndClassname");
    }

    @Test
    public void testModule() {
        buildRule.executeTarget("testModule");
    }

    @Test
    public void testModuleCommandLine() {
        final String moduleName = "TestModule"; //NOI18N
        final String arg = "appArg";    //NOI18N
        final Java java = new Java();
        java.setFork(true);
        java.setModule(moduleName);
        java.setJvmargs("-Xmx128M");
        java.setArgs(arg);
        final String[] cmdLine = java.getCommandLine().getCommandline();
        assertNotNull("Has command line.", cmdLine);
        assertEquals("Command line should have 5 elements", 5, cmdLine.length);
        assertEquals("Last command line element should be java argument: " + arg,
                arg,
                cmdLine[cmdLine.length - 1]);
        assertEquals("The command line element at index 3 should be module name: " + moduleName,
                moduleName,
                cmdLine[cmdLine.length - 2]);
        assertEquals("The command line element at index 2 should be -m",
                "-m",
                cmdLine[cmdLine.length - 3]);
    }

    @Test
    public void testModuleAndClassnameCommandLine() {
        final String moduleName = "TestModule"; //NOI18N
        final String className = "org.apache.Test"; //NOI18N
        final String moduleClassPair = String.format("%s/%s", moduleName, className);
        final String arg = "appArg";    //NOI18N
        final Java java = new Java();
        java.setFork(true);
        java.setModule(moduleName);
        java.setClassname(className);
        java.setJvmargs("-Xmx128M");    //NOI18N
        java.setArgs(arg);
        final String[] cmdLine = java.getCommandLine().getCommandline();
        assertNotNull("Has command line.", cmdLine);
        assertEquals("Command line should have 5 elements", 5, cmdLine.length);
        assertEquals("Last command line element should be java argument: " + arg,
                arg,
                cmdLine[cmdLine.length - 1]);
        assertEquals("The command line element at index 3 should be module class pair: " + moduleClassPair,
                moduleClassPair,
                cmdLine[cmdLine.length - 2]);
        assertEquals("The command line element at index 2 should be -m",
                "-m",
                cmdLine[cmdLine.length - 3]);
    }

    @Test
    public void testRun() {
        buildRule.executeTarget("testRun");
    }

    /** this test fails but we ignore the return value;
     *  we verify that failure only matters when failonerror is set
     */
    @Test
    public void testRunFail() {
        assumeTrue("Fatal tests have not been set to run", runFatalTests);
        buildRule.executeTarget("testRunFail");
    }

    @Test
    public void testRunFailFoe() {
        assumeTrue("Fatal tests have not been set to run", runFatalTests);
        thrown.expect(BuildException.class);
        thrown.expectMessage("Java returned:");
        buildRule.executeTarget("testRunFailFoe");
    }

    @Test
    public void testRunFailFoeFork() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Java returned:");
        buildRule.executeTarget("testRunFailFoeFork");
    }

    @Test
    public void testExcepting() {
        buildRule.executeTarget("testExcepting");
        assertThat(buildRule.getLog(), containsString("Exception raised inside called program"));
    }

    @Test
    public void testExceptingFork() {
        buildRule.executeTarget("testExceptingFork");
        assertThat(buildRule.getLog(), containsString("Java Result:"));
    }

    @Test
    public void testExceptingFoe() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Exception raised inside called program");
        buildRule.executeTarget("testExceptingFoe");
    }

    @Test
    public void testExceptingFoeFork() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Java returned:");
        buildRule.executeTarget("testExceptingFoeFork");
    }

    @Test
    public void testResultPropertyZero() {
        buildRule.executeTarget("testResultPropertyZero");
        assertEquals("0", buildRule.getProject().getProperty("exitcode"));
    }

    @Test
    public void testResultPropertyNonZero() {
        buildRule.executeTarget("testResultPropertyNonZero");
        assertEquals("2", buildRule.getProject().getProperty("exitcode"));
    }

    @Test
    public void testResultPropertyZeroNoFork() {
        buildRule.executeTarget("testResultPropertyZeroNoFork");
        assertEquals("0", buildRule.getProject().getProperty("exitcode"));
    }

    @Test
    public void testResultPropertyNonZeroNoFork() {
        buildRule.executeTarget("testResultPropertyNonZeroNoFork");
         assertEquals("-1", buildRule.getProject().getProperty("exitcode"));
     }

    @Test
    public void testRunFailWithFailOnError() {
        thrown.expect(BuildException.class);
        thrown.expectMessage("Java returned:");
        buildRule.executeTarget("testRunFailWithFailOnError");
    }

    @Test
    public void testRunSuccessWithFailOnError() {
        buildRule.executeTarget("testRunSuccessWithFailOnError");
    }

    @Test
    public void testSpawn() throws InterruptedException {
        File logFile = FILE_UTILS.createTempFile("spawn", "log",
                new File(buildRule.getProject().getProperty("output")), false, false);
        // this is guaranteed by FileUtils#createTempFile
        assertFalse("log file not existing", logFile.exists());
        buildRule.getProject().setProperty("logFile", logFile.getAbsolutePath());
        buildRule.getProject().setProperty("timeToWait", Long.toString(TIME_TO_WAIT));
        buildRule.getProject().executeTarget("testSpawn");

        Thread.sleep(TIME_TO_WAIT * 1000 + SECURITY_MARGIN);

        // let's be nice with the next generation of developers
        if (!logFile.exists()) {
            System.out.println("suggestion: increase the constant"
            + " SECURITY_MARGIN to give more time for java to start.");
        }
        assertTrue("log file exists", logFile.exists());
    }

    @Test
    public void testRedirect1() {
        buildRule.executeTarget("redirect1");
    }

    @Test
    public void testRedirect2() {
        buildRule.executeTarget("redirect2");
    }

    @Test
    public void testRedirect3() {
        buildRule.executeTarget("redirect3");
    }

    @Test
    public void testRedirector1() {
        buildRule.executeTarget("redirector1");
    }

    @Test
    public void testRedirector2() {
        buildRule.executeTarget("redirector2");
    }

    @Test
    public void testReleasedInput() throws Exception {
        PipedOutputStream out = new PipedOutputStream();
        final PipedInputStream in = new PipedInputStream(out);
        buildRule.getProject().setInputHandler(new DefaultInputHandler() {
            protected InputStream getInputStream() {
                return in;
            }
        });
        buildRule.getProject().setDefaultInputStream(in);

        Java java = new Java();
        java.setProject(buildRule.getProject());
        java.setClassname("org.apache.tools.ant.Main");
        java.setArgs("-version");
        java.setFork(true);
        // note: due to the missing classpath it will fail, but the input stream
        // reader will be read
        java.execute();

        Thread inputThread = new Thread(() -> {
            Input input = new Input();
            input.setProject(buildRule.getProject());
            input.setAddproperty("input.value");
            input.execute();
        });
        inputThread.start();

        // wait a little bit for the task to wait for input
        Thread.sleep(100);

        // write some stuff in the input stream to be caught by the input task
        out.write("foo\n".getBytes());
        out.flush();
        try {
            out.write("bar\n".getBytes());
            out.flush();
        } catch (IOException x) {
            // "Pipe closed" on XP; ignore?
        }

        inputThread.join(2000);

        assertEquals("foo", buildRule.getProject().getProperty("input.value"));
    }

    @Test
    public void testFlushedInput() throws Exception {
        final PipedOutputStream out = new PipedOutputStream();
        final PipedInputStream in = new PipedInputStream(out);
        buildRule.getProject().setInputHandler(new DefaultInputHandler() {
            protected InputStream getInputStream() {
                return in;
            }
        });
        buildRule.getProject().setDefaultInputStream(in);

        final boolean[] timeout = new boolean[1];
        timeout[0] = false;

        Thread writingThread = new Thread(() -> {
            try {
                // wait a little bit to have the target executed
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new AssumptionViolatedException("Thread interrupted", e);
            }
            try {
                out.write("foo-FlushedInput\n".getBytes());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
        writingThread.setDaemon(true);

        writingThread.start();
        buildRule.executeTarget("flushedInput");
    }

    /**
     * Test that the Java single file source program feature introduced in Java 11 works fine
     *
     * @throws Exception
     */
    @Test
    public void testSimpleSourceFile() {
        requireJava11();
        buildRule.executeTarget("simpleSourceFile");
    }

    /**
     * Test that the sourcefile option of the Java task can only be run when fork attribute is set
     *
     * @throws Exception
     */
    @Test
    public void testSourceFileRequiresFork() {
        requireJava11();
        thrown.expect(BuildException.class);
        thrown.expectMessage("Cannot execute sourcefile in non-forked mode. Please set fork='true'");
        buildRule.executeTarget("sourceFileRequiresFork");
    }

    /**
     * Tests that the sourcefile attribute and the classname attribute of the Java task cannot be used
     * together
     *
     * @throws Exception
     */
    @Test
    public void testSourceFileCantUseClassname() {
        requireJava11();
        thrown.expect(BuildException.class);
        thrown.expectMessage("Cannot use 'sourcefile' in combination with");
        buildRule.executeTarget("sourceFileCantUseClassname");
    }

    /**
     * Tests that the sourcefile attribute and the jar attribute of the Java task cannot be used
     * together
     *
     * @throws Exception
     */
    @Test
    public void testSourceFileCantUseJar() {
        requireJava11();
        thrown.expect(BuildException.class);
        thrown.expectMessage("Cannot use 'sourcefile' in combination with");
        buildRule.executeTarget("sourceFileCantUseJar");
    }

    /**
     * Tests that the sourcefile attribute and the module attribute of the Java task cannot be used
     * together
     *
     * @throws Exception
     */
    @Test
    public void testSourceFileCantUseModule() {
        requireJava11();
        thrown.expect(BuildException.class);
        thrown.expectMessage("Cannot use 'sourcefile' in combination with");
        buildRule.executeTarget("sourceFileCantUseModule");
    }

    private static void requireJava11() {
        final JavaVersion javaVersion = new JavaVersion();
        javaVersion.setAtLeast("11");
        Assume.assumeTrue("Skipping test which requires a minimum of Java 11 runtime", javaVersion.eval());
    }

    /**
     * entry point class with no dependencies other
     * than normal JRE runtime
     */
    public static class EntryPoint {

    /**
     * this entry point is used by the java.xml tests to
     * generate failure strings to handle
     * argv[0] = exit code (optional)
     * argv[1] = string to print to System.out (optional)
     * argv[1] = string to print to System.err (optional)
     */
        public static void main(String[] argv) {
            int exitCode = 0;
            if (argv.length > 0) {
                try {
                    exitCode = Integer.parseInt(argv[0]);
                } catch (NumberFormatException nfe) {
                    exitCode = -1;
                }
            }
            if (argv.length > 1) {
                System.out.println(argv[1]);
            }
            if (argv.length > 2) {
                System.err.println(argv[2]);
            }
            if (exitCode != 0) {
                System.exit(exitCode);
            }
        }
    }

    /**
     * entry point class with no dependencies other
     * than normal JRE runtime
     */
    public static class ExceptingEntryPoint {

        /**
         * throw a run time exception which does not need
         * to be in the signature of the entry point
         */
        public static void main(String[] argv) {
            throw new NullPointerException("Exception raised inside called program");
        }
    }
    /**
     * test class for spawn
     */
    public static class SpawnEntryPoint {
        public static void main(String[] argv) throws InterruptedException {
            int sleepTime = 10;
            String logFile = "spawn.log";
            if (argv.length >= 1) {
                sleepTime = Integer.parseInt(argv[0]);
            }
            if (argv.length >= 2) {
                logFile = argv[1];
            }
            Thread.sleep(sleepTime * 1000);

            File dest = new File(logFile);
            try (OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(dest))) {
                out.write("bye bye\n");
            } catch (Exception ex) {
            }
        }
    }

    /**
     * entry point class to pipe System.in to the specified stream:
     * "out", "err", or "both".  If none specified, swallow the input.
     */
    public static class PipeEntryPoint {

        /**
         * pipe input to specified output
         */
        public static void main(String[] args) throws InterruptedException {
            OutputStream os = null;
            if (args.length > 0) {
                if ("out".equalsIgnoreCase(args[0])) {
                    os = System.out;
                } else if ("err".equalsIgnoreCase(args[0])) {
                    os = System.err;
                } else if ("both".equalsIgnoreCase(args[0])) {
                    os = new TeeOutputStream(System.out, System.err);
                }
            }
            if (os != null) {
                Thread t = new Thread(new StreamPumper(System.in, os, true));
                t.setName("PipeEntryPoint " + args[0]);
                t.start();
                t.join();
            }
        }
    }

    public static class ReadPoint {
        public static void main(String[] args) throws IOException {
            String line = new BufferedReader(new InputStreamReader(System.in)).readLine();
            System.out.println(line);
        }
    }

}
