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

import java.io.File;
import java.io.OutputStream;
import java.io.PrintStream;

import org.apache.tools.ant.util.ProcessUtil;
import org.junit.rules.ExternalResource;

/**
 * Provides access for JUnit tests to execute Ant targets and access execution details (i.e logs).
 *
 * <p>Example usage:</p>
 * <pre>
 * public class MyTest {
 *
 *     \@Rule
 *     public BuildFileRule rule = new BuildFileRule();
 *
 *     \@Rule
 *     public ExpectedException thrown = ExpectedException.none();
 *
 *     \@Before
 *     public void setUp() {
 *         rule.configureProject("my/and/file.xml");
 *     }
 *
 *     \@Test
 *     public void testSuccess() {
 *         rule.executeTarget("passingTarget");
 *         assertEquals("Incorrect log message", "[taskName] Action Complete", rule.getLog());
 *     }
 *
 *     \@Test
 *     public void testException() {
 *         thrown.expect(BuildException.class);
 *         thrown.expectMessage("Could not find compiler on classpath");
 *         rule.executeTarget("failingTarget");
 *     }
 *
 * }
 * </pre>
 */
public class BuildFileRule extends ExternalResource {

    private Project project;

    private StringBuffer logBuffer;
    private StringBuffer fullLogBuffer;
    private StringBuffer outputBuffer;
    private StringBuffer errorBuffer;

    /**
     * Tidies up following a test execution. If the currently configured
     * project has a <code>tearDown</code> target then this will automatically
     * be called, otherwise this method will not perform any actions.
     */
    @Override
    protected void after()  {
        if (project == null) {
            // configureProject has not been called - nothing we can clean-up
            return;
        }
        final String tearDown = "tearDown";
        if (project.getTargets().containsKey(tearDown)) {
            project.executeTarget(tearDown);
        }
    }

    /**
     * Gets the INFO, WARNING and ERROR message from the current execution,
     * unless the logging level is set above any of these level in which case
     * the message is excluded.
     * This is only valid if configureProject() has been called.
     *
     * @return The INFO, WARN and ERROR messages in the log.
     */
    public String getLog() {
        return logBuffer.toString();
    }

    /**
     * Gets any messages that have been logged during the current execution, unless
     * the logging level has been set above the log level defined in the message.
     * <p>Only valid if configureProject() has been called.</p>
     * @return the content of the log.
     */
    public String getFullLog() {
        return fullLogBuffer.toString();
    }

    /**
     * Provides all output sent to the System.out stream during the current execution.
     * @return all output messages in a single string, normalised to have platform independent line breaks.
     */
    public String getOutput() {
        return cleanBuffer(outputBuffer);
    }

    /**
     * Provides all output sent to the System.err stream during the current execution.
     * @return all error messages in a single string, normalised to have platform independent line breaks.
     */
    public String getError() {
        return cleanBuffer(errorBuffer);
    }

    private String cleanBuffer(StringBuffer buffer) {
        StringBuilder cleanedBuffer = new StringBuilder();
        for (int i = 0; i < buffer.length(); i++) {
            char ch = buffer.charAt(i);
            if (ch != '\r') {
                cleanedBuffer.append(ch);
            }
        }
        return cleanedBuffer.toString();
    }

    /**
     * Sets up to run the named project
     *
     * @param filename name of project file to run
     */
    public void configureProject(String filename) throws BuildException {
        configureProject(filename, Project.MSG_DEBUG);
    }

    /**
     * Sets up to run the named project
     *
     * @param filename name of project file to run
     * @param logLevel int
     */
    public void configureProject(String filename, int logLevel) throws BuildException {
        logBuffer = new StringBuffer();
        fullLogBuffer = new StringBuffer();
        project = new Project();
        if (Boolean.getBoolean(MagicTestNames.TEST_BASEDIR_IGNORE)) {
            System.clearProperty(MagicNames.PROJECT_BASEDIR);
        }
        project.init();
        File antFile = new File(System.getProperty(MagicTestNames.TEST_ROOT_DIRECTORY), filename);
        project.setProperty(MagicTestNames.TEST_PROCESS_ID, ProcessUtil.getProcessId("<Process>"));
        project.setProperty(MagicTestNames.TEST_THREAD_NAME, Thread.currentThread().getName());
        project.setUserProperty(MagicNames.ANT_FILE, antFile.getAbsolutePath());
        project.addBuildListener(new AntTestListener(logLevel));
        ProjectHelper.configureProject(project, antFile);
    }

