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 java.io.File;
import java.io.IOException;
import java.io.StringWriter;

import junit.framework.TestCase;

import org.apache.oro.text.perl.Perl5Util;
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.TestLogChute;
import org.apache.velocity.util.StringUtils;

/**
 * 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: BaseTestCase.java 898032 2010-01-11 19:51:03Z nbubna $
 */
public abstract class BaseTestCase extends TestCase implements TemplateTestBase
{
    protected VelocityEngine engine;
    protected VelocityContext context;
    protected boolean DEBUG = false;
    protected TestLogChute 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 testcase = System.getProperty("testcase");
        if (testcase != null)
        {
            DEBUG = testcase.equals(getClass().getName());
        }
    }

    protected void setUp() throws Exception
    {
        engine = new VelocityEngine();

        //by default, make the engine's log output go to the test-report
        log = new TestLogChute(false, false);
        log.setEnabledLevel(TestLogChute.INFO_ID);
        log.setSystemErrLevel(TestLogChute.WARN_ID);
        engine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, log);

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

        setUpEngine(engine);

        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 template)
    {
        info("Template '"+name+"':  "+template);
        getStringRepository().putStringResource(name, template);
    }

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

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

    protected void info(String msg)
    {
        if (DEBUG)
        {
            if (engine == null)
            {
                Velocity.getLog().info(msg);
            }
            else
            {
                engine.getLog().info(msg);
            }
        }
    }

    protected void info(String msg, Throwable t)
    {
        if (DEBUG)
        {
            if (engine == null)
            {
                Velocity.getLog().info(msg);
            }
            else
            {
                engine.getLog().info(msg, t);
            }
        }
    }

    public void testBase()
    {
        if (DEBUG && engine != null)
        {
            assertSchmoo("");
            assertSchmoo("abc\n123");
        }
    }

    /**
     * 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 template)
    {
        info("Expectation: "+expected);
        assertEquals(expected, evaluate(template));
    }

    /**
     * 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)
    {
        try
        {
            if (!DEBUG)
            {
                log.off();
            }
            if (exceptionType != null)
            {
                info("Expectation: "+exceptionType.getName());
            }
            evaluate(evil);
            fail("Template '"+evil+"' should have thrown an exception.");
        }
        catch (Exception e)
        {
            if (exceptionType != null && !exceptionType.isAssignableFrom(e.getClass()))
            {
                fail("Was expecting template '"+evil+"' to throw "+exceptionType+" not "+e);
            }
            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 template)
    {
        StringWriter writer = new StringWriter();
        try
        {
            info("Template: "+template);

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

            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)
    {
        StringBuffer buf = new StringBuffer();
        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.lang.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);
            }
        }
    }


    //TODO: drop this for JDK regex once we move to JDK 1.5
    private static Perl5Util perl = new Perl5Util();
    /**
     * 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.
     *
     * @author <a href="mailto:rubys@us.ibm.com">Sam Ruby</a>
     * @return source with all line terminations changed to Unix style
     */
    protected String normalizeNewlines (String source)
    {
        return perl.substitute("s/\r[\r]?[\n]/\n/g", source);
    }

    /**
     * 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
    {
        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 StringUtils.fileContentsToString(fileName);
    }

    /**
     * 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: "+compare);
            info("Result: "+result);
        }
        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 final String getTestCaseName(String s)
    {
        StringBuffer name = new StringBuffer();
        name.append(Character.toTitleCase(s.charAt(0)));
        name.append(s.substring(1, s.length()).toLowerCase());
        return name.toString();
    }
}
