package org.apache.velocity.test;

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

import junit.framework.TestCase;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.StringResourceLoader;
import org.apache.velocity.runtime.resource.util.StringResourceRepository;
import org.apache.velocity.test.misc.TestLogger;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Locale;

/**
 * Base test case that provides utility methods for
 * the rest of the tests.
 *
 * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
 * @author Nathan Bubna
 * @version $Id$
 */
public abstract class BaseTestCase extends TestCase implements TemplateTestBase
{
    protected VelocityEngine engine;
    protected VelocityContext context;
    protected boolean DEBUG = Boolean.getBoolean("test.debug");
    protected TestLogger log;
    protected String stringRepoName = "string.repo";

    public BaseTestCase(String name)
    {
        super(name);

        // if we're just running one case, then have DEBUG
        // automatically set to true
        String test = System.getProperty("test");
        if (test != null)
        {
            DEBUG = test.equals(getClass().getSimpleName());
        }
    }

    protected VelocityEngine createEngine()
    {
        VelocityEngine ret = new VelocityEngine();
        ret.setProperty(RuntimeConstants.RUNTIME_LOG_INSTANCE, log);

        // use string resource loader by default, instead of file
        ret.setProperty(RuntimeConstants.RESOURCE_LOADERS, "file,string");
        ret.addProperty("string.resource.loader.class", StringResourceLoader.class.getName());
        ret.addProperty("string.resource.loader.repository.name", stringRepoName);
        ret.addProperty("string.resource.loader.repository.static", "false");

        setUpEngine(ret);
        return ret;
    }

    @Override
    protected void setUp() throws Exception
    {
        //by default, make the engine's log output go to the test-report
        log = new TestLogger(false, false);
        engine = createEngine();
        context = new VelocityContext();
        setUpContext(context);
    }

    protected void setUpEngine(VelocityEngine engine)
    {
        // extension hook
    }

    protected void setUpContext(VelocityContext context)
    {
        // extension hook
    }

    protected StringResourceRepository getStringRepository()
    {
        StringResourceRepository repo =
            (StringResourceRepository)engine.getApplicationAttribute(stringRepoName);
        if (repo == null)
        {
            engine.init();
            repo =
                (StringResourceRepository)engine.getApplicationAttribute(stringRepoName);
        }
        return repo;
    }

    protected void addTemplate(String name, String vtl)
    {
        info("Template '"+name+"':  "+vtl);
        getStringRepository().putStringResource(name, vtl);
    }

    protected void removeTemplate(String name)
    {
        info("Removed: '"+name+"'");
        getStringRepository().removeStringResource(name);
    }

    @Override
    public void tearDown()
    {
        engine = null;
        context = null;
    }

    protected void info(String msg)
    {
        info(msg, null);
    }

    protected void info(String msg, Throwable t)
    {
        if (DEBUG)
        {
            try
            {
                if (engine == null)
                {
                    Velocity.getLog().info(msg, t);
                }
                else
                {
                    engine.getLog().info(msg, t);
                }
            }
            catch (Throwable t2)
            {
                System.out.println("Failed to log: "+msg+(t!=null?" - "+t: ""));
                System.out.println("Cause: "+t2);
                t2.printStackTrace();
            }
        }
    }

    /**
     * Compare an expected string with the given loaded template
     */
    protected void assertTmplEquals(String expected, String template)
    {
        info("Expected:  " + expected + " from '" + template + "'");

        StringWriter writer = new StringWriter();
        try
        {
            engine.mergeTemplate(template, "utf-8", context, writer);
        }
        catch (RuntimeException re)
        {
            info("RuntimeException!", re);
            throw re;
        }
        catch (Exception e)
        {
            info("Exception!", e);
            throw new RuntimeException(e);
        }

        info("Result:  " + writer.toString());
        assertEquals(expected, writer.toString());
    }

    /**
     * Ensure that a context value is as expected.
     */
    protected void assertContextValue(String key, Object expected)
    {
        info("Expected value of '"+key+"': "+expected);
        Object value = context.get(key);
        info("Result: "+value);
        assertEquals(expected, value);
    }