    /**
     * Executes a target in the configured Ant build file. Requires #configureProject()
     * to have been invoked before this call.
     *
     * @param targetName the target in the currently configured build file to run.
     */
    public void executeTarget(String targetName) {
        outputBuffer = new StringBuffer();
        PrintStream out = new PrintStream(new AntOutputStream(outputBuffer));
        errorBuffer = new StringBuffer();
        PrintStream err = new PrintStream(new AntOutputStream(errorBuffer));
        logBuffer = new StringBuffer();
        fullLogBuffer = new StringBuffer();

        /* we synchronize to protect our custom output streams from being overridden
         * by other tests executing targets concurrently. Ultimately this would only
         * happen if we ran a multi-threaded test executing multiple targets at once, and
         * this protection doesn't prevent a target from internally modifying the output
         * stream during a test - but at least this scenario is fairly deterministic so
         * easier to troubleshoot.
         */
        synchronized (System.out) {
            PrintStream sysOut = System.out;
            PrintStream sysErr = System.err;
            sysOut.flush();
            sysErr.flush();
            try {
                System.setOut(out);
                System.setErr(err);
                project.executeTarget(targetName);
            } finally {
                System.setOut(sysOut);
                System.setErr(sysErr);
            }
        }
    }

    /**
     * Get the project which has been configured for a test.
     *
     * @return the Project instance for this test.
     */
    public Project getProject() {
        return project;
    }


    /**
     * An output stream which saves contents to our buffer.
     */
    protected static class AntOutputStream extends OutputStream {
        private StringBuffer buffer;

        public AntOutputStream(StringBuffer buffer) {
            this.buffer = buffer;
        }

        public void write(int b) {
            buffer.append((char) b);
        }
    }

    /**
     * Our own personal build listener.
     */
    private class AntTestListener implements BuildListener {
        private int logLevel;

        /**
         * Constructs a test listener which will ignore log events
         * above the given level.
         */
        public AntTestListener(int logLevel) {
            this.logLevel = logLevel;
        }

        /**
         * Fired before any targets are started.
         */
        public void buildStarted(BuildEvent event) {
        }

        /**
         * Fired after the last target has finished. This event
         * will still be thrown if an error occurred during the build.
         *
         * @see BuildEvent#getException()
         */
        public void buildFinished(BuildEvent event) {
        }

        /**
         * Fired when a target is started.
         *
         * @see BuildEvent#getTarget()
         */
        public void targetStarted(BuildEvent event) {
        }

        /**
         * Fired when a target has finished. This event will
         * still be thrown if an error occurred during the build.
         *
         * @see BuildEvent#getException()
         */
        public void targetFinished(BuildEvent event) {
        }

        /**
         * Fired when a task is started.
         *
         * @see BuildEvent#getTask()
         */
        public void taskStarted(BuildEvent event) {
        }

        /**
         * Fired when a task has finished. This event will still
         * be throw if an error occurred during the build.
         *
         * @see BuildEvent#getException()
         */
        public void taskFinished(BuildEvent event) {
        }

        /**
         * Fired whenever a message is logged.
         *
         * @see BuildEvent#getMessage()
         * @see BuildEvent#getPriority()
         */
        public void messageLogged(BuildEvent event) {
            if (event.getPriority() > logLevel) {
                // ignore event
                return;
            }

            if (event.getPriority() == Project.MSG_INFO
                || event.getPriority() == Project.MSG_WARN
                || event.getPriority() == Project.MSG_ERR) {
                logBuffer.append(event.getMessage());
            }
            fullLogBuffer.append(event.getMessage());
        }
    }

    public File getOutputDir() {
        return new File(getProject().getProperty("output"));
    }

}
