/*
 * 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 freemarker.template;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

import org.junit.Test;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import freemarker.cache.CacheStorageWithGetSize;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.NullCacheStorage;
import freemarker.cache.SoftCacheStorage;
import freemarker.cache.StringTemplateLoader;
import freemarker.cache.StrongCacheStorage;
import freemarker.cache.TemplateCache;
import freemarker.cache.TemplateLookupContext;
import freemarker.cache.TemplateLookupResult;
import freemarker.cache.TemplateLookupStrategy;
import freemarker.cache.TemplateNameFormat;
import freemarker.core.BaseNTemplateNumberFormatFactory;
import freemarker.core.CFormat;
import freemarker.core.CombinedMarkupOutputFormat;
import freemarker.core.Configurable;
import freemarker.core.Configurable.SettingValueAssignmentException;
import freemarker.core.Configurable.UnknownSettingException;
import freemarker.core.ConfigurableTest;
import freemarker.core.CustomHTMLOutputFormat;
import freemarker.core.DefaultTruncateBuiltinAlgorithm;
import freemarker.core.DummyOutputFormat;
import freemarker.core.Environment;
import freemarker.core.EpochMillisDivTemplateDateFormatFactory;
import freemarker.core.EpochMillisTemplateDateFormatFactory;
import freemarker.core.HTMLOutputFormat;
import freemarker.core.HexTemplateNumberFormatFactory;
import freemarker.core.JSONCFormat;
import freemarker.core.JavaCFormat;
import freemarker.core.JavaScriptCFormat;
import freemarker.core.JavaScriptOrJSONCFormat;
import freemarker.core.LegacyCFormat;
import freemarker.core.MarkupOutputFormat;
import freemarker.core.OptInTemplateClassResolver;
import freemarker.core.OutputFormat;
import freemarker.core.ParseException;
import freemarker.core.RTFOutputFormat;
import freemarker.core.TemplateClassResolver;
import freemarker.core.TemplateDateFormatFactory;
import freemarker.core.TemplateNumberFormatFactory;
import freemarker.core.UndefinedOutputFormat;
import freemarker.core.UnregisteredOutputFormatException;
import freemarker.core.XHTMLOutputFormat;
import freemarker.core.XMLOutputFormat;
import freemarker.core.XSCFormat;
import freemarker.core._CoreStringUtils;
import freemarker.ext.beans.BeansWrapperBuilder;
import freemarker.ext.beans.LegacyDefaultMemberAccessPolicy;
import freemarker.ext.beans.MemberAccessPolicy;
import freemarker.ext.beans.MemberSelectorListMemberAccessPolicy;
import freemarker.ext.beans.StringModel;
import freemarker.ext.beans.WhitelistMemberAccessPolicy;
import freemarker.template.utility.DateUtil;
import freemarker.template.utility.NullArgumentException;
import freemarker.template.utility.NullWriter;
import junit.framework.TestCase;

public class ConfigurationTest extends TestCase {

    public ConfigurationTest(String name) {
        super(name);
    }

    public void testIncompatibleImprovementsChangesDefaults() {
        Version newVersion = Configuration.VERSION_2_3_21;
        Version oldVersion = Configuration.VERSION_2_3_20;
        
        Configuration cfg = new Configuration();
        assertUsesLegacyObjectWrapper(cfg);
        assertUsesLegacyTemplateLoader(cfg);
        assertEquals(cfg.getIncompatibleImprovements(), Configuration.VERSION_2_3_0);
        
        cfg.setIncompatibleImprovements(newVersion);
        assertUsesNewObjectWrapper(cfg);
        assertUsesNewTemplateLoader(cfg);
        
        cfg.setIncompatibleImprovements(oldVersion);
        assertUsesLegacyObjectWrapper(cfg);
        assertUsesLegacyTemplateLoader(cfg);
        cfg.setIncompatibleImprovements(oldVersion);
        assertUsesLegacyObjectWrapper(cfg);
        assertUsesLegacyTemplateLoader(cfg);
        
        cfg.setIncompatibleImprovements(newVersion);
        assertUsesNewObjectWrapper(cfg);
        assertUsesNewTemplateLoader(cfg);
        cfg.setIncompatibleImprovements(newVersion);
        assertUsesNewObjectWrapper(cfg);
        assertUsesNewTemplateLoader(cfg);
        
        cfg.setObjectWrapper(new SimpleObjectWrapper());
        cfg.setIncompatibleImprovements(oldVersion);
        assertSame(SimpleObjectWrapper.class, cfg.getObjectWrapper().getClass());
        assertUsesLegacyTemplateLoader(cfg);
        
        cfg.setTemplateLoader(new StringTemplateLoader());
        cfg.setIncompatibleImprovements(newVersion);
        assertSame(SimpleObjectWrapper.class, cfg.getObjectWrapper().getClass());
        assertSame(StringTemplateLoader.class, cfg.getTemplateLoader().getClass());
        
        cfg.setIncompatibleImprovements(oldVersion);
        assertSame(SimpleObjectWrapper.class, cfg.getObjectWrapper().getClass());
        assertSame(StringTemplateLoader.class, cfg.getTemplateLoader().getClass());

        cfg.setObjectWrapper(ObjectWrapper.DEFAULT_WRAPPER);
        cfg.setIncompatibleImprovements(newVersion);
        assertSame(ObjectWrapper.DEFAULT_WRAPPER, cfg.getObjectWrapper());
        assertSame(StringTemplateLoader.class, cfg.getTemplateLoader().getClass());
        
        cfg.unsetObjectWrapper();
        assertUsesNewObjectWrapper(cfg);
        cfg.unsetTemplateLoader();
        assertUsesNewTemplateLoader(cfg);

        cfg.setIncompatibleImprovements(oldVersion);
        assertUsesLegacyObjectWrapper(cfg);
        assertUsesLegacyTemplateLoader(cfg);

        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_22);
        assertUses2322ObjectWrapper(cfg);
        assertUsesNewTemplateLoader(cfg);
        
        // ---
        
        cfg = new Configuration(newVersion);
        assertUsesNewObjectWrapper(cfg);
        assertUsesNewTemplateLoader(cfg);
        
        cfg.setIncompatibleImprovements(oldVersion);
        assertUsesLegacyObjectWrapper(cfg);
        assertUsesLegacyTemplateLoader(cfg);
        
        // ---
        
        cfg = new Configuration(Configuration.VERSION_2_3_22);
        assertUses2322ObjectWrapper(cfg);
        assertUsesNewTemplateLoader(cfg);
        
        cfg = new Configuration(Configuration.VERSION_2_3_25);
        assertFalse(((DefaultObjectWrapper) cfg.getObjectWrapper()).getTreatDefaultMethodsAsBeanMembers());
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_26);
        assertTrue(((DefaultObjectWrapper) cfg.getObjectWrapper()).getTreatDefaultMethodsAsBeanMembers());
        assertTrue(((DefaultObjectWrapper) cfg.getObjectWrapper()).getPreferIndexedReadMethod());
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_27);
        assertTrue(((DefaultObjectWrapper) cfg.getObjectWrapper()).getTreatDefaultMethodsAsBeanMembers());
        assertFalse(((DefaultObjectWrapper) cfg.getObjectWrapper()).getPreferIndexedReadMethod());

        cfg = new Configuration(Configuration.VERSION_2_3_0);
        assertSame(LegacyCFormat.INSTANCE, cfg.getCFormat());
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_20);
        assertSame(LegacyCFormat.INSTANCE, cfg.getCFormat());
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_21);
        assertSame(LegacyCFormat.INSTANCE, cfg.getCFormat());
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_31);
        assertSame(LegacyCFormat.INSTANCE, cfg.getCFormat());
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_32);
        assertSame(JavaScriptOrJSONCFormat.INSTANCE, cfg.getCFormat());
        cfg.setCFormat(JavaScriptOrJSONCFormat.INSTANCE); // Same as default, but explicitly set now
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_31);
        assertSame(JavaScriptOrJSONCFormat.INSTANCE, cfg.getCFormat());
    }

    private void assertUses2322ObjectWrapper(Configuration cfg) {
        Object ow = cfg.getObjectWrapper();
        assertEquals(DefaultObjectWrapper.class, ow.getClass());
        assertEquals(Configuration.VERSION_2_3_22,
                ((DefaultObjectWrapper) cfg.getObjectWrapper()).getIncompatibleImprovements());
    }
    
    public void testUnsetAndIsExplicitlySet() {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        
        assertFalse(cfg.isLogTemplateExceptionsExplicitlySet());
        assertTrue(cfg.getLogTemplateExceptions());
        //
        cfg.setLogTemplateExceptions(false);
        assertTrue(cfg.isLogTemplateExceptionsExplicitlySet());
        assertFalse(cfg.getLogTemplateExceptions());
        //
        for (int i = 0; i < 2; i++) {
            cfg.unsetLogTemplateExceptions();
            assertFalse(cfg.isLogTemplateExceptionsExplicitlySet());
            assertTrue(cfg.getLogTemplateExceptions());
        }
        
        assertFalse(cfg.isObjectWrapperExplicitlySet());
        assertSame(ObjectWrapper.DEFAULT_WRAPPER, cfg.getObjectWrapper());
        //
        cfg.setObjectWrapper(ObjectWrapper.SIMPLE_WRAPPER);
        assertTrue(cfg.isObjectWrapperExplicitlySet());
        assertSame(ObjectWrapper.SIMPLE_WRAPPER, cfg.getObjectWrapper());
        //
        for (int i = 0; i < 2; i++) {
            cfg.unsetObjectWrapper();
            assertFalse(cfg.isObjectWrapperExplicitlySet());
            assertSame(ObjectWrapper.DEFAULT_WRAPPER, cfg.getObjectWrapper());
        }
        
        assertFalse(cfg.isTemplateExceptionHandlerExplicitlySet());
        assertSame(TemplateExceptionHandler.DEBUG_HANDLER, cfg.getTemplateExceptionHandler());
        //
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        assertTrue(cfg.isTemplateExceptionHandlerExplicitlySet());
        assertSame(TemplateExceptionHandler.RETHROW_HANDLER, cfg.getTemplateExceptionHandler());
        //
        for (int i = 0; i < 2; i++) {
            cfg.unsetTemplateExceptionHandler();
            assertFalse(cfg.isTemplateExceptionHandlerExplicitlySet());
            assertSame(TemplateExceptionHandler.DEBUG_HANDLER, cfg.getTemplateExceptionHandler());
        }
        
        assertFalse(cfg.isTemplateLoaderExplicitlySet());
        assertTrue(cfg.getTemplateLoader() instanceof FileTemplateLoader);
        //
        cfg.setTemplateLoader(null);
        assertTrue(cfg.isTemplateLoaderExplicitlySet());
        assertNull(cfg.getTemplateLoader());
        //
        for (int i = 0; i < 3; i++) {
            if (i == 2) {
                cfg.setTemplateLoader(cfg.getTemplateLoader());
            }
            cfg.unsetTemplateLoader();
            assertFalse(cfg.isTemplateLoaderExplicitlySet());
            assertTrue(cfg.getTemplateLoader() instanceof FileTemplateLoader);
        }
        
        assertFalse(cfg.isTemplateLookupStrategyExplicitlySet());
        assertSame(TemplateLookupStrategy.DEFAULT_2_3_0, cfg.getTemplateLookupStrategy());
        //
        cfg.setTemplateLookupStrategy(TemplateLookupStrategy.DEFAULT_2_3_0);
        assertTrue(cfg.isTemplateLookupStrategyExplicitlySet());
        //
        for (int i = 0; i < 2; i++) {
            cfg.unsetTemplateLookupStrategy();
            assertFalse(cfg.isTemplateLookupStrategyExplicitlySet());
        }
        
        assertFalse(cfg.isTemplateNameFormatExplicitlySet());
        assertSame(TemplateNameFormat.DEFAULT_2_3_0, cfg.getTemplateNameFormat());
        //
        cfg.setTemplateNameFormat(TemplateNameFormat.DEFAULT_2_4_0);
        assertTrue(cfg.isTemplateNameFormatExplicitlySet());
        assertSame(TemplateNameFormat.DEFAULT_2_4_0, cfg.getTemplateNameFormat());
        //
        for (int i = 0; i < 2; i++) {
            cfg.unsetTemplateNameFormat();
            assertFalse(cfg.isTemplateNameFormatExplicitlySet());
            assertSame(TemplateNameFormat.DEFAULT_2_3_0, cfg.getTemplateNameFormat());
        }
        
        assertFalse(cfg.isCacheStorageExplicitlySet());
        assertTrue(cfg.getCacheStorage() instanceof SoftCacheStorage);
        //
        cfg.setCacheStorage(NullCacheStorage.INSTANCE);
        assertTrue(cfg.isCacheStorageExplicitlySet());
        assertSame(NullCacheStorage.INSTANCE, cfg.getCacheStorage());
        //
        for (int i = 0; i < 3; i++) {
            if (i == 2) {
                cfg.setCacheStorage(cfg.getCacheStorage());
            }
            cfg.unsetCacheStorage();
            assertFalse(cfg.isCacheStorageExplicitlySet());
            assertTrue(cfg.getCacheStorage() instanceof SoftCacheStorage);
        }

        assertFalse(cfg.isCFormatExplicitlySet());
        //
        cfg.setCFormat(XSCFormat.INSTANCE);
        assertTrue(cfg.isCFormatExplicitlySet());
        //
        cfg.unsetCFormat();
        assertFalse(cfg.isCFormatExplicitlySet());
    }
    
    public void testTemplateLoadingErrors() throws Exception {
        Configuration cfg = new Configuration();
        try {
            cfg.getTemplate("missing.ftl");
            fail();
        } catch (TemplateNotFoundException e) {
            assertThat(e.getMessage(), allOf(containsString("wasn't set"), containsString("default")));
        }
        
        cfg = new Configuration(Configuration.VERSION_2_3_21);
        try {
            cfg.getTemplate("missing.ftl");
            fail();
        } catch (TemplateNotFoundException e) {
            assertThat(e.getMessage(), allOf(containsString("wasn't set"), not(containsString("default"))));
        }
        
        cfg.setClassForTemplateLoading(this.getClass(), "nosuchpackage");
        try {
            cfg.getTemplate("missing.ftl");
            fail();
        } catch (TemplateNotFoundException e) {
            assertThat(e.getMessage(), not(containsString("wasn't set")));
        }
    }
    
    private void assertUsesLegacyObjectWrapper(Configuration cfg) {
        assertSame(ObjectWrapper.DEFAULT_WRAPPER, cfg.getObjectWrapper());
    }

    private void assertUsesNewObjectWrapper(Configuration cfg) {
        assertEquals(
                Configuration.VERSION_2_3_21,
                ((DefaultObjectWrapper) cfg.getObjectWrapper()).getIncompatibleImprovements());
    }
    
    private void assertUsesNewTemplateLoader(Configuration cfg) {
        assertNull(cfg.getTemplateLoader());
    }
    
    private void assertUsesLegacyTemplateLoader(Configuration cfg) {
        assertTrue(cfg.getTemplateLoader() instanceof FileTemplateLoader);
    }
    
    public void testVersion() {
        Version v = Configuration.getVersion();
        assertTrue(v.intValue() > _VersionInts.V_2_3_20);
        assertSame(v.toString(), Configuration.getVersionNumber());
        
        try {
            new Configuration(new Version(999, 1, 2));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("upgrade"));
        }
        
        try {
            new Configuration(new Version(2, 2, 2));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("2.3.0"));
        }
    }
    
    public void testShowErrorTips() throws Exception {
        Configuration cfg = new Configuration();
        try {
            new Template(null, "${x}", cfg).process(null, NullWriter.INSTANCE);
            fail();
        } catch (TemplateException e) {
            assertThat(e.getMessage(), containsString("Tip:"));
        }
        
        cfg.setShowErrorTips(false);
        try {
            new Template(null, "${x}", cfg).process(null, NullWriter.INSTANCE);
            fail();
        } catch (TemplateException e) {
            assertThat(e.getMessage(), not(containsString("Tip:")));
        }
    }
    
    @Test
    @SuppressWarnings("boxing")
    public void testGetTemplateOverloads() throws IOException, TemplateException {
        final Locale hu = new Locale("hu", "HU");
        final String latin1 = "ISO-8859-1";
        final String latin2 = "ISO-8859-2";
        final String utf8 = "utf-8";
        final String tFtl = "t.ftl";
        final String tEnFtl = "t_en.ftl";
        final String tUtf8Ftl = "t-utf8.ftl";
        final Integer custLookupCond = 123;
        
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        cfg.setLocale(Locale.GERMAN);
        cfg.setDefaultEncoding(latin1);
        cfg.setEncoding(hu, latin2);
        
        StringTemplateLoader tl = new StringTemplateLoader();
        tl.putTemplate(tFtl, "${1}");
        tl.putTemplate(tEnFtl, "${1}");
        tl.putTemplate(tUtf8Ftl, "<#ftl encoding='utf-8'>");
        cfg.setTemplateLoader(tl);
        
        // 1 args:
        {
            Template t = cfg.getTemplate(tFtl);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin1, t.getEncoding());
        }
        {
            Template t = cfg.getTemplate(tUtf8Ftl);
            assertEquals(tUtf8Ftl, t.getName());
            assertEquals(tUtf8Ftl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
        }
        
        // 2 args overload 1:
        {
            Template t = cfg.getTemplate(tFtl, Locale.GERMAN);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin1, t.getEncoding());
        }
        {
            Template t = cfg.getTemplate(tFtl, (Locale) null);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin1, t.getEncoding());
        }
        {
            Template t = cfg.getTemplate(tFtl, Locale.US);
            assertEquals(tFtl, t.getName());
            assertEquals(tEnFtl, t.getSourceName());
            assertEquals(Locale.US, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin1, t.getEncoding());
        }
        {
            Template t = cfg.getTemplate(tUtf8Ftl, Locale.US);
            assertEquals(tUtf8Ftl, t.getName());
            assertEquals(tUtf8Ftl, t.getSourceName());
            assertEquals(Locale.US, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
        }
        {
            Template t = cfg.getTemplate(tFtl, hu);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin2, t.getEncoding());
        }
        {
            Template t = cfg.getTemplate(tUtf8Ftl, hu);
            assertEquals(tUtf8Ftl, t.getName());
            assertEquals(tUtf8Ftl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
        }
        
        // 2 args overload 2:
        {
            Template t = cfg.getTemplate(tFtl, utf8);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
        }
        {
            Template t = cfg.getTemplate(tFtl, (String) null);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin1, t.getEncoding());
        }
        
        // 3 args:
        {
            Template t = cfg.getTemplate(tFtl, hu, utf8);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("1", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, hu, null);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin2, t.getEncoding());
            assertOutputEquals("1", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, null, utf8);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("1", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, null, null);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin1, t.getEncoding());
            assertOutputEquals("1", t);
        }
        
        // 4 args:
        {
            Template t = cfg.getTemplate(tFtl, hu, utf8, false);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("${1}", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, hu, utf8, true);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("1", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, null, utf8, true);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("1", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, hu, null, true);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin2, t.getEncoding());
            assertOutputEquals("1", t);
        }
        
        // 5 args:
        {
            Template t = cfg.getTemplate(tFtl, hu, utf8, false, true);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("${1}", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, hu, utf8, true, false);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("1", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, null, utf8, true, false);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("1", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, hu, null, true, false);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertNull(t.getCustomLookupCondition());
            assertEquals(latin2, t.getEncoding());
            assertOutputEquals("1", t);
        }
        try {
            cfg.getTemplate("missing.ftl", hu, utf8, true, false);
            fail();
        } catch (TemplateNotFoundException e) {
            // Expected
        }
        assertNull(cfg.getTemplate("missing.ftl", hu, utf8, true, true));
        
        // 6 args:
        {
            Template t = cfg.getTemplate(tFtl, hu, custLookupCond, utf8, true, false);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertEquals(custLookupCond, t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("1", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, hu, custLookupCond, utf8, false, false);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertEquals(custLookupCond, t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("${1}", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, null, custLookupCond, utf8, true, false);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(Locale.GERMAN, t.getLocale());
            assertEquals(custLookupCond, t.getCustomLookupCondition());
            assertEquals(utf8, t.getEncoding());
            assertOutputEquals("1", t);
        }
        {
            Template t = cfg.getTemplate(tFtl, hu, custLookupCond, null, true, false);
            assertEquals(tFtl, t.getName());
            assertEquals(tFtl, t.getSourceName());
            assertEquals(hu, t.getLocale());
            assertEquals(custLookupCond, t.getCustomLookupCondition());
            assertEquals(latin2, t.getEncoding());
            assertOutputEquals("1", t);
        }
        try {
            cfg.getTemplate("missing.ftl", hu, custLookupCond, utf8, true, false);
            fail();
        } catch (TemplateNotFoundException e) {
            // Expected
        }
        assertNull(cfg.getTemplate("missing.ftl", hu, custLookupCond, utf8, true, true));
    }

    private void assertOutputEquals(final String expectedContent, final Template t) throws TemplateException,
            IOException {
        StringWriter sw = new StringWriter();
        t.process(null, sw);
        assertEquals(expectedContent, sw.toString());
    }

    public void testSetTemplateLoaderAndCache() throws Exception {
        Configuration cfg = new Configuration();
        
        CacheStorageWithGetSize cacheStorage = (CacheStorageWithGetSize) cfg.getCacheStorage();
        assertEquals(0, cacheStorage.getSize());
        cfg.setCacheStorage(new StrongCacheStorage());
        cacheStorage = (CacheStorageWithGetSize) cfg.getCacheStorage();
        assertEquals(0, cacheStorage.getSize());
        
        cfg.setClassForTemplateLoading(ConfigurationTest.class, "");
        assertEquals(0, cacheStorage.getSize());
        cfg.getTemplate("toCache1.ftl");
        assertEquals(1, cacheStorage.getSize());
        cfg.getTemplate("toCache2.ftl");
        assertEquals(2, cacheStorage.getSize());
        cfg.setClassForTemplateLoading(ConfigurationTest.class, "");
        assertEquals(0, cacheStorage.getSize());
        cfg.getTemplate("toCache1.ftl");
        assertEquals(1, cacheStorage.getSize());
        cfg.setTemplateLoader(cfg.getTemplateLoader());
        assertEquals(1, cacheStorage.getSize());
    }

    public void testChangingLocalizedLookupClearsCache() throws Exception {
        Configuration cfg = new Configuration();
        cfg.setCacheStorage(new StrongCacheStorage());
        CacheStorageWithGetSize cache = (CacheStorageWithGetSize) cfg.getCacheStorage();
        cache = (CacheStorageWithGetSize) cfg.getCacheStorage();
        
        assertEquals(0, cache.getSize());
        
        cfg.setClassForTemplateLoading(ConfigurationTest.class, "");
        assertEquals(0, cache.getSize());
        cfg.getTemplate("toCache1.ftl");
        assertEquals(1, cache.getSize());
        cfg.setLocalizedLookup(true);
        assertEquals(1, cache.getSize());
        cfg.setLocalizedLookup(false);
        assertEquals(0, cache.getSize());
        cfg.getTemplate("toCache1.ftl");
        assertEquals(1, cache.getSize());
        cfg.setLocalizedLookup(false);
        assertEquals(1, cache.getSize());
        cfg.setLocalizedLookup(true);
        assertEquals(0, cache.getSize());
    }

    public void testChangingTemplateNameFormatClearsCache() throws Exception {
        Configuration cfg = new Configuration();
        cfg.setCacheStorage(new StrongCacheStorage());
        CacheStorageWithGetSize cache = (CacheStorageWithGetSize) cfg.getCacheStorage();
        cache = (CacheStorageWithGetSize) cfg.getCacheStorage();
        
        assertEquals(0, cache.getSize());
        
        cfg.setClassForTemplateLoading(ConfigurationTest.class, "");
        assertEquals(0, cache.getSize());
        cfg.getTemplate("toCache1.ftl");
        assertEquals(1, cache.getSize());
        cfg.setTemplateNameFormat(TemplateNameFormat.DEFAULT_2_3_0);
        assertEquals(1, cache.getSize());
        cfg.setTemplateNameFormat(TemplateNameFormat.DEFAULT_2_4_0);
        assertEquals(0, cache.getSize());
        cfg.getTemplate("toCache1.ftl");
        assertEquals(1, cache.getSize());
        cfg.setTemplateNameFormat(TemplateNameFormat.DEFAULT_2_4_0);
        assertEquals(1, cache.getSize());
        cfg.setTemplateNameFormat(TemplateNameFormat.DEFAULT_2_3_0);
        assertEquals(0, cache.getSize());
    }

    public void testChangingTemplateNameFormatHasEffect() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        
        StringTemplateLoader tl = new StringTemplateLoader();
        tl.putTemplate("a/b.ftl", "In a/b.ftl");
        tl.putTemplate("b.ftl", "In b.ftl");
        cfg.setTemplateLoader(tl);
        
        {
            final Template template = cfg.getTemplate("a/./../b.ftl");
            assertEquals("a/b.ftl", template.getName());
            assertEquals("a/b.ftl", template.getSourceName());
            assertEquals("In a/b.ftl", template.toString());
        }
        
        cfg.setTemplateNameFormat(TemplateNameFormat.DEFAULT_2_4_0);
        
        {
            final Template template = cfg.getTemplate("a/./../b.ftl");
            assertEquals("b.ftl", template.getName());
            assertEquals("b.ftl", template.getSourceName());
            assertEquals("In b.ftl", template.toString());
        }
    }

    public void testTemplateNameFormatSetSetting() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        assertSame(TemplateNameFormat.DEFAULT_2_3_0, cfg.getTemplateNameFormat());
        cfg.setSetting(Configuration.TEMPLATE_NAME_FORMAT_KEY, "defAult_2_4_0");
        assertSame(TemplateNameFormat.DEFAULT_2_4_0, cfg.getTemplateNameFormat());
        cfg.setSetting(Configuration.TEMPLATE_NAME_FORMAT_KEY, "defaUlt_2_3_0");
        assertSame(TemplateNameFormat.DEFAULT_2_3_0, cfg.getTemplateNameFormat());
        assertTrue(cfg.isTemplateNameFormatExplicitlySet());
        cfg.setSetting(Configuration.TEMPLATE_NAME_FORMAT_KEY, "defauLt");
        assertFalse(cfg.isTemplateNameFormatExplicitlySet());
    }

    public void testObjectWrapperSetSetting() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_20);
        
        {
            cfg.setSetting(Configurable.OBJECT_WRAPPER_KEY, "defAult");
            assertSame(ObjectWrapper.DEFAULT_WRAPPER, cfg.getObjectWrapper());
        }
        
        {
            cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_22);
            assertNotSame(ObjectWrapper.DEFAULT_WRAPPER, cfg.getObjectWrapper());
            DefaultObjectWrapper dow = (DefaultObjectWrapper) cfg.getObjectWrapper();
            assertEquals(Configuration.VERSION_2_3_22, dow.getIncompatibleImprovements());
            assertTrue(dow.getForceLegacyNonListCollections());
        }
        
        {
            cfg.setSetting(Configurable.OBJECT_WRAPPER_KEY, "defAult_2_3_0");
            assertSame(ObjectWrapper.DEFAULT_WRAPPER, cfg.getObjectWrapper());
        }
        
        {
            cfg.setSetting(Configurable.OBJECT_WRAPPER_KEY,
                    "DefaultObjectWrapper(2.3.21, useAdaptersForContainers=true, forceLegacyNonListCollections=false)");
            DefaultObjectWrapper dow = (DefaultObjectWrapper) cfg.getObjectWrapper();
            assertEquals(Configuration.VERSION_2_3_21, dow.getIncompatibleImprovements());
            assertFalse(dow.getForceLegacyNonListCollections());
        }
        
        {
            cfg.setSetting(Configurable.OBJECT_WRAPPER_KEY, "defAult");
            DefaultObjectWrapper dow = (DefaultObjectWrapper) cfg.getObjectWrapper();
            assertEquals(Configuration.VERSION_2_3_22, dow.getIncompatibleImprovements());
            assertTrue(dow.getForceLegacyNonListCollections());
        }
    }
    
    public void testTemplateLookupStrategyDefaultAndSet() throws IOException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        assertSame(TemplateLookupStrategy.DEFAULT_2_3_0, cfg.getTemplateLookupStrategy());
        
        cfg.setClassForTemplateLoading(ConfigurationTest.class, "");
        assertSame(TemplateLookupStrategy.DEFAULT_2_3_0, cfg.getTemplateLookupStrategy());
        
        CacheStorageWithGetSize cache = (CacheStorageWithGetSize) cfg.getCacheStorage();
        cfg.setClassForTemplateLoading(ConfigurationTest.class, "");
        assertEquals(0, cache.getSize());
        cfg.getTemplate("toCache1.ftl");
        assertEquals(1, cache.getSize());
        
        cfg.setTemplateLookupStrategy(TemplateLookupStrategy.DEFAULT_2_3_0);
        assertEquals(1, cache.getSize());
        
        final TemplateLookupStrategy myStrategy = new TemplateLookupStrategy() {
            @Override
            public TemplateLookupResult lookup(TemplateLookupContext ctx) throws IOException {
                return ctx.lookupWithAcquisitionStrategy(ctx.getTemplateName());
            }
        };
        cfg.setTemplateLookupStrategy(myStrategy);
        assertEquals(0, cache.getSize());
        assertSame(myStrategy, cfg.getTemplateLookupStrategy());
        cfg.getTemplate("toCache1.ftl");
        assertEquals(1, cache.getSize());
        
        cfg.setTemplateLookupStrategy(myStrategy);
        assertEquals(1, cache.getSize());
        
        cfg.setTemplateLookupStrategy(TemplateLookupStrategy.DEFAULT_2_3_0);
        assertEquals(0, cache.getSize());
    }
    
    public void testSetTemplateConfigurations() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
        assertNull(cfg.getTemplateConfigurations());

        StringTemplateLoader tl = new StringTemplateLoader();
        tl.putTemplate("t.de.ftlh", "");
        tl.putTemplate("t.fr.ftlx", "");
        tl.putTemplate("t.ftlx", "");
        tl.putTemplate("Stat/t.de.ftlx", "");
        cfg.setTemplateLoader(tl);
        
        cfg.setTimeZone(TimeZone.getTimeZone("GMT+09"));
        
        cfg.setSetting(Configuration.TEMPLATE_CONFIGURATIONS_KEY,
                "MergingTemplateConfigurationFactory("
                    + "FirstMatchTemplateConfigurationFactory("
                        + "ConditionalTemplateConfigurationFactory("
                            + "FileNameGlobMatcher('*.de.*'), TemplateConfiguration(timeZone=TimeZone('GMT+01'))), "
                        + "ConditionalTemplateConfigurationFactory("
                            + "FileNameGlobMatcher('*.fr.*'), TemplateConfiguration(timeZone=TimeZone('GMT'))), "
                        + "allowNoMatch=true"
                    + "), "
                    + "FirstMatchTemplateConfigurationFactory("
                        + "ConditionalTemplateConfigurationFactory("
                            + "FileExtensionMatcher('ftlh'), TemplateConfiguration(booleanFormat='TODO,HTML')), "
                        + "ConditionalTemplateConfigurationFactory("
                            + "FileExtensionMatcher('ftlx'), TemplateConfiguration(booleanFormat='TODO,XML')), "
                        + "noMatchErrorDetails='Unrecognized template file extension'"
                    + "), "
                    + "ConditionalTemplateConfigurationFactory("
                        + "PathGlobMatcher('stat/**', caseInsensitive=true), "
                        + "TemplateConfiguration(timeZone=TimeZone('UTC'))"
                    + ")"
                + ")");
        
        {
            Template t = cfg.getTemplate("t.de.ftlh");
            assertEquals("TODO,HTML", t.getBooleanFormat());
            assertEquals(TimeZone.getTimeZone("GMT+01"), t.getTimeZone());
        }
        {
            Template t = cfg.getTemplate("t.fr.ftlx");
            assertEquals("TODO,XML", t.getBooleanFormat());
            assertEquals(TimeZone.getTimeZone("GMT"), t.getTimeZone());
        }
        {
            Template t = cfg.getTemplate("t.ftlx");
            assertEquals("TODO,XML", t.getBooleanFormat());
            assertEquals(TimeZone.getTimeZone("GMT+09"), t.getTimeZone());
        }
        {
            Template t = cfg.getTemplate("Stat/t.de.ftlx");
            assertEquals("TODO,XML", t.getBooleanFormat());
            assertEquals(DateUtil.UTC, t.getTimeZone());
        }
        
        assertNotNull(cfg.getTemplateConfigurations());
        cfg.setSetting(Configuration.TEMPLATE_CONFIGURATIONS_KEY, "null");
        assertNull(cfg.getTemplateConfigurations());
    }

    public void testSetAutoEscaping() throws Exception {
       Configuration cfg = new Configuration();
    
       assertEquals(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());

       cfg.setAutoEscapingPolicy(Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY);
       assertEquals(Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());

       cfg.setAutoEscapingPolicy(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY);
       assertEquals(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());

       cfg.setAutoEscapingPolicy(Configuration.DISABLE_AUTO_ESCAPING_POLICY);
       assertEquals(Configuration.DISABLE_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());
       
       cfg.setSetting(Configuration.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "enableIfSupported");
       assertEquals(Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());

       cfg.setSetting(Configuration.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "enable_if_supported");
       assertEquals(Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());
       
       cfg.setSetting(Configuration.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "enableIfDefault");
       assertEquals(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());

       cfg.setSetting(Configuration.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "enable_if_default");
       assertEquals(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());
       
       cfg.setSetting(Configuration.AUTO_ESCAPING_POLICY_KEY_CAMEL_CASE, "disable");
       assertEquals(Configuration.DISABLE_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());
       
       try {
           cfg.setAutoEscapingPolicy(Configuration.CAMEL_CASE_NAMING_CONVENTION);
           fail();
       } catch (IllegalArgumentException e) {
           // Expected
       }
    }

    public void testSetOutputFormat() throws Exception {
       Configuration cfg = new Configuration();
       
       assertEquals(UndefinedOutputFormat.INSTANCE, cfg.getOutputFormat());
       assertFalse(cfg.isOutputFormatExplicitlySet());
       
       try {
           cfg.setOutputFormat(null);
           fail();
       } catch (NullArgumentException e) {
           // Expected
       }
       
       assertFalse(cfg.isOutputFormatExplicitlySet());
       
       cfg.setSetting(Configuration.OUTPUT_FORMAT_KEY_CAMEL_CASE, XMLOutputFormat.class.getSimpleName());
       assertEquals(XMLOutputFormat.INSTANCE, cfg.getOutputFormat());
       
       cfg.setSetting(Configuration.OUTPUT_FORMAT_KEY_SNAKE_CASE, HTMLOutputFormat.class.getSimpleName());
       assertEquals(HTMLOutputFormat.INSTANCE, cfg.getOutputFormat());

       // Set standard format by name instead of class name:
       cfg.setSetting(Configuration.OUTPUT_FORMAT_KEY_CAMEL_CASE, XHTMLOutputFormat.INSTANCE.getName());
       assertEquals(XHTMLOutputFormat.INSTANCE, cfg.getOutputFormat());
       
       cfg.unsetOutputFormat();
       assertEquals(UndefinedOutputFormat.INSTANCE, cfg.getOutputFormat());
       assertFalse(cfg.isOutputFormatExplicitlySet());
       
       cfg.setOutputFormat(UndefinedOutputFormat.INSTANCE);
       assertTrue(cfg.isOutputFormatExplicitlySet());
       cfg.setSetting(Configuration.OUTPUT_FORMAT_KEY_CAMEL_CASE, "default");
       assertFalse(cfg.isOutputFormatExplicitlySet());
       
       try {
           cfg.setSetting(Configuration.OUTPUT_FORMAT_KEY, "null");
       } catch (SettingValueAssignmentException e) {
           assertThat(e.getCause().getMessage(), containsString(UndefinedOutputFormat.class.getSimpleName()));
       }
    }
    
    @Test
    public void testGetOutputFormatByName() throws Exception {
        Configuration cfg = new Configuration();
        
        assertSame(HTMLOutputFormat.INSTANCE, cfg.getOutputFormat(HTMLOutputFormat.INSTANCE.getName()));
        
        try {
            cfg.getOutputFormat("noSuchFormat");
            fail();
        } catch (UnregisteredOutputFormatException e) {
            assertThat(e.getMessage(), containsString("noSuchFormat"));
        }
        
        try {
            cfg.getOutputFormat("HTML}");
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("'{'"));
        }
        
        {
            OutputFormat of = cfg.getOutputFormat("HTML{RTF}");
            assertThat(of, instanceOf(CombinedMarkupOutputFormat.class));
            CombinedMarkupOutputFormat combinedOF = (CombinedMarkupOutputFormat) of;
            assertSame(HTMLOutputFormat.INSTANCE, combinedOF.getOuterOutputFormat());
            assertSame(RTFOutputFormat.INSTANCE, combinedOF.getInnerOutputFormat());
        }

        {
            OutputFormat of = cfg.getOutputFormat("XML{HTML{RTF}}");
            assertThat(of, instanceOf(CombinedMarkupOutputFormat.class));
            CombinedMarkupOutputFormat combinedOF = (CombinedMarkupOutputFormat) of;
            assertSame(XMLOutputFormat.INSTANCE, combinedOF.getOuterOutputFormat());
            MarkupOutputFormat innerOF = combinedOF.getInnerOutputFormat();
            assertThat(innerOF, instanceOf(CombinedMarkupOutputFormat.class));
            CombinedMarkupOutputFormat innerCombinedOF = (CombinedMarkupOutputFormat) innerOF; 
            assertSame(HTMLOutputFormat.INSTANCE, innerCombinedOF.getOuterOutputFormat());
            assertSame(RTFOutputFormat.INSTANCE, innerCombinedOF.getInnerOutputFormat());
        }
        
        try {
            cfg.getOutputFormat("plainText{HTML}");
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), allOf(containsString("plainText"), containsString("markup")));
        }
        try {
            cfg.getOutputFormat("HTML{plainText}");
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), allOf(containsString("plainText"), containsString("markup")));
        }
    }

    public void testSetRegisteredCustomOutputFormats() throws Exception {
        Configuration cfg = new Configuration();
        
        assertTrue(cfg.getRegisteredCustomOutputFormats().isEmpty());
        
        cfg.setSetting(Configuration.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_CAMEL_CASE,
                "[freemarker.core.CustomHTMLOutputFormat(), freemarker.core.DummyOutputFormat()]");
        assertEquals(
                ImmutableList.of(CustomHTMLOutputFormat.INSTANCE, DummyOutputFormat.INSTANCE),
                new ArrayList(cfg.getRegisteredCustomOutputFormats()));
        
        try {
            cfg.setSetting(Configuration.REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_SNAKE_CASE, "[TemplateConfiguration()]");
            fail();
        } catch (Exception e) {
            assertThat(e.getCause().getMessage(), containsString(OutputFormat.class.getSimpleName()));
        }
    }

    public void testSetRecognizeStandardFileExtensions() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
     
        assertFalse(cfg.getRecognizeStandardFileExtensions());
        assertFalse(cfg.isRecognizeStandardFileExtensionsExplicitlySet());

        cfg.setRecognizeStandardFileExtensions(true);
        assertTrue(cfg.getRecognizeStandardFileExtensions());
        assertTrue(cfg.isRecognizeStandardFileExtensionsExplicitlySet());
     
        cfg.unsetRecognizeStandardFileExtensions();
        assertFalse(cfg.getRecognizeStandardFileExtensions());
        assertFalse(cfg.isRecognizeStandardFileExtensionsExplicitlySet());
     
        cfg.setSetting(Configuration.RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_CAMEL_CASE, "false");
        assertFalse(cfg.getRecognizeStandardFileExtensions());
        assertTrue(cfg.isRecognizeStandardFileExtensionsExplicitlySet());
        
        cfg.setSetting(Configuration.RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_SNAKE_CASE, "default");
        assertFalse(cfg.getRecognizeStandardFileExtensions());
        assertFalse(cfg.isRecognizeStandardFileExtensionsExplicitlySet());
        
        cfg.unsetRecognizeStandardFileExtensions();
        assertFalse(cfg.getRecognizeStandardFileExtensions());
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_24);
        assertTrue(cfg.getRecognizeStandardFileExtensions());
        assertFalse(cfg.isRecognizeStandardFileExtensionsExplicitlySet());
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_23);
        assertFalse(cfg.getRecognizeStandardFileExtensions());
        cfg.setRecognizeStandardFileExtensions(false);
        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_24);
        assertFalse(cfg.getRecognizeStandardFileExtensions());
        assertTrue(cfg.isRecognizeStandardFileExtensionsExplicitlySet());
     }
    
    public void testSetTimeZone() throws TemplateException {
        TimeZone origSysDefTZ = TimeZone.getDefault();
        try {
            TimeZone sysDefTZ = TimeZone.getTimeZone("GMT-01");
            TimeZone.setDefault(sysDefTZ);
            
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
            assertEquals(sysDefTZ, cfg.getTimeZone());
            assertEquals(sysDefTZ.getID(), cfg.getSetting(Configurable.TIME_ZONE_KEY));
            cfg.setSetting(Configurable.TIME_ZONE_KEY, "JVM default");
            assertEquals(sysDefTZ, cfg.getTimeZone());
            assertEquals(sysDefTZ.getID(), cfg.getSetting(Configurable.TIME_ZONE_KEY));
            
            TimeZone newSysDefTZ = TimeZone.getTimeZone("GMT+09");
            TimeZone.setDefault(newSysDefTZ);
            assertEquals(sysDefTZ, cfg.getTimeZone());
            assertEquals(sysDefTZ.getID(), cfg.getSetting(Configurable.TIME_ZONE_KEY));
            cfg.setSetting(Configurable.TIME_ZONE_KEY, "JVM default");
            assertEquals(newSysDefTZ, cfg.getTimeZone());
            assertEquals(newSysDefTZ.getID(), cfg.getSetting(Configurable.TIME_ZONE_KEY));
        } finally {
            TimeZone.setDefault(origSysDefTZ);
        }
    }
    
    public void testSetSQLDateAndTimeTimeZone() throws TemplateException {
        TimeZone origSysDefTZ = TimeZone.getDefault();
        try {
            TimeZone sysDefTZ = TimeZone.getTimeZone("GMT-01");
            TimeZone.setDefault(sysDefTZ);
            
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
            assertNull(cfg.getSQLDateAndTimeTimeZone());
            assertEquals("null", cfg.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
            
            cfg.setSQLDateAndTimeTimeZone(null);
            assertNull(cfg.getSQLDateAndTimeTimeZone());
            assertEquals("null", cfg.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
            
            cfg.setSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY, "JVM default");
            assertEquals(sysDefTZ, cfg.getSQLDateAndTimeTimeZone());
            assertEquals(sysDefTZ.getID(), cfg.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
            
            cfg.setSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY, "null");
            assertNull(cfg.getSQLDateAndTimeTimeZone());
            assertEquals("null", cfg.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        } finally {
            TimeZone.setDefault(origSysDefTZ);
        }
    }

    public void testTimeZoneLayers() throws TemplateException, IOException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        Template t = new Template(null, "", cfg);
        Environment env1 = t.createProcessingEnvironment(null, new StringWriter());
        Environment env2 = t.createProcessingEnvironment(null, new StringWriter());
        
        // cfg:
        assertEquals(TimeZone.getDefault(), cfg.getTimeZone());
        assertNull(cfg.getSQLDateAndTimeTimeZone());
        assertEquals("null", cfg.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        // env:
        assertEquals(TimeZone.getDefault(), env1.getTimeZone());
        assertNull(env1.getSQLDateAndTimeTimeZone());
        assertEquals("null", env1.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        // env 2:
        assertEquals(TimeZone.getDefault(), env2.getTimeZone());
        assertNull(env2.getSQLDateAndTimeTimeZone());
        assertEquals("null", env2.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        
        env1.setSQLDateAndTimeTimeZone(DateUtil.UTC);
        // cfg:
        assertEquals(TimeZone.getDefault(), cfg.getTimeZone());
        assertNull(cfg.getSQLDateAndTimeTimeZone());
        assertEquals("null", cfg.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        // env:
        assertEquals(TimeZone.getDefault(), env1.getTimeZone());
        assertEquals(DateUtil.UTC, env1.getSQLDateAndTimeTimeZone());
        assertEquals(DateUtil.UTC.getID(), env1.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        
        TimeZone localTZ = TimeZone.getTimeZone("Europe/Brussels");
        env1.setTimeZone(localTZ);
        // cfg:
        assertEquals(TimeZone.getDefault(), cfg.getTimeZone());
        assertNull(cfg.getSQLDateAndTimeTimeZone());
        assertEquals("null", cfg.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        // env:
        assertEquals(localTZ, env1.getTimeZone());
        assertEquals(DateUtil.UTC, env1.getSQLDateAndTimeTimeZone());
        assertEquals(DateUtil.UTC.getID(), env1.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        // env 2:
        assertEquals(TimeZone.getDefault(), env2.getTimeZone());
        assertNull(env2.getSQLDateAndTimeTimeZone());
        assertEquals("null", env2.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        
        TimeZone otherTZ1 = TimeZone.getTimeZone("GMT+05");
        TimeZone otherTZ2 = TimeZone.getTimeZone("GMT+06");
        cfg.setTimeZone(otherTZ1);
        cfg.setSQLDateAndTimeTimeZone(otherTZ2);
        // cfg:
        assertEquals(otherTZ1, cfg.getTimeZone());
        assertEquals(otherTZ2, cfg.getSQLDateAndTimeTimeZone());
        assertEquals(otherTZ1.getID(), cfg.getSetting(Configurable.TIME_ZONE_KEY));
        assertEquals(otherTZ2.getID(), cfg.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        // env:
        assertEquals(localTZ, env1.getTimeZone());
        assertEquals(DateUtil.UTC, env1.getSQLDateAndTimeTimeZone());
        assertEquals(DateUtil.UTC.getID(), env1.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        // env 2:
        assertEquals(otherTZ1, env2.getTimeZone());
        assertEquals(otherTZ2, env2.getSQLDateAndTimeTimeZone());
        assertEquals(otherTZ1.getID(), env2.getSetting(Configurable.TIME_ZONE_KEY));
        assertEquals(otherTZ2.getID(), env2.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
        
        try {
            setTimeZoneToNull(env2);
            fail();
        } catch (IllegalArgumentException e) {
            // expected
        }
        env2.setSQLDateAndTimeTimeZone(null);
        assertEquals(otherTZ1, env2.getTimeZone());
        assertNull(env2.getSQLDateAndTimeTimeZone());
        assertEquals(otherTZ1.getID(), env2.getSetting(Configurable.TIME_ZONE_KEY));
        assertEquals("null", env2.getSetting(Configurable.SQL_DATE_AND_TIME_TIME_ZONE_KEY));
    }

    @SuppressFBWarnings(value="NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS", justification="Expected to fail")
    private void setTimeZoneToNull(Environment env2) {
        env2.setTimeZone(null);
    }
    
    public void testSetICIViaSetSettingAPI() throws TemplateException {
        Configuration cfg = new Configuration();
        assertEquals(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS, cfg.getIncompatibleImprovements());
        cfg.setSetting(Configuration.INCOMPATIBLE_IMPROVEMENTS, "2.3.21");
        assertEquals(Configuration.VERSION_2_3_21, cfg.getIncompatibleImprovements());
    }

    public void testSetLogTemplateExceptionsViaSetSettingAPI() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        assertEquals(true, cfg.getLogTemplateExceptions());
        cfg.setSetting(Configurable.LOG_TEMPLATE_EXCEPTIONS_KEY, "false");
        assertEquals(false, cfg.getLogTemplateExceptions());
    }

    public void testSetWrapUncheckedExceptionsViaSetSettingAPI() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_27);
        assertEquals(false, cfg.getWrapUncheckedExceptions());
        cfg.setSetting(Configurable.WRAP_UNCHECKED_EXCEPTIONS_KEY_CAMEL_CASE, "true");
        assertEquals(true, cfg.getWrapUncheckedExceptions());
        cfg.setSetting(Configurable.WRAP_UNCHECKED_EXCEPTIONS_KEY_SNAKE_CASE, "false");
        assertEquals(false, cfg.getWrapUncheckedExceptions());
    }
    
    public void testSetAttemptExceptionReporter() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        assertEquals(AttemptExceptionReporter.LOG_ERROR_REPORTER, cfg.getAttemptExceptionReporter());
        assertFalse(cfg.isAttemptExceptionReporterExplicitlySet());
        cfg.setSetting(Configurable.ATTEMPT_EXCEPTION_REPORTER_KEY, "log_warn");
        assertEquals(AttemptExceptionReporter.LOG_WARN_REPORTER, cfg.getAttemptExceptionReporter());
        assertTrue(cfg.isAttemptExceptionReporterExplicitlySet());
        cfg.setSetting(Configurable.ATTEMPT_EXCEPTION_REPORTER_KEY, "default");
        assertEquals(AttemptExceptionReporter.LOG_ERROR_REPORTER, cfg.getAttemptExceptionReporter());
        assertFalse(cfg.isAttemptExceptionReporterExplicitlySet());
    }
    
    public void testSharedVariables() throws TemplateModelException {
        Configuration cfg = new Configuration();

        cfg.setSharedVariable("erased", "");
        assertNotNull(cfg.getSharedVariable("erased"));
        
        Map<String, Object> vars = new HashMap<>();
        vars.put("a", "aa");
        vars.put("b", "bb");
        vars.put("c", new MyScalarModel());
        cfg.setSharedVaribles(vars);

        assertNull(cfg.getSharedVariable("erased"));
        
        {
            TemplateScalarModel aVal = (TemplateScalarModel) cfg.getSharedVariable("a");
            assertEquals("aa", aVal.getAsString());
            assertEquals(SimpleScalar.class, aVal.getClass());
            
            TemplateScalarModel bVal = (TemplateScalarModel) cfg.getSharedVariable("b");
            assertEquals("bb", bVal.getAsString());
            assertEquals(SimpleScalar.class, bVal.getClass());
            
            TemplateScalarModel cVal = (TemplateScalarModel) cfg.getSharedVariable("c");
            assertEquals("my", cVal.getAsString());
            assertEquals(MyScalarModel.class, cfg.getSharedVariable("c").getClass());
        }
        
        // Legacy method: Keeps TemplateModel created on the time it was called. 
        cfg.setSharedVariable("b", "bbLegacy");
        
        // Cause re-wrapping of variables added via setSharedVaribles:
        cfg.setObjectWrapper(new BeansWrapperBuilder(Configuration.VERSION_2_3_0).build());

        {
            TemplateScalarModel aVal = (TemplateScalarModel) cfg.getSharedVariable("a");
            assertEquals("aa", aVal.getAsString());
            assertEquals(StringModel.class, aVal.getClass());
            
            TemplateScalarModel bVal = (TemplateScalarModel) cfg.getSharedVariable("b");
            assertEquals("bbLegacy", bVal.getAsString());
            assertEquals(SimpleScalar.class, bVal.getClass());
            
            TemplateScalarModel cVal = (TemplateScalarModel) cfg.getSharedVariable("c");
            assertEquals("my", cVal.getAsString());
            assertEquals(MyScalarModel.class, cVal.getClass());
        }
    }

    @Test
    public void testApiBuiltinEnabled() throws IOException, TemplateException {
        for (Version v : new Version[] { Configuration.VERSION_2_3_0, Configuration.VERSION_2_3_22 }) {
            Configuration cfg = new Configuration(v);
            try {
                new Template(null, "${1?api}", cfg).process(null, NullWriter.INSTANCE);
                fail();
            } catch (TemplateException e) {
                assertThat(e.getMessage(), containsString(Configurable.API_BUILTIN_ENABLED_KEY));
            }
        }
        
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        cfg.setAPIBuiltinEnabled(true);
        new Template(null, "${m?api.hashCode()}", cfg)
                .process(Collections.singletonMap("m", new HashMap()), NullWriter.INSTANCE);
    }

    @Test
    public void testTemplateUpdateDelay() throws IOException, TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);

        assertEquals(TemplateCache.DEFAULT_TEMPLATE_UPDATE_DELAY_MILLIS, cfg.getTemplateUpdateDelayMilliseconds());
        
        cfg.setTemplateUpdateDelay(4);
        assertEquals(4000L, cfg.getTemplateUpdateDelayMilliseconds());
        
        cfg.setTemplateUpdateDelayMilliseconds(100);
        assertEquals(100L, cfg.getTemplateUpdateDelayMilliseconds());
        
        cfg.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, "5");
        assertEquals(5000L, cfg.getTemplateUpdateDelayMilliseconds());
        
        cfg.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, "3 ms");
        assertEquals(3L, cfg.getTemplateUpdateDelayMilliseconds());
        cfg.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, "4ms");
        assertEquals(4L, cfg.getTemplateUpdateDelayMilliseconds());
        
        cfg.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, "3 s");
        assertEquals(3000L, cfg.getTemplateUpdateDelayMilliseconds());
        cfg.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, "4s");
        assertEquals(4000L, cfg.getTemplateUpdateDelayMilliseconds());
        
        cfg.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, "3 m");
        assertEquals(1000L * 60 * 3, cfg.getTemplateUpdateDelayMilliseconds());
        cfg.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, "4m");
        assertEquals(1000L * 60 * 4, cfg.getTemplateUpdateDelayMilliseconds());

        cfg.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, "1 h");
        assertEquals(1000L * 60 * 60, cfg.getTemplateUpdateDelayMilliseconds());
        cfg.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, "2h");
        assertEquals(1000L * 60 * 60 * 2, cfg.getTemplateUpdateDelayMilliseconds());
    }
    
    @Test
    @SuppressFBWarnings(value = "NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS ", justification = "Testing wrong args")
    public void testSetCustomNumberFormat() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        
        try {
            cfg.setCustomNumberFormats(null);
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("null"));
        }

        try {
            cfg.setCustomNumberFormats(Collections.singletonMap("", HexTemplateNumberFormatFactory.INSTANCE));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("0 length"));
        }

        try {
            cfg.setCustomNumberFormats(Collections.singletonMap("a_b", HexTemplateNumberFormatFactory.INSTANCE));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("a_b"));
        }

        try {
            cfg.setCustomNumberFormats(Collections.singletonMap("a b", HexTemplateNumberFormatFactory.INSTANCE));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("a b"));
        }
        
        try {
            cfg.setCustomNumberFormats(ImmutableMap.of(
                    "a", HexTemplateNumberFormatFactory.INSTANCE,
                    "@wrong", HexTemplateNumberFormatFactory.INSTANCE));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("@wrong"));
        }
        
        cfg.setSetting(Configurable.CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE,
                "{ 'base': " + BaseNTemplateNumberFormatFactory.class.getName() + "() }");
        assertEquals(
                Collections.singletonMap("base", BaseNTemplateNumberFormatFactory.INSTANCE),
                cfg.getCustomNumberFormats());
        
        cfg.setSetting(Configurable.CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE,
                "{ "
                + "'base': " + BaseNTemplateNumberFormatFactory.class.getName() + "(), "
                + "'hex': " + HexTemplateNumberFormatFactory.class.getName() + "()"
                + " }");
        assertEquals(
                ImmutableMap.of(
                        "base", BaseNTemplateNumberFormatFactory.INSTANCE,
                        "hex", HexTemplateNumberFormatFactory.INSTANCE),
                cfg.getCustomNumberFormats());
        
        cfg.setSetting(Configurable.CUSTOM_NUMBER_FORMATS_KEY, "{}");
        assertEquals(Collections.emptyMap(), cfg.getCustomNumberFormats());
        
        try {
            cfg.setSetting(Configurable.CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE,
                    "{ 'x': " + EpochMillisTemplateDateFormatFactory.class.getName() + "() }");
            fail();
        } catch (TemplateException e) {
            assertThat(e.getCause().getMessage(), allOf(
                    containsString(EpochMillisTemplateDateFormatFactory.class.getName()),
                    containsString(TemplateNumberFormatFactory.class.getName())));
        }
    }

    @Test
    public void testSetBooleanFormat() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);

        cfg.setBooleanFormat("yes,no");
        assertOutputEquals("yes no", new Template(null, "${true} ${false}", cfg));

        cfg.setBooleanFormat("c");
        assertOutputEquals("true false", new Template(null, "${true} ${false}", cfg));

        try {
            cfg.setBooleanFormat("yes no");
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("comma"));
        }
    }

    @Test
    public void testSetTabSize() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        
        String ftl = "${\t}";
        
        try {
            new Template(null, ftl, cfg);
            fail();
        } catch (ParseException e) {
            assertEquals(9, e.getColumnNumber());
        }
        
        cfg.setTabSize(1);
        try {
            new Template(null, ftl, cfg);
            fail();
        } catch (ParseException e) {
            assertEquals(4, e.getColumnNumber());
        }
        
        try {
            cfg.setTabSize(0);
            fail();
        } catch (IllegalArgumentException e) {
            // Expected
        }
        
        try {
            cfg.setTabSize(257);
            fail();
        } catch (IllegalArgumentException e) {
            // Expected
        }
    }

    @Test
    public void testTabSizeSetting() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        assertEquals(8, cfg.getTabSize());
        cfg.setSetting(Configuration.TAB_SIZE_KEY_CAMEL_CASE, "4");
        assertEquals(4, cfg.getTabSize());
        cfg.setSetting(Configuration.TAB_SIZE_KEY_SNAKE_CASE, "1");
        assertEquals(1, cfg.getTabSize());
        
        try {
            cfg.setSetting(Configuration.TAB_SIZE_KEY_SNAKE_CASE, "x");
            fail();
        } catch (TemplateException e) {
            assertThat(e.getCause(), instanceOf(NumberFormatException.class));
        }
    }
    
    @SuppressFBWarnings(value="NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS", justification="We test failures")
    @Test
    public void testSetCustomDateFormat() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        
        try {
            cfg.setCustomDateFormats(null);
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("null"));
        }
        
        try {
            cfg.setCustomDateFormats(Collections.singletonMap("", EpochMillisTemplateDateFormatFactory.INSTANCE));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("0 length"));
        }

        try {
            cfg.setCustomDateFormats(Collections.singletonMap("a_b", EpochMillisTemplateDateFormatFactory.INSTANCE));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("a_b"));
        }

        try {
            cfg.setCustomDateFormats(Collections.singletonMap("a b", EpochMillisTemplateDateFormatFactory.INSTANCE));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("a b"));
        }
        
        try {
            cfg.setCustomDateFormats(ImmutableMap.of(
                    "a", EpochMillisTemplateDateFormatFactory.INSTANCE,
                    "@wrong", EpochMillisTemplateDateFormatFactory.INSTANCE));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("@wrong"));
        }
        
        cfg.setSetting(Configurable.CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
                "{ 'epoch': " + EpochMillisTemplateDateFormatFactory.class.getName() + "() }");
        assertEquals(
                Collections.singletonMap("epoch", EpochMillisTemplateDateFormatFactory.INSTANCE),
                cfg.getCustomDateFormats());
        
        cfg.setSetting(Configurable.CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE,
                "{ "
                + "'epoch': " + EpochMillisTemplateDateFormatFactory.class.getName() + "(), "
                + "'epochDiv': " + EpochMillisDivTemplateDateFormatFactory.class.getName() + "()"
                + " }");
        assertEquals(
                ImmutableMap.of(
                        "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE,
                        "epochDiv", EpochMillisDivTemplateDateFormatFactory.INSTANCE),
                cfg.getCustomDateFormats());
        
        cfg.setSetting(Configurable.CUSTOM_DATE_FORMATS_KEY, "{}");
        assertEquals(Collections.emptyMap(), cfg.getCustomDateFormats());
        
        try {
            cfg.setSetting(Configurable.CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
                    "{ 'x': " + HexTemplateNumberFormatFactory.class.getName() + "() }");
            fail();
        } catch (TemplateException e) {
            assertThat(e.getCause().getMessage(), allOf(
                    containsString(HexTemplateNumberFormatFactory.class.getName()),
                    containsString(TemplateDateFormatFactory.class.getName())));
        }
    }
    
    @Test
    public void testHasCustomFormats() throws IOException, TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        Template t = new Template(null, "", cfg);
        Environment env = t.createProcessingEnvironment(null, null);
        assertFalse(cfg.hasCustomFormats());
        assertFalse(t.hasCustomFormats());
        assertFalse(env.hasCustomFormats());
        
        env.setCustomDateFormats(Collections.singletonMap("f", EpochMillisTemplateDateFormatFactory.INSTANCE));
        assertFalse(t.hasCustomFormats());
        assertTrue(env.hasCustomFormats());
        t.setCustomDateFormats(Collections.singletonMap("f", EpochMillisTemplateDateFormatFactory.INSTANCE));
        assertFalse(cfg.hasCustomFormats());
        assertTrue(t.hasCustomFormats());
        cfg.setCustomDateFormats(Collections.singletonMap("f", EpochMillisTemplateDateFormatFactory.INSTANCE));
        assertTrue(cfg.hasCustomFormats());
        assertTrue(t.hasCustomFormats());
        assertTrue(env.hasCustomFormats());
        
        cfg.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap());
        t.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap());
        env.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap());
        assertFalse(cfg.hasCustomFormats());
        assertFalse(t.hasCustomFormats());
        assertFalse(env.hasCustomFormats());
        
        cfg.setCustomDateFormats(Collections.singletonMap("f", EpochMillisTemplateDateFormatFactory.INSTANCE));
        assertTrue(cfg.hasCustomFormats());
        assertTrue(t.hasCustomFormats());
        assertTrue(env.hasCustomFormats());
        
        cfg.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap());
        t.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap());
        env.setCustomDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap());
        assertFalse(cfg.hasCustomFormats());
        assertFalse(t.hasCustomFormats());
        assertFalse(env.hasCustomFormats());
        
        // Same with number formats:
        
        env.setCustomNumberFormats(Collections.singletonMap("f", HexTemplateNumberFormatFactory.INSTANCE));
        assertFalse(t.hasCustomFormats());
        assertTrue(env.hasCustomFormats());
        t.setCustomNumberFormats(Collections.singletonMap("f", HexTemplateNumberFormatFactory.INSTANCE));
        assertFalse(cfg.hasCustomFormats());
        assertTrue(t.hasCustomFormats());
        cfg.setCustomNumberFormats(Collections.singletonMap("f", HexTemplateNumberFormatFactory.INSTANCE));
        assertTrue(cfg.hasCustomFormats());
        assertTrue(t.hasCustomFormats());
        assertTrue(env.hasCustomFormats());
        
        cfg.setCustomNumberFormats(Collections.<String, TemplateNumberFormatFactory>emptyMap());
        t.setCustomNumberFormats(Collections.<String, TemplateNumberFormatFactory>emptyMap());
        env.setCustomNumberFormats(Collections.<String, TemplateNumberFormatFactory>emptyMap());
        assertFalse(cfg.hasCustomFormats());
        assertFalse(t.hasCustomFormats());
        assertFalse(env.hasCustomFormats());
        
        cfg.setCustomNumberFormats(Collections.singletonMap("f", HexTemplateNumberFormatFactory.INSTANCE));
        assertTrue(cfg.hasCustomFormats());
        assertTrue(t.hasCustomFormats());
        assertTrue(env.hasCustomFormats());
    }
    
    public void testNamingConventionSetSetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);

        assertEquals(Configuration.AUTO_DETECT_NAMING_CONVENTION, cfg.getNamingConvention());
        
        cfg.setSetting("naming_convention", "legacy");
        assertEquals(Configuration.LEGACY_NAMING_CONVENTION, cfg.getNamingConvention());
        
        cfg.setSetting("naming_convention", "camel_case");
        assertEquals(Configuration.CAMEL_CASE_NAMING_CONVENTION, cfg.getNamingConvention());
        
        cfg.setSetting("naming_convention", "auto_detect");
        assertEquals(Configuration.AUTO_DETECT_NAMING_CONVENTION, cfg.getNamingConvention());
    }

    public void testTagSyntaxSetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);

        // Default is "angle brackets":
        assertEquals(Configuration.ANGLE_BRACKET_TAG_SYNTAX, cfg.getTagSyntax());

        cfg.setSetting("tag_syntax", "angle_bracket");
        assertEquals(Configuration.ANGLE_BRACKET_TAG_SYNTAX, cfg.getTagSyntax());
        
        cfg.setSetting("tag_syntax", "square_bracket");
        assertEquals(Configuration.SQUARE_BRACKET_TAG_SYNTAX, cfg.getTagSyntax());
        
        cfg.setSetting("tag_syntax", "auto_detect");
        assertEquals(Configuration.AUTO_DETECT_TAG_SYNTAX, cfg.getTagSyntax());
        
        // Camel case:
        cfg.setSetting("tagSyntax", "squareBracket");
        assertEquals(Configuration.SQUARE_BRACKET_TAG_SYNTAX, cfg.getTagSyntax());
        
        try {
            cfg.setSetting("tag_syntax", "no_such_syntax");
            fail();
        } catch (TemplateException e) {
            assertThat(e.getMessage(), containsString("no_such_syntax"));
        }
        
        // Catch an oversight that's easy to do:
        try {
            cfg.setTagSyntax(Configuration.SQUARE_BRACKET_INTERPOLATION_SYNTAX);
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("SQUARE_BRACKET_TAG_SYNTAX"));
        }
    }
    
    public void testInterpolationSyntaxSetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);

        // Default is "legacy":
        assertEquals(Configuration.LEGACY_INTERPOLATION_SYNTAX, cfg.getInterpolationSyntax());
        
        cfg.setSetting("interpolation_syntax", "dollar");
        assertEquals(Configuration.DOLLAR_INTERPOLATION_SYNTAX, cfg.getInterpolationSyntax());
        
        cfg.setSetting("interpolation_syntax", "square_bracket");
        assertEquals(Configuration.SQUARE_BRACKET_INTERPOLATION_SYNTAX, cfg.getInterpolationSyntax());
        
        cfg.setSetting("interpolation_syntax", "legacy");
        assertEquals(Configuration.LEGACY_INTERPOLATION_SYNTAX, cfg.getInterpolationSyntax());
        
        // Camel case:
        cfg.setSetting("interpolationSyntax", "squareBracket");
        assertEquals(Configuration.SQUARE_BRACKET_INTERPOLATION_SYNTAX, cfg.getInterpolationSyntax());
        
        try {
            cfg.setSetting("interpolation_syntax", "no_such_syntax");
            fail();
        } catch (TemplateException e) {
            assertThat(e.getMessage(), containsString("no_such_syntax"));
        }

        // Catch an oversight that's easy to do:
        try {
            cfg.setInterpolationSyntax(Configuration.SQUARE_BRACKET_TAG_SYNTAX);
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("SQUARE_BRACKET_INTERPOLATION_SYNTAX"));
        }
    }
    
    public void testLazyImportsSetSetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);

        assertFalse(cfg.getLazyImports());
        assertTrue(cfg.isLazyImportsSet());
        cfg.setSetting("lazy_imports", "true");
        assertTrue(cfg.getLazyImports());
        cfg.setSetting("lazyImports", "false");
        assertFalse(cfg.getLazyImports());
    }
    
    public void testLazyAutoImportsSetSetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);

        assertNull(cfg.getLazyAutoImports());
        assertTrue(cfg.isLazyAutoImportsSet());
        cfg.setSetting("lazy_auto_imports", "true");
        assertEquals(Boolean.TRUE, cfg.getLazyAutoImports());
        assertTrue(cfg.isLazyAutoImportsSet());
        cfg.setSetting("lazyAutoImports", "false");
        assertEquals(Boolean.FALSE, cfg.getLazyAutoImports());
        cfg.setSetting("lazyAutoImports", "null");
        assertNull(cfg.getLazyAutoImports());
        assertTrue(cfg.isLazyAutoImportsSet());
    }
    
    public void testLocaleSetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        
        assertEquals(Locale.getDefault(), cfg.getLocale());
        assertFalse(cfg.isLocaleExplicitlySet());
        
        Locale nonDefault = Locale.getDefault().equals(Locale.GERMANY) ? Locale.FRANCE : Locale.GERMANY;
        cfg.setLocale(nonDefault);
        assertTrue(cfg.isLocaleExplicitlySet());
        assertEquals(nonDefault, cfg.getLocale());
        
        cfg.unsetLocale();
        assertEquals(Locale.getDefault(), cfg.getLocale());
        assertFalse(cfg.isLocaleExplicitlySet());
        
        cfg.setSetting(Configuration.LOCALE_KEY, "JVM default");
        assertEquals(Locale.getDefault(), cfg.getLocale());
        assertTrue(cfg.isLocaleExplicitlySet());
    }
    
    public void testDefaultEncodingSetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        
        String defaultFileEncoding = System.getProperty("file.encoding");
        assertNotNull(defaultFileEncoding);
        
        assertEquals(defaultFileEncoding, cfg.getDefaultEncoding());
        assertFalse(cfg.isDefaultEncodingExplicitlySet());
        
        String nonDefault = defaultFileEncoding.equalsIgnoreCase("UTF-8") ? "ISO-8859-1" : "UTF-8";
        cfg.setDefaultEncoding(nonDefault);
        assertTrue(cfg.isDefaultEncodingExplicitlySet());
        assertEquals(nonDefault, cfg.getDefaultEncoding());
        
        cfg.unsetDefaultEncoding();
        assertEquals(defaultFileEncoding, cfg.getDefaultEncoding());
        assertFalse(cfg.isDefaultEncodingExplicitlySet());
        
        cfg.setSetting(Configuration.DEFAULT_ENCODING_KEY, "JVM default");
        assertEquals(defaultFileEncoding, cfg.getDefaultEncoding());
        assertTrue(cfg.isDefaultEncodingExplicitlySet());
    }
    
    public void testTimeZoneSetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        
        assertEquals(TimeZone.getDefault(), cfg.getTimeZone());
        assertFalse(cfg.isTimeZoneExplicitlySet());
        
        TimeZone nonDefault = TimeZone.getDefault().equals(DateUtil.UTC) ? TimeZone.getTimeZone("PST") : DateUtil.UTC;
        cfg.setTimeZone(nonDefault);
        assertTrue(cfg.isTimeZoneExplicitlySet());
        assertEquals(nonDefault, cfg.getTimeZone());
        
        cfg.unsetTimeZone();
        assertEquals(TimeZone.getDefault(), cfg.getTimeZone());
        assertFalse(cfg.isTimeZoneExplicitlySet());
        
        cfg.setSetting(Configuration.TIME_ZONE_KEY, "JVM default");
        assertEquals(TimeZone.getDefault(), cfg.getTimeZone());
        assertTrue(cfg.isTimeZoneExplicitlySet());
    }
    
    public void testNewBuiltinClassResolverSetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        assertSame(TemplateClassResolver.UNRESTRICTED_RESOLVER, cfg.getNewBuiltinClassResolver());
        
        cfg.setSetting(Configuration.NEW_BUILTIN_CLASS_RESOLVER_KEY_SNAKE_CASE,
                "allowed_classes: com.example.C1, com.example.C2, trusted_templates: lib/*, safe.ftl");
        assertThat(cfg.getNewBuiltinClassResolver(), instanceOf(OptInTemplateClassResolver.class));
        
        cfg.setSetting(Configuration.NEW_BUILTIN_CLASS_RESOLVER_KEY_SNAKE_CASE, "safer");
        assertSame(TemplateClassResolver.SAFER_RESOLVER, cfg.getNewBuiltinClassResolver());
        
        cfg.setSetting(Configuration.NEW_BUILTIN_CLASS_RESOLVER_KEY_CAMEL_CASE,
                "allowedClasses: com.example.C1, com.example.C2, trustedTemplates: lib/*, safe.ftl");
        assertThat(cfg.getNewBuiltinClassResolver(), instanceOf(OptInTemplateClassResolver.class));
        
        cfg.setSetting(Configuration.NEW_BUILTIN_CLASS_RESOLVER_KEY_SNAKE_CASE, "allowsNothing");
        assertSame(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER, cfg.getNewBuiltinClassResolver());
        
        cfg.setSetting(Configuration.NEW_BUILTIN_CLASS_RESOLVER_KEY_SNAKE_CASE, "allows_nothing");
        assertSame(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER, cfg.getNewBuiltinClassResolver());
    }

    public void testTruncateBuiltinAlgorithm() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
        assertSame(DefaultTruncateBuiltinAlgorithm.ASCII_INSTANCE, cfg.getTruncateBuiltinAlgorithm());

        cfg.setSetting("truncateBuiltinAlgorithm", "unicodE");
        assertSame(DefaultTruncateBuiltinAlgorithm.UNICODE_INSTANCE, cfg.getTruncateBuiltinAlgorithm());

        cfg.setSetting("truncate_builtin_algorithm", "ASCII");
        assertSame(DefaultTruncateBuiltinAlgorithm.ASCII_INSTANCE, cfg.getTruncateBuiltinAlgorithm());

        {
            cfg.setSetting("truncate_builtin_algorithm",
                    "DefaultTruncateBuiltinAlgorithm('...', false)");
            DefaultTruncateBuiltinAlgorithm alg =
                    (DefaultTruncateBuiltinAlgorithm) cfg.getTruncateBuiltinAlgorithm();
            assertEquals("...", alg.getDefaultTerminator());
            assertFalse(alg.getAddSpaceAtWordBoundary());
            assertEquals(3, alg.getDefaultTerminatorLength());
            assertNull(alg.getDefaultMTerminator());
            assertNull(alg.getDefaultMTerminatorLength());
            assertEquals(
                    DefaultTruncateBuiltinAlgorithm.DEFAULT_WORD_BOUNDARY_MIN_LENGTH,
                    alg.getWordBoundaryMinLength(),
                    0);
        }

        {
            cfg.setSetting("truncate_builtin_algorithm",
                    "DefaultTruncateBuiltinAlgorithm(" +
                            "'...', " +
                            "markup(HTMLOutputFormat(), '<span class=trunc>...</span>'), " +
                            "true)");
            DefaultTruncateBuiltinAlgorithm alg =
                    (DefaultTruncateBuiltinAlgorithm) cfg.getTruncateBuiltinAlgorithm();
            assertEquals("...", alg.getDefaultTerminator());
            assertTrue(alg.getAddSpaceAtWordBoundary());
            assertEquals(3, alg.getDefaultTerminatorLength());
            assertEquals("markupOutput(format=HTML, markup=<span class=trunc>...</span>)",
                    alg.getDefaultMTerminator().toString());
            assertEquals(Integer.valueOf(3), alg.getDefaultMTerminatorLength());
            assertEquals(
                    DefaultTruncateBuiltinAlgorithm.DEFAULT_WORD_BOUNDARY_MIN_LENGTH,
                    alg.getWordBoundaryMinLength(),
                    0);
        }

        {
            cfg.setSetting("truncate_builtin_algorithm",
                    "DefaultTruncateBuiltinAlgorithm(" +
                            "DefaultTruncateBuiltinAlgorithm.STANDARD_ASCII_TERMINATOR, null, null, " +
                            "DefaultTruncateBuiltinAlgorithm.STANDARD_M_TERMINATOR, null, null, " +
                            "true, 0.5)");
            DefaultTruncateBuiltinAlgorithm alg =
                    (DefaultTruncateBuiltinAlgorithm) cfg.getTruncateBuiltinAlgorithm();
            assertEquals(DefaultTruncateBuiltinAlgorithm.STANDARD_ASCII_TERMINATOR, alg.getDefaultTerminator());
            assertTrue(alg.getAddSpaceAtWordBoundary());
            assertEquals(5, alg.getDefaultTerminatorLength());
            assertEquals(DefaultTruncateBuiltinAlgorithm.STANDARD_M_TERMINATOR.toString(),
                    alg.getDefaultMTerminator().toString());
            assertEquals(Integer.valueOf(3), alg.getDefaultMTerminatorLength());
            assertEquals(0.5, alg.getWordBoundaryMinLength());
        }
    }

    public void testCFormat() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_21);

        assertSame(LegacyCFormat.INSTANCE, cfg.getCFormat());
        cfg.setSetting(Configuration.C_FORMAT_KEY_SNAKE_CASE, LegacyCFormat.NAME);
        assertSame(LegacyCFormat.INSTANCE, cfg.getCFormat());
        cfg.setSetting(Configuration.C_FORMAT_KEY_CAMEL_CASE, JSONCFormat.NAME);
        assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());

        cfg.setSetting(Configuration.C_FORMAT_KEY_CAMEL_CASE, "default");
        cfg.setSetting(Configuration.C_FORMAT_KEY_SNAKE_CASE, LegacyCFormat.NAME);

        for (CFormat standardCFormat : new CFormat[] {
                        LegacyCFormat.INSTANCE,
                        JSONCFormat.INSTANCE, JavaScriptCFormat.INSTANCE, JavaScriptOrJSONCFormat.INSTANCE,
                        JavaCFormat.INSTANCE, XSCFormat.INSTANCE
                }) {
            cfg.setSetting(Configuration.C_FORMAT_KEY, standardCFormat.getName());
            assertSame(standardCFormat, cfg.getCFormat());
        }

        // Object Builder value:
        cfg.setSetting(Configuration.C_FORMAT_KEY, JSONCFormat.class.getName() + "()");
        assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());
    }

    public void testFallbackOnNullLoopVariable() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_29);
        assertTrue(cfg.getFallbackOnNullLoopVariable());

        cfg.setSetting("fallback_on_null_loop_variable", "false");
        assertFalse(cfg.getFallbackOnNullLoopVariable());

        cfg.setSetting("fallback_on_null_loop_variable", "true");
        assertTrue(cfg.getFallbackOnNullLoopVariable());

        cfg.setSetting("fallbackOnNullLoopVariable", "NO");
        assertFalse(cfg.getFallbackOnNullLoopVariable());
    }

    public static final MemberAccessPolicy CONFIG_TEST_MEMBER_ACCESS_POLICY;
    static {
        try {
            CONFIG_TEST_MEMBER_ACCESS_POLICY = new WhitelistMemberAccessPolicy(MemberSelectorListMemberAccessPolicy.MemberSelector.parse(
                    ImmutableList.of(
                            File.class.getName() + ".getName()",
                            File.class.getName() + ".isFile()"),
                    false,
                    ConfigurationTest.class.getClassLoader()));
        } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void testMemberAccessPolicySetting() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
        cfg.setSetting(
                "objectWrapper",
                "DefaultObjectWrapper(2.3.30, "
                        + "memberAccessPolicy="
                        + ConfigurationTest.class.getName() + ".CONFIG_TEST_MEMBER_ACCESS_POLICY"
                        + ")");
        TemplateHashModel m = (TemplateHashModel) cfg.getObjectWrapper().wrap(new File("x"));
        assertNotNull(m.get("getName"));
        assertNotNull(m.get("isFile"));
        assertNull(m.get("delete"));
    }

    @Test
    public void testMemberAccessPolicySetting2() throws TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
        cfg.setSetting(
                "objectWrapper",
                "DefaultObjectWrapper(2.3.30, "
                        + "memberAccessPolicy=" + LegacyDefaultMemberAccessPolicy.class.getName() + "())");
        assertSame(((DefaultObjectWrapper) cfg.getObjectWrapper()).getMemberAccessPolicy(),
                LegacyDefaultMemberAccessPolicy.INSTANCE);
    }

    @Test
    public void testGetSettingNamesAreSorted() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        for (boolean camelCase : new boolean[] { false, true }) {
            List<String> names = new ArrayList<>(cfg.getSettingNames(camelCase));
            List<String> cfgableNames = new ArrayList<>(new Template(null, "", cfg).getSettingNames(camelCase));
            assertStartsWith(names, cfgableNames);
            
            String prevName = null;
            for (int i = cfgableNames.size(); i < names.size(); i++) {
                String name = names.get(i);
                if (prevName != null) {
                    assertThat(name, greaterThan(prevName));
                }
                prevName = name;
            }
        }
    }

    @Test
    public void testGetSettingNamesNameConventionsContainTheSame() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        ConfigurableTest.testGetSettingNamesNameConventionsContainTheSame(
                new ArrayList<>(cfg.getSettingNames(false)),
                new ArrayList<>(cfg.getSettingNames(true)));
    }

    @Test
    public void testStaticFieldKeysCoverAllGetSettingNames() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        List<String> names = new ArrayList<>(cfg.getSettingNames(false));
        List<String> cfgableNames = new ArrayList<>(cfg.getSettingNames(false));
        assertStartsWith(names, cfgableNames);
        
        for (int i = cfgableNames.size(); i < names.size(); i++) {
            String name = names.get(i);
            assertTrue("No field was found for " + name, keyFieldExists(name));
        }
    }
    
    @Test
    public void testGetSettingNamesCoversAllStaticKeyFields() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        Collection<String> names = cfg.getSettingNames(false);
        
        for (Class<? extends Configurable> cfgableClass : new Class[] { Configuration.class, Configurable.class }) {
            for (Field f : cfgableClass.getFields()) {
                if (f.getName().endsWith("_KEY")) {
                    final Object name = f.get(null);
                    assertTrue("Missing setting name: " + name, names.contains(name));
                }
            }
        }
    }
    
    @Test
    public void testKeyStaticFieldsHasAllVariationsAndCorrectFormat() throws IllegalArgumentException, IllegalAccessException {
        ConfigurableTest.testKeyStaticFieldsHasAllVariationsAndCorrectFormat(Configuration.class);
    }

    @Test
    public void testGetSettingNamesCoversAllSettingNames() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        Collection<String> names = cfg.getSettingNames(false);
        
        for (Field f : Configurable.class.getFields()) {
            if (f.getName().endsWith("_KEY")) {
                final Object name = f.get(null);
                assertTrue("Missing setting name: " + name, names.contains(name));
            }
        }
    }

    @Test
    public void testSetSettingSupportsBothNamingConventions() throws Exception {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
        
        cfg.setSetting(Configuration.DEFAULT_ENCODING_KEY_CAMEL_CASE, "UTF-16LE");
        assertEquals("UTF-16LE", cfg.getDefaultEncoding());
        cfg.setSetting(Configuration.DEFAULT_ENCODING_KEY_SNAKE_CASE, "UTF-8");
        assertEquals("UTF-8", cfg.getDefaultEncoding());
        
        for (String nameCC : cfg.getSettingNames(true)) {
            for (String value : new String[] { "1", "default", "true" }) {
                Exception resultCC = null;
                try {
                    cfg.setSetting(nameCC, value);
                } catch (Exception e) {
                    assertThat(e, not(instanceOf(UnknownSettingException.class)));
                    resultCC = e;
                }
                
                String nameSC = _CoreStringUtils.camelCaseToUnderscored(nameCC);
                Exception resultSC = null;
                try {
                    cfg.setSetting(nameSC, value);
                } catch (Exception e) {
                    assertThat(e, not(instanceOf(UnknownSettingException.class)));
                    resultSC = e;
                }
                
                if (resultCC == null) {
                    assertNull(resultSC);
                } else {
                    assertNotNull(resultSC);
                    assertEquals(resultCC.getClass(), resultSC.getClass());
                }
            }
        }
    }
    
    @Test
    public void testGetSupportedBuiltInDirectiveNames() {
        Configuration cfg = new Configuration();
        
        Set<String> allNames = cfg.getSupportedBuiltInDirectiveNames(Configuration.AUTO_DETECT_NAMING_CONVENTION);
        Set<String> lNames = cfg.getSupportedBuiltInDirectiveNames(Configuration.LEGACY_NAMING_CONVENTION);
        Set<String> cNames = cfg.getSupportedBuiltInDirectiveNames(Configuration.CAMEL_CASE_NAMING_CONVENTION);
        
        checkNamingConventionNameSets(allNames, lNames, cNames);
        
        for (String name : cNames) {
            assertThat(name.toLowerCase(), isIn(lNames));
        }
    }

    @Test
    public void testGetSupportedBuiltInNames() {
        Configuration cfg = new Configuration();
        
        Set<String> allNames = cfg.getSupportedBuiltInNames(Configuration.AUTO_DETECT_NAMING_CONVENTION);
        Set<String> lNames = cfg.getSupportedBuiltInNames(Configuration.LEGACY_NAMING_CONVENTION);
        Set<String> cNames = cfg.getSupportedBuiltInNames(Configuration.CAMEL_CASE_NAMING_CONVENTION);
        
        checkNamingConventionNameSets(allNames, lNames, cNames);
    }

    private void checkNamingConventionNameSets(Set<String> allNames, Set<String> lNames, Set<String> cNames) {
        for (String name : lNames) {
            assertThat(allNames, hasItem(name));
            assertTrue("Should be all-lowercase: " + name, name.equals(name.toLowerCase()));
        }
        for (String name : cNames) {
            assertThat(allNames, hasItem(name));
        }
        for (String name : allNames) {
            assertThat(name, anyOf(isIn(lNames), isIn(cNames)));
        }
        assertEquals(lNames.size(), cNames.size());
    }
    
    @SuppressWarnings("boxing")
    private void assertStartsWith(List<String> list, List<String> headList) {
        int index = 0;
        for (String name : headList) {
            assertThat(index, lessThan(list.size()));
            assertEquals(name, list.get(index));
            index++;
        }
    }

    private boolean keyFieldExists(String name) throws Exception {
        Field field;
        try {
            field = Configuration.class.getField(name.toUpperCase() + "_KEY");
        } catch (NoSuchFieldException e) {
            return false;
        }
        assertEquals(name, field.get(null));
        return true;
    }
    
    private static class MyScalarModel implements TemplateScalarModel {

        public String getAsString() throws TemplateModelException {
            return "my";
        }
        
    }
    
}