    /**
     * Ensure that a template renders as expected.
     */
    protected void assertEvalEquals(String expected, String vtl)
    {
        assertEvalEquals(expected, vtl, engine);
    }

    /**
     * Ensure that a template renders as expected against the provided engine.
     */
    protected void assertEvalEquals(String expected, String vtl, VelocityEngine ve)
    {
        info("Expectation: "+expected);
        assertEquals(expected, evaluate(vtl, ve));
    }

    /**
     * Ensure that the given string renders as itself when evaluated.
     */
    protected void assertSchmoo(String templateIsExpected)
    {
        assertEvalEquals(templateIsExpected, templateIsExpected);
    }

    /**
     * Ensure that an exception occurs when the string is evaluated.
     */
    protected Exception assertEvalException(String evil)
    {
        return assertEvalException(evil, null);
    }

    /**
     * Ensure that a specified type of exception occurs when evaluating the string.
     */
    protected Exception assertEvalException(String evil, Class<?> exceptionType)
    {
        return assertEvalException(evil, exceptionType, engine);
    }

    /**
     * Ensure that a specified type of exception occurs when evaluating the string with the provided engine.
     */
    protected Exception assertEvalException(String evil, Class<?> exceptionType, VelocityEngine ve)
    {
        try
        {
            if (!DEBUG)
            {
                log.off();
            }
            if (exceptionType != null)
            {
                info("Expectation: "+exceptionType.getName());
            }
            else
            {
                info("Expectation: "+Exception.class.getName());
            }
            evaluate(evil, ve);
            String msg = "Template '"+evil+"' should have thrown an exception.";
            info("Fail: "+msg);
            fail(msg);
        }
        catch (Exception e)
        {
            if (exceptionType != null && !exceptionType.isAssignableFrom(e.getClass()))
            {
                String msg = "Was expecting template '"+evil+"' to throw "+exceptionType+" not "+e;
                info("Fail: "+msg);
                fail(msg);
            }
            return e;
        }
        finally
        {
            if (!DEBUG)
            {
                log.on();
            }
        }
        return null;
    }

    /**
     * Ensure that the error message of the expected exception has the proper location info.
     */
    protected Exception assertEvalExceptionAt(String evil, String template,
                                              int line, int col)
    {
        String loc = template+"[line "+line+", column "+col+"]";
        info("Expectation: Exception at "+loc);
        Exception e = assertEvalException(evil);

        info("Result: "+e.getClass().getName()+" - "+e.getMessage());
        if (e.getMessage().indexOf(loc) < 1)
        {
            fail("Was expecting exception at "+loc+" instead of "+e.getMessage());
        }
        return e;
    }

    /**
     * Only ensure that the error message of the expected exception
     * has the proper line and column info.
     */
    protected Exception assertEvalExceptionAt(String evil, int line, int col)
    {
         return assertEvalExceptionAt(evil, "", line, col);
    }

    /**
     * Evaluate the specified String as a template and return the result as a String.
     */
    protected String evaluate(String vtl)
    {
        return evaluate(vtl, engine);
    }

