/*
 * 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 freemarker.test.hamcerst.Matchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.hamcrest.Matchers;
import org.junit.Test;
import org.python.core.PyString;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

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

import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.EnumerationModel;
import freemarker.ext.beans.HashAdapter;
import freemarker.ext.beans.WhitelistMemberAccessPolicy;
import freemarker.ext.jython.JythonSequenceModel;
import freemarker.ext.util.WrapperTemplateModel;

public class DefaultObjectWrapperTest {

    private final static DefaultObjectWrapper OW0 = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_0)
            .build();

    private final static DefaultObjectWrapper OW22 = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_22)
            .build();

    private final static DefaultObjectWrapper OW22NM = new DefaultObjectWrapper(Configuration.VERSION_2_3_22);
    static {
        OW22NM.setNullModel(NullModel.INSTANCE);
    }

    private final static DefaultObjectWrapper OW22CA = new DefaultObjectWrapper(Configuration.VERSION_2_3_22);
    static {
        OW22CA.setForceLegacyNonListCollections(false);
    }

    private final static DefaultObjectWrapper OW22IS = new DefaultObjectWrapper(Configuration.VERSION_2_3_22);
    static {
        OW22IS.setIterableSupport(true);
    }
    
    @Test
    public void testIncompatibleImprovementsVersionBreakPoints() throws Exception {
        List<Version> expected = new ArrayList<>();
        for (int u = 0; u < 21; u++) {
            expected.add(Configuration.VERSION_2_3_0);
        }
        expected.add(Configuration.VERSION_2_3_21);
        expected.add(Configuration.VERSION_2_3_22);
        expected.add(Configuration.VERSION_2_3_22); // no non-BC change in 2.3.23
        expected.add(Configuration.VERSION_2_3_24);
        expected.add(Configuration.VERSION_2_3_24); // no non-BC change in 2.3.25
        expected.add(Configuration.VERSION_2_3_26);
        expected.add(Configuration.VERSION_2_3_27);
        expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.28
        expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.29
        expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.30
        expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.31
        expected.add(Configuration.VERSION_2_3_27); // no non-BC change in 2.3.32

        List<Version> actual = new ArrayList<>();
        for (int i = _VersionInts.V_2_3_0; i <= Configuration.getVersion().intValue(); i++) {
            int major = i / 1000000;
            int minor = i % 1000000 / 1000;
            int micro = i % 1000;
            final Version version = new Version(major, minor, micro);
            final Version normalizedVersion = DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(version);
            actual.add(normalizedVersion);

            final DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(version);
            assertEquals(normalizedVersion, builder.getIncompatibleImprovements());
            assertEquals(normalizedVersion, builder.build().getIncompatibleImprovements());
        }

        assertEquals(expected, actual);
    }

    @Test
    public void testIncompatibleImprovementsVersionOutOfBounds() throws Exception {
        try {
            DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(new Version(2, 2, 0));
            fail();
        } catch (IllegalArgumentException e) {
            // expected
        }

        Version curVersion = Configuration.getVersion();
        final Version futureVersion = new Version(curVersion.getMajor(), curVersion.getMicro(),
                curVersion.getMicro() + 1);
        try {
            DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(futureVersion);
            fail();
        } catch (IllegalArgumentException e) {
            // expected
        }
        try {
            new DefaultObjectWrapperBuilder(futureVersion);
            fail();
        } catch (IllegalArgumentException e) {
            // expected
        }
    }

    @SuppressWarnings("boxing")
    @Test
    public void testBuilder() throws Exception {
        {
            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_19);
            DefaultObjectWrapper bw = builder.build();
            assertSame(bw, builder.build());
            assertSame(bw.getClass(), DefaultObjectWrapper.class);
            assertEquals(Configuration.VERSION_2_3_0, bw.getIncompatibleImprovements());
            assertTrue(bw.isWriteProtected());
            assertTrue(bw.isClassIntrospectionCacheRestricted());

            assertFalse(bw.getUseAdaptersForContainers());
            assertTrue(bw.getForceLegacyNonListCollections());
            assertTrue(bw.wrap(new HashMap()) instanceof SimpleHash);
            assertTrue(bw.wrap(new ArrayList()) instanceof SimpleSequence);
            assertTrue(bw.wrap(new String[] {}) instanceof SimpleSequence);
            assertTrue(bw.wrap(new HashSet()) instanceof SimpleSequence);

        }

        for (boolean simpleMapWrapper : new boolean[] { true, false }) {
            {
                DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_21);
                builder.setSimpleMapWrapper(simpleMapWrapper); // Shouldn't mater
                DefaultObjectWrapper bw = builder.build();
                assertSame(bw, builder.build());
                assertSame(bw.getClass(), DefaultObjectWrapper.class);
                assertEquals(Configuration.VERSION_2_3_21, bw.getIncompatibleImprovements());
                assertTrue(bw.isWriteProtected());
                assertEquals(simpleMapWrapper, bw.isSimpleMapWrapper());
                assertFalse(bw.getUseAdaptersForContainers());
                assertTrue(bw.getForceLegacyNonListCollections());
                assertTrue(bw.wrap(new HashMap()) instanceof SimpleHash);
                assertTrue(bw.wrap(new ArrayList()) instanceof SimpleSequence);
                assertTrue(bw.wrap(new String[] {}) instanceof SimpleSequence);
                assertTrue(bw.wrap(new HashSet()) instanceof SimpleSequence);
                assertTrue(bw.wrap('c') instanceof TemplateScalarModel); // StringModel now, but should change later
            }

            {
                DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_22);
                builder.setSimpleMapWrapper(simpleMapWrapper); // Shouldn't mater
                DefaultObjectWrapper bw = builder.build();
                assertSame(bw, builder.build());
                assertSame(bw.getClass(), DefaultObjectWrapper.class);
                assertEquals(Configuration.VERSION_2_3_22, bw.getIncompatibleImprovements());
                assertTrue(bw.isWriteProtected());
                assertEquals(simpleMapWrapper, bw.isSimpleMapWrapper());
                assertTrue(bw.getUseAdaptersForContainers());
                assertTrue(bw.getForceLegacyNonListCollections());
                assertTrue(bw.wrap(new HashMap()) instanceof DefaultMapAdapter);
                assertTrue(bw.wrap(new ArrayList()) instanceof DefaultListAdapter);
                assertTrue(bw.wrap(new String[] {}) instanceof DefaultArrayAdapter);
                assertTrue(bw.wrap(new HashSet()) instanceof SimpleSequence);
            }
        }

        {
            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.getVersion());
            builder.setSimpleMapWrapper(true);
            BeansWrapper bw = builder.build();
            assertSame(bw, builder.build());
            assertSame(bw.getClass(), DefaultObjectWrapper.class);
            assertEquals(
                    DefaultObjectWrapper.normalizeIncompatibleImprovementsVersion(Configuration.getVersion()),
                    bw.getIncompatibleImprovements());
            assertTrue(bw.isWriteProtected());
            assertTrue(bw.isSimpleMapWrapper());
            assertTrue(bw.wrap(new HashMap()) instanceof DefaultMapAdapter);
        }

        {
            DefaultObjectWrapper bw = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_19).build();
            assertSame(bw.getClass(), DefaultObjectWrapper.class);
            assertEquals(Configuration.VERSION_2_3_0, bw.getIncompatibleImprovements());
            assertTrue(bw.isWriteProtected());
            assertFalse(bw.isSimpleMapWrapper());
            assertTrue(bw.wrap(new HashMap()) instanceof SimpleHash);

            assertSame(bw, new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_20).build());
            assertSame(bw, new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_0).build());
            assertSame(bw, new DefaultObjectWrapperBuilder(new Version(2, 3, 5)).build());
        }

        {
            DefaultObjectWrapper bw = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_21).build();
            assertSame(bw.getClass(), DefaultObjectWrapper.class);
            assertEquals(Configuration.VERSION_2_3_21, bw.getIncompatibleImprovements());
            assertTrue(bw.isWriteProtected());
            assertFalse(bw.isSimpleMapWrapper());
            assertTrue(bw.wrap(new HashMap()) instanceof SimpleHash);
            assertTrue(bw.isClassIntrospectionCacheRestricted());

            assertSame(bw, new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_21).build());
        }

        {
            DefaultObjectWrapper bw = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_19).build();
            assertEquals(Configuration.VERSION_2_3_0, bw.getIncompatibleImprovements());
        }

        {
            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_19);
            builder.setExposureLevel(BeansWrapper.EXPOSE_PROPERTIES_ONLY);
            DefaultObjectWrapper bw = builder.build();
            DefaultObjectWrapper bw2 = builder.build();
            assertSame(bw, bw2); // not cached

            assertSame(bw.getClass(), DefaultObjectWrapper.class);
            assertEquals(Configuration.VERSION_2_3_0, bw.getIncompatibleImprovements());
            assertTrue(bw.isWriteProtected());
            assertFalse(bw.isSimpleMapWrapper());
            assertTrue(bw.wrap(new HashMap()) instanceof SimpleHash);
            assertEquals(BeansWrapper.EXPOSE_PROPERTIES_ONLY, bw.getExposureLevel());
        }

        {
            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_19);
            builder.setExposeFields(true);
            BeansWrapper bw = builder.build();
            BeansWrapper bw2 = builder.build();
            assertSame(bw, bw2); // not cached

            assertSame(bw.getClass(), DefaultObjectWrapper.class);
            assertEquals(Configuration.VERSION_2_3_0, bw.getIncompatibleImprovements());
            assertTrue(bw.isWriteProtected());
            assertFalse(bw.isSimpleMapWrapper());
            assertTrue(bw.wrap(new HashMap()) instanceof SimpleHash);
            assertEquals(true, bw.isExposeFields());
        }

        {
            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_21);
            builder.setForceLegacyNonListCollections(false);
            DefaultObjectWrapper bw = builder.build();
            assertSame(bw, builder.build());
            assertFalse(bw.getForceLegacyNonListCollections());

            assertTrue(bw.wrap(new HashMap()) instanceof SimpleHash);
            assertTrue(bw.wrap(new ArrayList()) instanceof SimpleSequence);
            assertTrue(bw.wrap(new String[] {}) instanceof SimpleSequence);
            assertTrue(bw.wrap(new HashSet()) instanceof SimpleSequence);
        }

        {
            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_22);
            builder.setForceLegacyNonListCollections(false);
            DefaultObjectWrapper bw = builder.build();
            assertSame(bw, builder.build());
            assertFalse(bw.getForceLegacyNonListCollections());

            assertTrue(bw.wrap(new HashMap()) instanceof DefaultMapAdapter);
            assertTrue(bw.wrap(new ArrayList()) instanceof DefaultListAdapter);
            assertTrue(bw.wrap(new String[] {}) instanceof DefaultArrayAdapter);
            assertTrue(bw.wrap(new HashSet()) instanceof DefaultNonListCollectionAdapter);
        }
        
        {
            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.getVersion());
            DefaultObjectWrapper bw = builder.build();
            assertSame(bw, builder.build());
            assertFalse(bw.getIterableSupport());

            assertFalse(bw.wrap(new PureIterable()) instanceof DefaultIterableAdapter);
        }
        {
            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.getVersion());
            builder.setIterableSupport(true);
            DefaultObjectWrapper bw = builder.build();
            assertSame(bw, builder.build());
            assertTrue(bw.getIterableSupport());

            assertTrue(bw.wrap(new PureIterable()) instanceof DefaultIterableAdapter);
        }

        {
            DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.getVersion());

            DefaultObjectWrapper bwDefault = builder.build();
            assertSame(bwDefault, builder.build());

            WhitelistMemberAccessPolicy memberAccessPolicy =
                    new WhitelistMemberAccessPolicy(
                            WhitelistMemberAccessPolicy.MemberSelector.parse(
                                    Arrays.asList(SomeBean.class.getName() + ".getX()"), false,
                                    DefaultObjectWrapperTest.class.getClassLoader()));
            builder.setMemberAccessPolicy(memberAccessPolicy);
            DefaultObjectWrapper bw = builder.build();
            assertNotSame(bw, bwDefault);
            assertSame(bw, builder.build());
            assertSame(bw.getMemberAccessPolicy(), memberAccessPolicy);

            TemplateHashModel m = (TemplateHashModel) bw.wrap(new SomeBean());
            assertNotNull(m.get("x"));
            assertNotNull(m.get("getX"));
            assertNull(m.get("y"));
            assertNull(m.get("getY"));
        }
    }
    
    @Test
    public void testConstructors() throws Exception {
        {
            DefaultObjectWrapper ow = new DefaultObjectWrapper();
            assertEquals(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS, ow.getIncompatibleImprovements());
            assertFalse(ow.isWriteProtected());
            assertFalse(ow.getUseAdaptersForContainers());
            assertTrue(ow.getForceLegacyNonListCollections());
        }

        {
            DefaultObjectWrapper ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_20);
            assertEquals(Configuration.VERSION_2_3_0, ow.getIncompatibleImprovements());
            assertFalse(ow.isWriteProtected());
            assertFalse(ow.getUseAdaptersForContainers());
            assertTrue(ow.getForceLegacyNonListCollections());
        }

        {
            DefaultObjectWrapper ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_21);
            assertEquals(Configuration.VERSION_2_3_21, ow.getIncompatibleImprovements());
            assertFalse(ow.isWriteProtected());
            assertFalse(ow.getUseAdaptersForContainers());
            assertTrue(ow.getForceLegacyNonListCollections());
        }
        
        {
            DefaultObjectWrapper ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_22);
            assertEquals(Configuration.VERSION_2_3_22, ow.getIncompatibleImprovements());
            assertFalse(ow.isWriteProtected());
            assertTrue(ow.getUseAdaptersForContainers());
            assertTrue(ow.getForceLegacyNonListCollections());
        }
        
        try {
            new DefaultObjectWrapper(new Version(99, 9, 9));
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), containsString("version"));
        }
    }
    
    
    @Test
    public void testCustomization() throws TemplateModelException {
        {
            CustomizedDefaultObjectWrapper ow = new CustomizedDefaultObjectWrapper(Configuration.VERSION_2_3_22);
            assertEquals(Configuration.VERSION_2_3_22, ow.getIncompatibleImprovements());
            assertTrue(ow.getUseAdaptersForContainers());
            testCustomizationCommonPart(ow,
                    DefaultMapAdapter.class, DefaultListAdapter.class, DefaultArrayAdapter.class);
        }
        
        {
            CustomizedDefaultObjectWrapper ow = new CustomizedDefaultObjectWrapper(Configuration.VERSION_2_3_21);
            assertFalse(ow.getUseAdaptersForContainers());
            assertEquals(Configuration.VERSION_2_3_21, ow.getIncompatibleImprovements());
            testCustomizationCommonPart(ow,
                    SimpleHash.class, SimpleSequence.class, SimpleSequence.class);
        }
        
        {
            CustomizedDefaultObjectWrapper ow = new CustomizedDefaultObjectWrapper(Configuration.VERSION_2_3_20);
            assertFalse(ow.getUseAdaptersForContainers());
            assertEquals(Configuration.VERSION_2_3_0, ow.getIncompatibleImprovements());
            testCustomizationCommonPart(ow,
                    SimpleHash.class, SimpleSequence.class, SimpleSequence.class);
        }
    }

    @SuppressWarnings("boxing")
    private void testCustomizationCommonPart(CustomizedDefaultObjectWrapper ow,
            Class<? extends TemplateHashModel> mapTMClass,
            Class<? extends TemplateSequenceModel> listTMClass,
            Class<? extends TemplateSequenceModel> arrayTMClass)
            throws TemplateModelException {
        assertFalse(ow.isWriteProtected());
        
        TemplateSequenceModel seq = (TemplateSequenceModel) ow.wrap(new Tupple(11, 22));
        assertEquals(2, seq.size());
        assertEquals(11, ow.unwrap(seq.get(0)));
        assertEquals(22, ow.unwrap(seq.get(1)));
        
        assertTrue(ow.wrap("x") instanceof SimpleScalar);
        assertTrue(ow.wrap(1.5) instanceof SimpleNumber);
        assertTrue(ow.wrap(new Date()) instanceof SimpleDate);
        assertEquals(TemplateBooleanModel.TRUE, ow.wrap(true));
        
        assertTrue(mapTMClass.isInstance(ow.wrap(Collections.emptyMap())));
        assertTrue(listTMClass.isInstance(ow.wrap(Collections.emptyList())));
        assertTrue(arrayTMClass.isInstance(ow.wrap(new boolean[] { })));
        assertTrue(ow.wrap(new HashSet()) instanceof SimpleSequence);  // at least until IcI 2.4
        assertTrue(ow.wrap('c') instanceof TemplateScalarModel); // StringModel right now, but should change later
        
        TemplateHashModel bean = (TemplateHashModel) ow.wrap(new TestBean());
        assertEquals(1, ow.unwrap(bean.get("x")));
        {
            // Check method calls, and also if the return value is wrapped with the overidden "wrap".
            final TemplateModel mr = (TemplateModel) ((TemplateMethodModelEx) bean.get("m")).exec(Collections.emptyList());
            assertEquals(
                    Collections.singletonList(1),
                    ow.unwrap(mr));
            assertTrue(listTMClass.isInstance(mr));
        }
        {
            // Check custom TM usage and round trip:
            final TemplateModel mr = (TemplateModel) ((TemplateMethodModelEx) bean.get("incTupple"))
                    .exec(Collections.singletonList(ow.wrap(new Tupple<>(1, 2))));
            assertEquals(new Tupple<>(2, 3), ow.unwrap(mr));
            assertTrue(TuppleAdapter.class.isInstance(mr));
        }
    }

    @SuppressWarnings("boxing")
    @Test
    public void testRoundtripping() throws TemplateModelException, ClassNotFoundException {
        DefaultObjectWrapper dow21 = new DefaultObjectWrapper(Configuration.VERSION_2_3_21);
        DefaultObjectWrapper dow22 = new DefaultObjectWrapper(Configuration.VERSION_2_3_22);

        final Map hashMap = new HashMap();
        inintTestMap(hashMap);
        final Map treeMap = new TreeMap();
        inintTestMap(treeMap);
        final Map linkedHashMap = new LinkedHashMap();
        inintTestMap(linkedHashMap);
        final Map gMap = ImmutableMap.<String, Object> of("a", 1, "b", 2, "c", 3);
        final LinkedList linkedList = new LinkedList();
        linkedList.add("a");
        linkedList.add("b");
        linkedList.add("c");
        final int[] intArray = new int[] { 1, 2, 3 };
        final String[] stringArray = new String[] { "a", "b", "c" };

        assertRoundtrip(dow21, linkedHashMap, SimpleHash.class, HashAdapter.class, linkedHashMap.toString());
        assertRoundtrip(dow21, treeMap, SimpleHash.class, HashAdapter.class, treeMap.toString());
        assertRoundtrip(dow21, gMap, SimpleHash.class, HashAdapter.class, hashMap.toString());
        assertRoundtrip(dow21, linkedList, SimpleSequence.class, Class.forName("freemarker.ext.beans.SequenceAdapter"),
                linkedList.toString());
        assertRoundtrip(dow21, intArray, SimpleSequence.class, Class.forName("freemarker.ext.beans.SequenceAdapter"),
                "[1, 2, 3]");
        assertRoundtrip(dow21, stringArray, SimpleSequence.class,
                Class.forName("freemarker.ext.beans.SequenceAdapter"),
                "[a, b, c]");

        assertRoundtrip(dow22, linkedHashMap, DefaultMapAdapter.class, LinkedHashMap.class, linkedHashMap.toString());
        assertRoundtrip(dow22, treeMap, DefaultMapAdapter.class, TreeMap.class, treeMap.toString());
        assertRoundtrip(dow22, gMap, DefaultMapAdapter.class, ImmutableMap.class, gMap.toString());
        assertRoundtrip(dow22, linkedList, DefaultListAdapter.class, LinkedList.class, linkedList.toString());
        assertRoundtrip(dow22, intArray, DefaultArrayAdapter.class, int[].class, null);
        assertRoundtrip(dow22, stringArray, DefaultArrayAdapter.class, String[].class, null);
    }

    @SuppressWarnings("boxing")
    private void inintTestMap(Map map) {
        map.put("a", 1);
        map.put("b", 2);
        map.put("c", 3);
    }

    @SuppressWarnings("boxing")
    @Test
    public void testMapAdapter() throws TemplateModelException {
        HashMap<String, Object> testMap = new LinkedHashMap<>();
        testMap.put("a", 1);
        testMap.put("b", null);
        testMap.put("c", "C");
        testMap.put("d", Collections.singletonList("x"));

        {
            TemplateHashModelEx hash = (TemplateHashModelEx) OW22.wrap(testMap);
            assertEquals(4, hash.size());
            assertFalse(hash.isEmpty());
            assertNull(hash.get("e"));
            assertEquals(1, ((TemplateNumberModel) hash.get("a")).getAsNumber());
            assertNull(hash.get("b"));
            assertEquals("C", ((TemplateScalarModel) hash.get("c")).getAsString());
            assertTrue(hash.get("d") instanceof DefaultListAdapter);

            assertCollectionTMEquals(hash.keys(), "a", "b", "c", "d");
            assertCollectionTMEquals(hash.values(), 1, null, "C", Collections.singletonList("x"));
            
            assertSizeThroughAPIModel(4, hash);
        }

        {
            assertTrue(((TemplateHashModel) OW22.wrap(Collections.emptyMap())).isEmpty());
        }

        {
            final TemplateHashModelEx hash = (TemplateHashModelEx) OW22NM.wrap(testMap);
            assertSame(NullModel.INSTANCE, hash.get("b"));
            assertNull(hash.get("e"));

            assertCollectionTMEquals(hash.keys(), "a", "b", "c", "d");
            assertCollectionTMEquals(hash.values(), 1, null, "C", Collections.singletonList("x"));
        }
    }

    private void assertCollectionTMEquals(TemplateCollectionModel coll, Object... expectedItems)
            throws TemplateModelException {
        for (int i = 0; i < 2; i++) { // Run twice to check if we always get a new iterator
            int idx = 0;
            TemplateModelIterator it2 = null;
            for (TemplateModelIterator it = coll.iterator(); it.hasNext(); ) {
                TemplateModel actualItem = it.next();
                if (idx >= expectedItems.length) {
                    fail("Number of items is more than the expected " + expectedItems.length);
                }
                assertEquals(expectedItems[idx], OW22.unwrap(actualItem));
                if (i == 1) {
                    // In the 2nd round we also test with two iterators in parallel.
                    // This 2nd iterator is also special in that its hasNext() is never called.
                    if (it2 == null) {
                        it2 = coll.iterator();
                    }
                    assertEquals(expectedItems[idx], OW22.unwrap(it2.next()));
                }
                idx++;
            }
            if (expectedItems.length != idx) {
                fail("Number of items is " + idx + ", which is less than the expected " + expectedItems.length);
            }
        }
    }

    @SuppressWarnings("boxing")
    @Test
    public void testListAdapter() throws TemplateModelException {
        {
            List testList = new ArrayList<Object>();
            testList.add(1);
            testList.add(null);
            testList.add("c");
            testList.add(new String[] { "x" });

            TemplateSequenceModel seq = (TemplateSequenceModel) OW22.wrap(testList);
            assertTrue(seq instanceof DefaultListAdapter);
            assertFalse(seq instanceof TemplateCollectionModel); // Maybe changes at 2.4.0
            assertEquals(4, seq.size());
            assertNull(seq.get(-1));
            assertEquals(1, ((TemplateNumberModel) seq.get(0)).getAsNumber());
            assertNull(seq.get(1));
            assertEquals("c", ((TemplateScalarModel) seq.get(2)).getAsString());
            assertTrue(seq.get(3) instanceof DefaultArrayAdapter);
            assertNull(seq.get(4));
            
            assertSizeThroughAPIModel(4, seq);
        }

        {
            List testList = new LinkedList<Object>();
            testList.add(1);
            testList.add(null);
            testList.add("c");

            TemplateSequenceModel seq = (TemplateSequenceModel) OW22.wrap(testList);
            assertTrue(seq instanceof DefaultListAdapter);
            assertTrue(seq instanceof TemplateCollectionModel); // Maybe changes at 2.4.0
            assertEquals(3, seq.size());
            assertNull(seq.get(-1));
            assertEquals(1, ((TemplateNumberModel) seq.get(0)).getAsNumber());
            assertNull(seq.get(1));
            assertEquals("c", ((TemplateScalarModel) seq.get(2)).getAsString());
            assertNull(seq.get(3));

            assertCollectionTMEquals((TemplateCollectionModel) seq, 1, null, "c");

            TemplateModelIterator it = ((TemplateCollectionModel) seq).iterator();
            it.next();
            it.next();
            it.next();
            try {
                it.next();
                fail();
            } catch (TemplateModelException e) {
                assertThat(e.getMessage(), containsString("no more"));
            }
        }

        {
            List testList = new ArrayList<Object>();
            testList.add(null);

            final TemplateSequenceModel seq = (TemplateSequenceModel) OW22NM.wrap(testList);
            assertSame(NullModel.INSTANCE, seq.get(0));
            assertNull(seq.get(1));
        }
    }

    @Test
    public void testArrayAdapterTypes() throws TemplateModelException {
        assertArrayAdapterClass("Object", OW22.wrap(new Object[] {}));
        assertArrayAdapterClass("Object", OW22.wrap(new String[] {}));
        assertArrayAdapterClass("byte", OW22.wrap(new byte[] {}));
        assertArrayAdapterClass("short", OW22.wrap(new short[] {}));
        assertArrayAdapterClass("int", OW22.wrap(new int[] {}));
        assertArrayAdapterClass("long", OW22.wrap(new long[] {}));
        assertArrayAdapterClass("float", OW22.wrap(new float[] {}));
        assertArrayAdapterClass("double", OW22.wrap(new double[] {}));
        assertArrayAdapterClass("boolean", OW22.wrap(new boolean[] {}));
        assertArrayAdapterClass("char", OW22.wrap(new char[] {}));
    }

    private void assertArrayAdapterClass(String adapterCompType, TemplateModel adaptedArray) {
        assertTrue(adaptedArray instanceof DefaultArrayAdapter);
        assertThat(adaptedArray.getClass().getName(),
                containsString("$" + adapterCompType.substring(0, 1).toUpperCase() + adapterCompType.substring(1)));
    }

    @SuppressWarnings("boxing")
    @Test
    public void testArrayAdapters() throws TemplateModelException {
        {
            final String[] testArray = new String[] { "a", null, "c" };

            {
                TemplateSequenceModel seq = (TemplateSequenceModel) OW22.wrap(testArray);
                assertEquals(3, seq.size());
                assertNull(seq.get(-1));
                assertEquals("a", ((TemplateScalarModel) seq.get(0)).getAsString());
                assertNull(seq.get(1));
                assertEquals("c", ((TemplateScalarModel) seq.get(2)).getAsString());
                assertNull(seq.get(3));
            }

            {
                TemplateSequenceModel seq = (TemplateSequenceModel) OW22NM.wrap(testArray);
                assertNull(seq.get(-1));
                assertEquals("a", ((TemplateScalarModel) seq.get(0)).getAsString());
                assertSame(NullModel.INSTANCE, seq.get(1));
                assertEquals("c", ((TemplateScalarModel) seq.get(2)).getAsString());
                assertNull(seq.get(3));
            }
        }

        {
            final int[] testArray = new int[] { 11, 22 };
            TemplateSequenceModel seq = (TemplateSequenceModel) OW22.wrap(testArray);
            assertEquals(2, seq.size());
            assertNull(seq.get(-1));
            assertEqualsAndSameClass(Integer.valueOf(11), ((TemplateNumberModel) seq.get(0)).getAsNumber());
            assertEqualsAndSameClass(Integer.valueOf(22), ((TemplateNumberModel) seq.get(1)).getAsNumber());
            assertNull(seq.get(2));
        }

        {
            final double[] testArray = new double[] { 11, 22 };
            TemplateSequenceModel seq = (TemplateSequenceModel) OW22.wrap(testArray);
            assertEquals(2, seq.size());
            assertNull(seq.get(-1));
            assertEqualsAndSameClass(Double.valueOf(11), ((TemplateNumberModel) seq.get(0)).getAsNumber());
            assertEqualsAndSameClass(Double.valueOf(22), ((TemplateNumberModel) seq.get(1)).getAsNumber());
            assertNull(seq.get(2));
        }

        {
            final boolean[] testArray = new boolean[] { true, false };
            TemplateSequenceModel seq = (TemplateSequenceModel) OW22.wrap(testArray);
            assertEquals(2, seq.size());
            assertNull(seq.get(-1));
            assertEqualsAndSameClass(Boolean.valueOf(true), ((TemplateBooleanModel) seq.get(0)).getAsBoolean());
            assertEqualsAndSameClass(Boolean.valueOf(false), ((TemplateBooleanModel) seq.get(1)).getAsBoolean());
            assertNull(seq.get(2));
        }

        {
            final char[] testArray = new char[] { 'a', 'b' };
            TemplateSequenceModel seq = (TemplateSequenceModel) OW22.wrap(testArray);
            assertEquals(2, seq.size());
            assertNull(seq.get(-1));
            assertEquals("a", ((TemplateScalarModel) seq.get(0)).getAsString());
            assertEquals("b", ((TemplateScalarModel) seq.get(1)).getAsString());
            assertNull(seq.get(2));
        }
    }

    private void assertEqualsAndSameClass(Object expected, Object actual) {
        assertEquals(expected, actual);
        if (expected != null) {
            assertEquals(expected.getClass(), actual.getClass());
        }
    }

    private void assertRoundtrip(DefaultObjectWrapper dow, Object obj, Class expectedTMClass,
            Class expectedPojoClass,
            String expectedPojoToString)
            throws TemplateModelException {
        final TemplateModel objTM = dow.wrap(obj);
        assertTrue(expectedTMClass.isAssignableFrom(objTM.getClass()));

        final TemplateHashModel testBeanTM = (TemplateHashModel) dow.wrap(new RoundtripTesterBean());

        {
            TemplateMethodModelEx getClassM = (TemplateMethodModelEx) testBeanTM.get("getClass");
            Object r = getClassM.exec(Collections.singletonList(objTM));
            final Class rClass = (Class) ((WrapperTemplateModel) r).getWrappedObject();
            assertTrue(rClass + " instanceof " + expectedPojoClass, expectedPojoClass.isAssignableFrom(rClass));
        }

        if (expectedPojoToString != null) {
            TemplateMethodModelEx getToStringM = (TemplateMethodModelEx) testBeanTM.get("toString");
            Object r = getToStringM.exec(Collections.singletonList(objTM));
            assertEquals(expectedPojoToString, ((TemplateScalarModel) r).getAsString());
        }
    }

    @SuppressWarnings("boxing")
    @Test
    public void testCollectionAdapterBasics() throws TemplateModelException {
        {
            Set set = new TreeSet();
            set.add("a");
            set.add("b");
            set.add("c");
            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) OW22CA.wrap(set);
            assertTrue(coll instanceof DefaultNonListCollectionAdapter);
            assertEquals(3, coll.size());
            assertFalse(coll.isEmpty());
            assertCollectionTMEquals(coll, "a", "b", "c");

            assertRoundtrip(OW22CA, set, DefaultNonListCollectionAdapter.class, TreeSet.class, "[a, b, c]");
            
            assertSizeThroughAPIModel(3, coll);
        }

        {
            Set set = new HashSet();
            final List<String> list = Collections.singletonList("b");
            set.add(list);
            set.add(null);
            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) OW22CA.wrap(set);
            TemplateModelIterator it = coll.iterator();
            final TemplateModel tm1 = it.next();
            Object obj1 = OW22CA.unwrap(tm1);
            final TemplateModel tm2 = it.next();
            Object obj2 = OW22CA.unwrap(tm2);
            assertTrue(obj1 == null || obj2 == null);
            assertTrue(obj1 != null && obj1.equals(list) || obj2 != null && obj2.equals(list));
            assertTrue(tm1 instanceof DefaultListAdapter || tm2 instanceof DefaultListAdapter);

            assertRoundtrip(OW22CA, set, DefaultNonListCollectionAdapter.class, HashSet.class, "[" + obj1 + ", "
                    + obj2 + "]");
        }
    }

    @SuppressWarnings("boxing")
    @Test
    public void testCollectionAdapterOutOfBounds() throws TemplateModelException {
        Set set = Collections.singleton(123);

        TemplateCollectionModelEx coll = (TemplateCollectionModelEx) OW22CA.wrap(set);
        TemplateModelIterator it = coll.iterator();

        for (int i = 0; i < 3; i++) {
            assertTrue(it.hasNext());
        }

        assertEquals(123, OW22CA.unwrap(it.next()));

        for (int i = 0; i < 3; i++) {
            assertFalse(it.hasNext());
            try {
                it.next();
                fail();
            } catch (TemplateModelException e) {
                assertThat(e.getMessage(), containsStringIgnoringCase("no more"));
            }
        }
    }

    @Test
    public void testCollectionAdapterAndNulls() throws TemplateModelException {
        Set set = new HashSet();
        set.add(null);

        {
            DefaultObjectWrapper dow = new DefaultObjectWrapper(Configuration.VERSION_2_3_22);
            dow.setForceLegacyNonListCollections(false);
            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) dow.wrap(set);
            assertEquals(1, coll.size());
            assertFalse(coll.isEmpty());
            assertNull(coll.iterator().next());
        }

        {
            DefaultObjectWrapper dow = new DefaultObjectWrapper(Configuration.VERSION_2_3_22);
            dow.setForceLegacyNonListCollections(false);
            dow.setNullModel(NullModel.INSTANCE);
            TemplateCollectionModelEx coll = (TemplateCollectionModelEx) dow.wrap(set);
            assertEquals(1, coll.size());
            assertFalse(coll.isEmpty());
            assertEquals(NullModel.INSTANCE, coll.iterator().next());
        }
    }

    @Test
    public void testLegacyNonListCollectionWrapping() throws TemplateModelException {
        Set set = new TreeSet();
        set.add("a");
        set.add("b");
        set.add("c");
        TemplateSequenceModel seq = (TemplateSequenceModel) OW22.wrap(set);
        assertTrue(seq instanceof SimpleSequence);
        assertEquals(3, seq.size());
        assertEquals("a", OW22.unwrap(seq.get(0)));
        assertEquals("b", OW22.unwrap(seq.get(1)));
        assertEquals("c", OW22.unwrap(seq.get(2)));
    }

    @Test
    public void testIteratorWrapping() throws TemplateModelException, ClassNotFoundException {
        testIteratorWrapping(OW0, SimpleCollection.class, Class.forName("freemarker.ext.beans.SetAdapter"));
        testIteratorWrapping(OW22, DefaultIteratorAdapter.class, Iterator.class);
    }

    private void testIteratorWrapping(DefaultObjectWrapper ow, Class<?> expectedTMClass, Class<?> expectedPOJOClass)
            throws TemplateModelException {
        final List<String> list = ImmutableList.of("a", "b", "c");
        Iterator<String> it = list.iterator();
        TemplateCollectionModel coll = (TemplateCollectionModel) ow.wrap(it);

        assertRoundtrip(ow, coll, expectedTMClass, expectedPOJOClass, null);

        TemplateModelIterator itIt = coll.iterator();
        TemplateModelIterator itIt2 = coll.iterator(); // used later
        assertTrue(itIt.hasNext());
        assertEquals("a", ow.unwrap(itIt.next()));
        assertTrue(itIt.hasNext());
        assertEquals("b", ow.unwrap(itIt.next()));
        assertTrue(itIt.hasNext());
        assertEquals("c", ow.unwrap(itIt.next()));
        assertFalse(itIt.hasNext());
        try {
            itIt.next();
            fail();
        } catch (TemplateModelException e) {
            assertThat(e.getMessage(), containsStringIgnoringCase("no more"));
        }

        try {
            itIt2.hasNext();
            fail();
        } catch (TemplateModelException e) {
            assertThat(e.getMessage(), containsString("can be listed only once"));
        }

        TemplateModelIterator itIt3 = coll.iterator();
        try {
            itIt3.hasNext();
            fail();
        } catch (TemplateModelException e) {
            assertThat(e.getMessage(), containsString("can be listed only once"));
        }
    }
    
    @Test
    public void testIteratorApiSupport() throws TemplateModelException {
        TemplateModel wrappedIterator = OW22.wrap(Collections.emptyIterator());
        assertThat(wrappedIterator, instanceOf(DefaultIteratorAdapter.class));
        DefaultIteratorAdapter iteratorAdapter = (DefaultIteratorAdapter) wrappedIterator;
        
        TemplateHashModel api = (TemplateHashModel) iteratorAdapter.getAPI();
        assertFalse(((TemplateBooleanModel) ((TemplateMethodModelEx)
                api.get("hasNext")).exec(Collections.emptyList())).getAsBoolean());
    }
    
    @SuppressWarnings("boxing")
    @Test
    public void testCharKeyFallback() throws TemplateModelException {
        Map hashMapS = new HashMap<String, Integer>();
        hashMapS.put("a", 1);
        Map sortedMapS = new TreeMap<String, Integer>();
        sortedMapS.put("a", 1);
        Map hashMapC = new HashMap<Character, Integer>();
        hashMapC.put('a', 1);
        Map sortedMapC = new TreeMap<Character, Integer>();
        sortedMapC.put('a', 1);
        
        for (DefaultObjectWrapper ow : new DefaultObjectWrapper[] { OW0, OW22 } ) {
            assertEquals(1, ow.unwrap(((TemplateHashModel) ow.wrap(hashMapS)).get("a")));
            assertEquals(1, ow.unwrap(((TemplateHashModel) ow.wrap(hashMapC)).get("a")));
            assertEquals(1, ow.unwrap(((TemplateHashModel) ow.wrap(sortedMapS)).get("a")));
            try {
                ((TemplateHashModel) ow.wrap(sortedMapC)).get("a");
            } catch (TemplateModelException e) {
                assertThat(e.getMessage(), containsStringIgnoringCase("String key"));
            }
            
            assertNull(((TemplateHashModel) ow.wrap(hashMapS)).get("b"));
            assertNull(((TemplateHashModel) ow.wrap(hashMapC)).get("b"));
            assertNull(((TemplateHashModel) ow.wrap(sortedMapS)).get("b"));
            try {
                ((TemplateHashModel) ow.wrap(sortedMapC)).get("b");
            } catch (TemplateModelException e) {
                assertThat(e.getMessage(), containsStringIgnoringCase("String key"));
            }
        }
    }
    
    @Test
    public void testIterableSupport() throws TemplateException, IOException {
        Iterable<String> iterable = new PureIterable();
        
        String listingFTL = "<#list value as x>${x}<#sep>, </#list>";
        
        {
            DefaultObjectWrapper ow = OW22;
            TemplateModel tm = ow.wrap(iterable);
            assertThat(tm, instanceOf(TemplateHashModel.class));
            assertThat(tm, not(instanceOf(TemplateCollectionModel.class)));
            assertThat(tm, not(instanceOf(TemplateSequenceModel.class)));
            assertNotNull(((TemplateHashModel) tm).get("iterator"));
            
            assertTemplateFails(ow, iterable, listingFTL, "iterableSupport");
        }
        
        {
            DefaultObjectWrapper ow = OW22IS;
            TemplateModel tm = ow.wrap(iterable);
            assertThat(tm, instanceOf(TemplateCollectionModel.class));
            TemplateCollectionModel iterableTM = (TemplateCollectionModel) tm;
            
            for (int i = 0; i < 2; i++) {
                TemplateModelIterator iteratorTM = iterableTM.iterator();
                assertTrue(iteratorTM.hasNext());
                assertEquals("a", ow.unwrap(iteratorTM.next()));
                assertTrue(iteratorTM.hasNext());
                assertEquals("b", ow.unwrap(iteratorTM.next()));
                assertTrue(iteratorTM.hasNext());
                assertEquals("c", ow.unwrap(iteratorTM.next()));
                assertFalse(iteratorTM.hasNext());
                try {
                    iteratorTM.next();
                    fail();
                } catch (TemplateModelException e) {
                    assertThat(e.getMessage(), containsStringIgnoringCase("no more"));
                }
            }
            
            assertTemplateOutput(OW22IS, iterable, listingFTL, "a, b, c");
        }
    }

    @Test
    public void testNoEnumerationAdapter() throws TemplateModelException {
         DefaultObjectWrapper ow = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_25).build();
         Vector<String> vector = new Vector<>();
         vector.add("a");
         vector.add("b");
         
         TemplateModel wrappedEnumeration = ow.wrap(vector.elements());
         assertThat(wrappedEnumeration, instanceOf(EnumerationModel.class));
         EnumerationModel enumModel = (EnumerationModel) wrappedEnumeration;
         assertNotNull(enumModel.get("nextElement"));
    }
    
    @Test
    public void testEnumerationAdapter() throws TemplateModelException {
         DefaultObjectWrapper ow = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_26).build();
         Vector<String> vector = new Vector<>();
         vector.add("a");
         vector.add("b");
         
         TemplateModel wrappedEnumeration = ow.wrap(vector.elements());
         assertThat(wrappedEnumeration, instanceOf(DefaultEnumerationAdapter.class));
         DefaultEnumerationAdapter enumAdapter = (DefaultEnumerationAdapter) wrappedEnumeration;
         TemplateModelIterator iterator = enumAdapter.iterator();
         assertTrue(iterator.hasNext());
         assertEquals("a", ((TemplateScalarModel) iterator.next()).getAsString());
         assertTrue(iterator.hasNext());
         assertEquals("b", ((TemplateScalarModel) iterator.next()).getAsString());
         assertFalse(iterator.hasNext());
         
         iterator = enumAdapter.iterator();
         try {
             iterator.hasNext();
         } catch (TemplateException e) {
             assertThat(e.getMessage(), containsStringIgnoringCase("only once"));
         }
         
         TemplateHashModel api = (TemplateHashModel) enumAdapter.getAPI();
         assertFalse(((TemplateBooleanModel) ((TemplateMethodModelEx)
                 api.get("hasMoreElements")).exec(Collections.emptyList())).getAsBoolean());
    }
    
    @Test
    public void testCanWrapDOM() throws SAXException, IOException, ParserConfigurationException,
            TemplateModelException {
        assertTrue(OW22.wrap(createDocument()) instanceof TemplateNodeModel);
    }

    @Test
    public void testDisabledDOMNodeWrapping() throws SAXException, IOException, ParserConfigurationException,
            TemplateModelException {
        Document doc = createDocument();
        DefaultObjectWrapperBuilder dowB = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_31);
        dowB.setDOMNodeSupport(false);
        DefaultObjectWrapper ow = dowB.build();

        testDisabledDomWrappingInternal(doc, ow);

        ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_31);
        ow.setDOMNodeSupport(false);
        testDisabledDomWrappingInternal(doc, ow);
    }

    private void testDisabledDomWrappingInternal(Document doc, DefaultObjectWrapper ow) throws TemplateModelException {
        TemplateModel model = ow.wrap(doc);
        assertFalse(model instanceof TemplateNodeModel);
        assertTrue(model instanceof TemplateHashModel);
        assertNotNull(((TemplateHashModel) model).get("getDoctype"));
        assertNotNull(((TemplateHashModel) model).get("class"));
    }

    @Test
    public void testDisabledJythonWrapping() throws SAXException, IOException, ParserConfigurationException,
            TemplateModelException {
        PyString pyString = new PyString("foo");

        DefaultObjectWrapper ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_31);
        assertThat(ow.wrap(pyString), Matchers.instanceOf(JythonSequenceModel.class));

        ow.setJythonSupport(false);
        assertThat(ow.wrap(pyString), not(Matchers.instanceOf(JythonSequenceModel.class)));
    }

    private Document createDocument() throws ParserConfigurationException, SAXException, IOException {
        DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        InputSource is = new InputSource();
        is.setCharacterStream(new StringReader("<doc><sub a='1' /></doc>"));
        return db.parse(is);
    }

    @Test
    public void testPreferIndexedReadMethodAndIcI() {
        assertTrue(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_26).build().getPreferIndexedReadMethod());
        assertTrue(new DefaultObjectWrapper(Configuration.VERSION_2_3_26).getPreferIndexedReadMethod());
        
        assertFalse(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27).build().getPreferIndexedReadMethod());
        assertFalse(new DefaultObjectWrapper(Configuration.VERSION_2_3_27).getPreferIndexedReadMethod());
    }
    
    private void assertSizeThroughAPIModel(int expectedSize, TemplateModel normalModel) throws TemplateModelException {
        if (!(normalModel instanceof TemplateModelWithAPISupport)) {
            fail(); 
        }
        TemplateHashModel apiModel = (TemplateHashModel) ((TemplateModelWithAPISupport) normalModel).getAPI();
        TemplateMethodModelEx sizeMethod = (TemplateMethodModelEx) apiModel.get("size");
        TemplateNumberModel r = (TemplateNumberModel) sizeMethod.exec(Collections.emptyList());
        assertEquals(expectedSize, r.getAsNumber().intValue());
    }

    private void assertTemplateOutput(ObjectWrapper objectWrapper, Object value, String ftl, String expectedOutput)
            throws TemplateException, IOException {
        assertEquals(expectedOutput, processTemplate(objectWrapper, value, ftl));
    }

    private void assertTemplateFails(ObjectWrapper objectWrapper, Object value, String ftl, String expectedMessagePart)
            throws TemplateException, IOException {
        try {
            processTemplate(objectWrapper, value, ftl);
            fail();
        } catch (TemplateException e) {
            assertThat(e.getMessage(), containsString(expectedMessagePart));
        }
    }
    
    private String processTemplate(ObjectWrapper objectWrapper, Object value, String ftl)
            throws TemplateException, IOException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_24);
        cfg.setLogTemplateExceptions(false);
        cfg.setObjectWrapper(objectWrapper);
        StringWriter out = new StringWriter();
        new Template(null, ftl, cfg).process(ImmutableMap.of("value", value), out);
        return out.toString();
    }

    private static final class PureIterable implements Iterable<String> {
        public Iterator<String> iterator() {
            return ImmutableList.of("a", "b", "c").iterator();
        }
    }

    public static class RoundtripTesterBean {

        public Class getClass(Object o) {
            return o.getClass();
        }

        public String toString(Object o) {
            return o.toString();
        }

    }

    private static final class NullModel implements TemplateModel, AdapterTemplateModel {

        final static NullModel INSTANCE = new NullModel();

        public Object getAdaptedObject(Class hint) {
            return null;
        }

    }
    
    private static class Tupple<E1, E2> {
        
        private final E1 e1;
        private final E2 e2;

        public Tupple(E1 e1, E2 e2) {
            if (e1 == null || e2 == null) throw new NullPointerException();
            this.e1 = e1;
            this.e2 = e2;
        }

        public E1 getE1() {
            return e1;
        }

        public E2 getE2() {
            return e2;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((e1 == null) ? 0 : e1.hashCode());
            result = prime * result + ((e2 == null) ? 0 : e2.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            Tupple other = (Tupple) obj;
            if (e1 == null) {
                if (other.e1 != null) return false;
            } else if (!e1.equals(other.e1)) return false;
            if (e2 == null) {
                if (other.e2 != null) return false;
            } else if (!e2.equals(other.e2)) return false;
            return true;
        }
        
    }
    
    @SuppressWarnings("boxing")
    public static class TestBean {
        
        public int getX() {
            return 1;
        }
        
        public List<Integer> m() {
            return Collections.singletonList(1);
        }

        public Tupple incTupple(Tupple<Integer, Integer> tupple) {
            return new Tupple(tupple.e1 + 1, tupple.e2 + 1);
        }
        
    }
    
    private static class CustomizedDefaultObjectWrapper extends DefaultObjectWrapper {

        private CustomizedDefaultObjectWrapper(Version incompatibleImprovements) {
            super(incompatibleImprovements);
        }
        
        @Override
        protected TemplateModel handleUnknownType(final Object obj) throws TemplateModelException {
            if (obj instanceof Tupple) {
                return new TuppleAdapter((Tupple<?, ?>) obj, this);
            }
            
            return super.handleUnknownType(obj);
        }
        
    }
    
    private static class TuppleAdapter extends WrappingTemplateModel implements TemplateSequenceModel,
            AdapterTemplateModel {
        
        private final Tupple<?, ?> tupple;
        
        public TuppleAdapter(Tupple<?, ?> tupple, ObjectWrapper ow) {
            super(ow);
            this.tupple = tupple;
        }

        public int size() throws TemplateModelException {
            return 2;
        }
        
        public TemplateModel get(int index) throws TemplateModelException {
            switch (index) {
            case 0: return wrap(tupple.getE1());
            case 1: return wrap(tupple.getE2());
            default: return null;
            }
        }

        public Object getAdaptedObject(Class hint) {
            return tupple;
        }
        
    };

    public static class SomeBean {
        public int getX() {
            return 1;
        }
        public int getY() {
            return 1;
        }
    }
    
}
