/*
 *
 * 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.qpid.proton;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.junit.Test;
import org.python.core.PyException;
import org.python.core.PyString;
import org.python.core.PySystemState;
import org.python.util.PythonInterpreter;

/**
 * Runs all the python tests, or just those that match the system property {@value #TEST_PATTERN_SYSTEM_PROPERTY}
 * if it exists.
 * Use {@value #TEST_INVOCATIONS_SYSTEM_PROPERTY} to specify the number of repetitions, or use 0
 * for unlimited repetitions.
 */
public class JythonTest
{
    public interface PathBuilder {
        public PathBuilder append(String path);
    }

    private static final Logger LOGGER = Logger.getLogger(JythonTest.class.getName());

    /* System properties expected to be defined in test/pom.xml */
    protected static final String PROTON_JYTHON_BINDING = "protonJythonBinding";
    protected static final String PROTON_JYTHON_SHIM = "protonJythonShim";
    protected static final String PROTON_JYTHON_TEST_ROOT = "protonJythonTestRoot";
    protected static final String PROTON_JYTHON_TEST_SCRIPT = "protonJythonTestScript";
    protected static final String PROTON_JYTHON_TESTS_XML_OUTPUT_DIRECTORY = "protonJythonTestXmlOutputDirectory";
    protected static final String PROTON_JYTHON_IGNORE_FILE = "protonJythonIgnoreFile";

    /** Name of the junit style xml report to be written by the python test script */
    private static final String XML_REPORT_NAME = "TEST-jython-test-results.xml";

    public static final String TEST_PATTERN_SYSTEM_PROPERTY = "proton.pythontest.pattern";
    public static final String IGNORE_FILE_SYSTEM_PROPERTY = "proton.pythontest.ignore_file";

    /** The number of times to run the test, or forever if zero */
    public static final String TEST_INVOCATIONS_SYSTEM_PROPERTY = "proton.pythontest.invocations";

    public static final String ALWAYS_COLORIZE_SYSTEM_PROPERTY = "proton.pythontest.always_colorize";

    @Test
    public void test() throws Exception
    {
        String testScript = getJythonTestScript();
        String testRoot = getJythonTestRoot();
        String xmlReportFile = getOptionalXmlReportFilename();
        String ignoreFile = getOptionalIgnoreFile();

        final PythonInterpreter interp = createInterpreterWithArgs(xmlReportFile, ignoreFile);
        PathBuilder pathBuilder = new PathBuilder() {
            @Override
            public PathBuilder append(String path) {
                interp.getSystemState().path.insert(0, new PyString(path));
                return this;
            }
        };
        extendPath(pathBuilder);

        LOGGER.info("About to call Jython test script: '" + testScript
                + "' with '" + testRoot + "' added to Jython path");

        int maxInvocations = Integer.getInteger(TEST_INVOCATIONS_SYSTEM_PROPERTY, 1);
        assertTrue("Number of invocations should be non-negative", maxInvocations >= 0);
        boolean loopForever = maxInvocations == 0;
        if(maxInvocations > 1)
        {
            LOGGER.info("Will invoke Python test " + maxInvocations + " times");
        }
        if(loopForever)
        {
            LOGGER.info("Will repeatedly invoke Python test forever");
        }
        int invocations = 1;
        while(loopForever || invocations++ <= maxInvocations)
        {
            runTestOnce(testScript, interp, invocations);
        }
    }

    protected void extendPath(PathBuilder pathBuilder) throws Exception {
        String binding = getJythonBinding();
        String shim = getJythonShim();
        String testRoot = getJythonTestRoot();
        pathBuilder.append(binding).append(shim).append(testRoot);

    }

    private void runTestOnce(String testScript, PythonInterpreter interp, int invocationsSoFar)
    {
        try
        {
            interp.execfile(testScript);
        }
        catch (PyException e)
        {
            if( e.type.toString().equals("<type 'exceptions.SystemExit'>") && e.value.toString().equals("0") )
            {
                // Build succeeded.
            }
            else
            {
                if (LOGGER.isLoggable(Level.FINE))
                {
                    LOGGER.log(Level.FINE, "Jython interpreter failed. Test failures?", e);
                }

                // This unusual code is necessary because PyException toString() contains the useful Python traceback
                // and getMessage() is usually null
                fail("Caught PyException on invocation number " + invocationsSoFar + ": " + e.toString() + " with message: " + e.getMessage());
            }
        }
    }