    /**
     * Evaluate the specified String as a template against the provided engine and return the result as a String.
     */
    protected String evaluate(String vtl, VelocityEngine ve)
    {
        StringWriter writer = new StringWriter();
        try
        {
            info("Template: "+vtl);

            // use template as its own name, since our templates are short
            // unless it's not that short, then shorten it...
            String name = (vtl.length() <= 15) ? vtl : vtl.substring(0,15);
            ve.evaluate(context, writer, name, vtl);

            String result = writer.toString();
            info("Result: "+result);
            return result;
        }
        catch (RuntimeException re)
        {
            info("RuntimeException!", re);
            throw re;
        }
        catch (Exception e)
        {
            info("Exception!", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Concatenates the file name parts together appropriately.
     *
     * @return The full path to the file.
     */
    protected String getFileName(final String dir, final String base, final String ext)
    {
        return getFileName(dir, base, ext, false);
    }

    protected String getFileName(final String dir, final String base, final String ext, final boolean mustExist)
    {
        StringBuilder buf = new StringBuilder();
        try
        {
            File baseFile = new File(base);
            if (dir != null)
            {
                if (!baseFile.isAbsolute())
                {
                    baseFile = new File(dir, base);
                }

                buf.append(baseFile.getCanonicalPath());
            }
            else
            {
                buf.append(baseFile.getPath());
            }

            if (org.apache.commons.lang3.StringUtils.isNotEmpty(ext))
            {
                buf.append('.').append(ext);
            }

            if (mustExist)
            {
                File testFile = new File(buf.toString());

                if (!testFile.exists())
                {
                    String msg = "getFileName() result " + testFile.getPath() + " does not exist!";
                    info(msg);
                    fail(msg);
                }

                if (!testFile.isFile())
                {
                    String msg = "getFileName() result " + testFile.getPath() + " is not a file!";
                    info(msg);
                    fail(msg);
                }
            }
        }
        catch (IOException e)
        {
            fail("IO Exception while running getFileName(" + dir + ", " + base + ", "+ ext + ", " + mustExist + "): " + e.getMessage());
        }

        return buf.toString();
    }

    /**
     * Assures that the results directory exists.  If the results directory
     * cannot be created, fails the test.
     */
    protected void assureResultsDirectoryExists(String resultsDirectory)
    {
        File dir = new File(resultsDirectory);
        if (!dir.exists())
        {
            info("Template results directory ("+resultsDirectory+") does not exist");
            if (dir.mkdirs())
            {
                info("Created template results directory");
                if (DEBUG)
                {
                    info("Created template results directory: "+resultsDirectory);
                }
            }
            else
            {
                String errMsg = "Unable to create '"+resultsDirectory+"'";
                info(errMsg);
                fail(errMsg);
            }
        }
    }


    /**
     * Normalizes lines to account for platform differences.  Macs use
     * a single \r, DOS derived operating systems use \r\n, and Unix
     * uses \n.  Replace each with a single \n.
     *
     * @return source with all line terminations changed to Unix style
     */
    protected String normalizeNewlines (String source)
    {
        return source.replaceAll("\r\n?", "\n");
    }

    /**
     * Returns whether the processed template matches the
     * content of the provided comparison file.
     *
     * @return Whether the output matches the contents
     *         of the comparison file.
     *
     * @exception Exception Test failure condition.
     */
    protected boolean isMatch (String resultsDir,
                               String compareDir,
                               String baseFileName,
                               String resultExt,
                               String compareExt) throws Exception
    {
        if (DEBUG)
        {
            info("Result: "+resultsDir+'/'+baseFileName+'.'+resultExt);
        }
        String result = getFileContents(resultsDir, baseFileName, resultExt);
        return isMatch(result,compareDir,baseFileName,compareExt);
    }


    protected String getFileContents(String dir, String baseFileName, String ext)
    {
        String fileName = getFileName(dir, baseFileName, ext, true);
        return getFileContents(fileName);
    }

    protected String getFileContents(String file)
    {
        String contents = null;

        try
        {
            contents = new String(Files.readAllBytes(Paths.get(file)), StandardCharsets.UTF_8);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        return contents;
    }

    /**
     * Returns whether the processed template matches the
     * content of the provided comparison file.
     *
     * @return Whether the output matches the contents
     *         of the comparison file.
     *
     * @exception Exception Test failure condition.
     */
    protected boolean isMatch (String result,
                               String compareDir,
                               String baseFileName,
                               String compareExt) throws Exception
    {
        String compare = getFileContents(compareDir, baseFileName, compareExt);

        // normalize each wrt newline
        result = normalizeNewlines(result);
        compare = normalizeNewlines(compare);
        if (DEBUG)
        {
            info("Expection: "+compareDir+'/'+baseFileName+'.'+compareExt);
        }
        return result.equals(compare);
    }

    /**
     * Turns a base file name into a test case name.
     *
     * @param s The base file name.
     * @return  The test case name.
     */
    protected static String getTestCaseName(String s)
    {
        StringBuilder name = new StringBuilder();
        name.append(Character.toTitleCase(s.charAt(0)));
        name.append(s.substring(1, s.length()).toLowerCase(Locale.ROOT));
        return name.toString();
    }
}
