/*
 *  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.poi.ss.excelant;

import static org.apache.poi.POITestCase.assertContains;
import static org.apache.poi.POITestCase.assertNotContained;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;

import org.apache.poi.POIDataSamples;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 *  JUnit test for the ExcelAnt tasks.
 *  Leverages Ant's test framework.
 */
public class TestBuildFile {

    protected Project project;
    private final StringBuilder logBuffer = new StringBuilder();

    @BeforeEach
    void setUp() {
        String filename = TestBuildFile.getDataDir() + "/../poi-excelant/src/test/resources/tests.xml";
        int logLevel = Project.MSG_DEBUG;
        logBuffer.setLength(0);
        project = new Project();
        project.init();
        project.setNewProperty("data.dir.name", getDataDir());
        File antFile = new File(System.getProperty("root"), filename);
        project.setUserProperty("ant.file", antFile.getAbsolutePath());
        project.addBuildListener(new AntTestListener(logLevel));
        ProjectHelper.configureProject(project, antFile);
    }

    /**
     * Automatically calls the target called "tearDown"
     * from the build file tested if it exits.
     * <p>
     * This allows to use Ant tasks directly in the build file
     * to clean up after each test. Note that no "setUp" target
     * is automatically called, since it's trivial to have a
     * test target depend on it.
     */
    @AfterEach
    void tearDown() {
        if (project == null) {
            /*
             * Maybe the BuildFileTest was subclassed and there is
             * no initialized project. So we could avoid getting a
             * NPE.
             * If there is an initialized project getTargets() does
             * not return null as it is initialized by an empty
             * HashSet.
             */
            return;
        }
        final String tearDown = "tearDown";
        if (project.getTargets().containsKey(tearDown)) {
            project.executeTarget(tearDown);
        }
    }

    /**
     * Assert that the given substring is in the log messages.
     */
    void assertLogContaining(String substring) {
        assertContains(getLog(), substring);
    }

    /**
     * Assert that the given substring is not in the log messages.
     */
    void assertLogNotContaining(String substring) {
        assertNotContained(getLog(), substring);
    }

    /**
     * Gets the log the BuildFileTest object.
     * Only valid if configureProject() has been called.
     *
     * @return The log value
     */
    public String getLog() {
        return logBuffer.toString();
    }

    /**
     * Executes a target we have set up
     *
     * @param targetName target to run
     */
    void executeTarget(String targetName) {
        project.executeTarget(targetName);
    }

    /**
     * Runs a target, wait for a build exception.
     *
     * @param target target to run
     * @param cause  information string to reader of report
     * @param msg    the message value of the build exception we are waiting
     *               for set to null for any build exception to be valid
     */
    void expectBuildException(String target, String cause, String msg) {
        BuildException be = assertThrows(BuildException.class, () -> executeTarget(target));
        if (msg != null) {
            assertEquals(msg, be.getMessage(), cause);
        }
    }

    public static String getDataDir() {
        String dataDirName = System.getProperty(POIDataSamples.TEST_PROPERTY);
        return dataDirName == null ? "test-data" : dataDirName;
    }

    /**
     * Our own personal build listener.
     */
    private class AntTestListener implements BuildListener {
        private final 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.
         */
        @Override
        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()
         */
        @Override
        public void buildFinished(BuildEvent event) { }

        /**
         * Fired when a target is started.
         *
         * @see BuildEvent#getTarget()
         */
        @Override
        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()
         */
        @Override
        public void targetFinished(BuildEvent event) { }

        /**
         * Fired when a task is started.
         *
         * @see BuildEvent#getTask()
         */
        @Override
        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()
         */
        @Override
        public void taskFinished(BuildEvent event) { }