    private PythonInterpreter createInterpreterWithArgs(String xmlReportFile, String ignoreFile)
    {
        PySystemState systemState = new PySystemState();

        if (xmlReportFile != null)
        {
            systemState.argv.append(new PyString("--xml"));
            systemState.argv.append(new PyString(xmlReportFile));
        }

        if(ignoreFile == null)
        {
            ignoreFile = System.getProperty(IGNORE_FILE_SYSTEM_PROPERTY);
        }

        if(ignoreFile != null)
        {
            systemState.argv.append(new PyString("-I"));
            systemState.argv.append(new PyString(ignoreFile));
        }

        String testPattern = System.getProperty(TEST_PATTERN_SYSTEM_PROPERTY);
        if(testPattern != null)
        {
            systemState.argv.append(new PyString(testPattern));
        }

        if(Boolean.getBoolean(ALWAYS_COLORIZE_SYSTEM_PROPERTY))
        {
            systemState.argv.append(new PyString("--always-colorize"));
        }

        PythonInterpreter interp = new PythonInterpreter(null, systemState);
        return interp;
    }

    private String getJythonTestScript() throws FileNotFoundException
    {
        String testScriptString = getNonNullSystemProperty(PROTON_JYTHON_TEST_SCRIPT, "System property '%s' must provide the location of the python test script");
        File testScript = new File(testScriptString);
        if (!testScript.canRead())
        {
            throw new FileNotFoundException("Can't read python test script " + testScript);
        }
        return testScript.getAbsolutePath();
    }

    private String getJythonBinding() throws FileNotFoundException
    {
        String str = getNonNullSystemProperty(PROTON_JYTHON_BINDING, "System property '%s' must provide the location of the proton python binding");
        File file = new File(str);
        if (!file.isDirectory())
        {
            throw new FileNotFoundException("Binding location '" + file + "' should be a directory.");
        }
        return file.getAbsolutePath();
    }

    private String getJythonShim() throws FileNotFoundException
    {
        String str = getNonNullSystemProperty(PROTON_JYTHON_SHIM, "System property '%s' must provide the location of the proton jython shim");
        File file = new File(str);
        if (!file.isDirectory())
        {
            throw new FileNotFoundException("Shim location '" + file + "' should be a directory.");
        }
        return file.getAbsolutePath();
    }

    private String getJythonTestRoot() throws FileNotFoundException
    {
        String testRootString = getNonNullSystemProperty(PROTON_JYTHON_TEST_ROOT, "System property '%s' must provide the location of the python test root");
        File testRoot = new File(testRootString);
        if (!testRoot.isDirectory())
        {
            throw new FileNotFoundException("Test root '" + testRoot + "' should be a directory.");
        }
        return testRoot.getAbsolutePath();
    }

    private String getOptionalIgnoreFile()
    {
        String ignoreFile = System.getProperty(PROTON_JYTHON_IGNORE_FILE);

        if(ignoreFile != null)
        {
            File f = new File(ignoreFile);
            if(f.exists() && f.canRead())
            {
                return ignoreFile;
            }
            else
            {
                LOGGER.info(PROTON_JYTHON_IGNORE_FILE + " system property set to " + ignoreFile + " but this cannot be read.");
            }
        }
        return null;

    }

    private String getOptionalXmlReportFilename()
    {
        String xmlOutputDirString = System.getProperty(PROTON_JYTHON_TESTS_XML_OUTPUT_DIRECTORY);
        if (xmlOutputDirString == null)
        {
            LOGGER.info(PROTON_JYTHON_TESTS_XML_OUTPUT_DIRECTORY + " system property not set; xml output will not be written");
            return null;
        }
        else
        {
            File xmlOutputDir = new File(xmlOutputDirString);
            createXmlOutputDirectoryIfNecessary(xmlOutputDirString, xmlOutputDir);
            return new File(xmlOutputDir, XML_REPORT_NAME).getAbsolutePath();
        }
    }

    private void createXmlOutputDirectoryIfNecessary(String xmlOutputDirString, File xmlOutputDir)
    {
        if (!xmlOutputDir.isDirectory())
        {
            boolean success = xmlOutputDir.mkdirs();
            if (!success)
            {
                LOGGER.warning("Failed to create directory " + xmlOutputDir + " Thread name :" + Thread.currentThread().getName());
            }

            if (!xmlOutputDir.isDirectory())
            {
                throw new RuntimeException("Failed to create one or more directories with path " + xmlOutputDirString);
            }
        }
    }

    protected String getNonNullSystemProperty(String systemProperty, String messageWithStringFormatToken)
    {
        String testScriptString = System.getProperty(systemProperty);
        if (testScriptString == null)
        {
            String message = messageWithStringFormatToken;
            throw new IllegalStateException(String.format(message, systemProperty));
        }
        return testScriptString;
    }
}