        /**
         * Fired whenever a message is logged.
         *
         * @see BuildEvent#getMessage()
         * @see BuildEvent#getPriority()
         */
        @Override
        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());
            }
        }
    }

    @Test
    void testMissingFilename() {
        expectBuildException("test-nofile", "required argument not specified", "fileName attribute must be set!");
    }

    @Test
    void testFileNotFound() {
        expectBuildException("test-filenotfound", "required argument not specified",
             "Cannot load file invalid.xls. Make sure the path and file permissions are correct.");
    }

    @Test
    void testEvaluate() {
        executeTarget("test-evaluate");
        assertLogContaining("Using input file: " + TestBuildFile.getDataDir() + "/spreadsheet/excelant.xls");
        assertLogContaining("Succeeded when evaluating 'MortgageCalculator'!$B$4.");
    }

    @Test
    void testEvaluateNoDetails() {
        executeTarget("test-evaluate-nodetails");
        assertLogContaining("Using input file: " + TestBuildFile.getDataDir() + "/spreadsheet/excelant.xls");
        assertLogNotContaining("Succeeded when evaluating 'MortgageCalculator'!$B$4.");
    }

    @Test
    void testPrecision() {
        executeTarget("test-precision");

        assertLogContaining("Using input file: " + TestBuildFile.getDataDir() + "/spreadsheet/excelant.xls");
        assertLogContaining("Succeeded when evaluating 'MortgageCalculator'!$B$4.  " +
            "It evaluated to 2285.5761494145563 when the value of 2285.576149 with precision of 1.0E-4");
        assertLogContaining("Succeeded when evaluating 'MortgageCalculator'!$B$4.  " +
            "It evaluated to 2285.5761494145563 when the value of 2285.576149 with precision of 1.0E-5");
        assertLogContaining("Failed to evaluate cell 'MortgageCalculator'!$B$4.  " +
            "It evaluated to 2285.5761494145563 when the value of 2285.576149 with precision of 1.0E-10 was expected.");
        assertLogContaining("2/3 tests passed");
    }

    @Test
    void testPrecisionFail() {
        expectBuildException("test-precision-fails", "precision not matched",
             "\tFailed to evaluate cell 'MortgageCalculator'!$B$4.  It evaluated to 2285.5761494145563 " +
             "when the value of 2285.576149 with precision of 1.0E-10 was expected.");
    }

    @Test
    void testPassOnError() {
        executeTarget("test-passonerror");
        assertLogContaining("Using input file: " + TestBuildFile.getDataDir() + "/spreadsheet/excelant.xls");
        assertLogContaining("Test named failonerror failed because 1 of 0 evaluations failed to evaluate correctly.");
    }

    @Test
    void testFailOnError() {
        expectBuildException("test-failonerror", "fail on error", null);
        assertLogContaining("Using input file: " + TestBuildFile.getDataDir() + "/spreadsheet/excelant.xls");
        assertLogNotContaining("failed because 1 of 0 evaluations failed to evaluate correctly. " +
            "Failed to evaluate cell 'MortageCalculatorFunction'!$D$3");
    }

    @Test
    void testFailOnErrorNoDetails() {
        expectBuildException("test-failonerror-nodetails", "fail on error", null);
        assertLogNotContaining("Using input file: " + TestBuildFile.getDataDir() + "/spreadsheet/excelant.xls");
        assertLogNotContaining("failed because 1 of 0 evaluations failed to evaluate correctly. " +
            "Failed to evaluate cell 'MortageCalculatorFunction'!$D$3");
    }

    @Test
    void testUdf() {
        executeTarget("test-udf");
        assertLogContaining("1/1 tests passed");
    }

    @Test
    void testSetText() {
        executeTarget("test-settext");
        assertLogContaining("1/1 tests passed");
    }

    @Test
    void testAddHandler() {
        executeTarget("test-addhandler");
        assertLogContaining("Using input file: " + TestBuildFile.getDataDir() + "/spreadsheet/excelant.xls");
        assertLogContaining("Succeeded when evaluating 'MortgageCalculator'!$B$4.");

        assertNotNull(MockExcelAntWorkbookHandler.instance.workbook, "The workbook should have been passed to the handler");
        assertTrue(MockExcelAntWorkbookHandler.instance.executed, "The handler should have been executed");
    }

    @Test
    void testAddHandlerWrongClass() {
        executeTarget("test-addhandler-wrongclass");
        assertLogContaining("Using input file: " + TestBuildFile.getDataDir() + "/spreadsheet/excelant.xls");
        assertLogContaining("Succeeded when evaluating 'MortgageCalculator'!$B$4.");
    }

    @Test
    void testAddHandlerFails() {
        expectBuildException("test-addhandler-fails", "NullPointException", null);
    }

}
