/* $This file is distributed under the terms of the license in LICENSE$ */

package freemarker.ext.dump;

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

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.commons.lang3.time.DateUtils;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;

import freemarker.core.CollectionAndSequence;
import freemarker.core.Environment;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.CollectionModel;
import freemarker.ext.dump.BaseDumpDirective.DateType;
import freemarker.ext.dump.BaseDumpDirective.Key;
import freemarker.ext.dump.BaseDumpDirective.Type;
import freemarker.ext.dump.BaseDumpDirective.Value;
import freemarker.template.Configuration;
import freemarker.template.SimpleCollection;
import freemarker.template.Template;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateMethodModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;

/**
 * Unit tests of dump directive. The tests follow the same basic pattern:
 * 1. Create the data model
 * 2. Create the expected dump data structure
 * 3. Create the actual dump data structure by running the data model through a processing environment
 * 4. Compare expected and actual dump data structures
 * 
 * @author rjy7
 *
 */
public class DumpDirectiveTest {
    
    private Template template;

    @Before
    public void setUp() {
        Configuration config = new Configuration();
        String templateStr = "";
        try {
            template = new Template("template", new StringReader(templateStr), config);
        } catch (Exception e) {
            fail(e.getMessage());
        }
        // Turn off log messages to console
        Logger.getLogger(BaseDumpDirective.class).setLevel(Level.OFF);
    }

    @Test
    public void dumpUndefinedValue() {
        
        String varName = "dog";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.VALUE.toString(), Value.UNDEFINED.toString());

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);
    }
    
    @Test
    public void dumpString() {
        
        String varName = "dog";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        String value = "Rover";
        dataModel.put(varName, value);
         
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.STRING);
        expectedDumpValue.put(Key.VALUE.toString(), value);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);
    }
    
    @Test
    public void dumpBoolean() {

        String varName = "isLoggedIn";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        boolean value = true;
        dataModel.put(varName, value);
         
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.BOOLEAN);
        expectedDumpValue.put(Key.VALUE.toString(), value);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);     
    }
    
    @Test
    public void dumpNumber() {
        
        String varName = "tabCount";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        int value = 7;
        dataModel.put(varName, value);
         
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.NUMBER);
        expectedDumpValue.put(Key.VALUE.toString(), value);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);         
    }
    
    @Test
    public void dumpSimpleDate() {

        String varName = "now";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Date now = new Date();
        dataModel.put(varName, now);
         
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.DATE);
        expectedDumpValue.put(Key.DATE_TYPE.toString(), DateType.UNKNOWN);
        expectedDumpValue.put(Key.VALUE.toString(), now);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);
    }

    @Test
    public void dumpCalendarDate() {

        String varName = "myDate";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Calendar c = Calendar.getInstance();
        c.set(1991, Calendar.MAY, 5);
        Date myDate = c.getTime();
        dataModel.put("myDate", myDate);
         
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.DATE);
        expectedDumpValue.put(Key.DATE_TYPE.toString(), DateType.UNKNOWN);
        expectedDumpValue.put(Key.VALUE.toString(), myDate);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);
    }
    @Test
    public void dumpDateTime() {
        
        String varName = "timestamp";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Timestamp ts = new Timestamp(1302297332043L);
        dataModel.put(varName, ts);
         
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.DATE);
        expectedDumpValue.put(Key.DATE_TYPE.toString(), DateType.DATETIME);
        expectedDumpValue.put(Key.VALUE.toString(), ts);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);
    }

    @Test
    public void dumpSqlDate() {
        
        String varName = "date";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        java.sql.Date date = new java.sql.Date(1302297332043L);
        dataModel.put(varName, date);
         
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.DATE);
        expectedDumpValue.put(Key.DATE_TYPE.toString(), DateType.DATE);
        expectedDumpValue.put(Key.VALUE.toString(), date);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);
    }
    
    @Test
    public void dumpTime() {
        
        String varName = "time";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Time time = new Time(1302297332043L);
        dataModel.put(varName, time);
         
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.DATE);
        expectedDumpValue.put(Key.DATE_TYPE.toString(), DateType.TIME);
        expectedDumpValue.put(Key.VALUE.toString(), time);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);
    }

   @Test
   public void dumpHelplessMethod() {

       String varName = "square";
       Map<String, Object> dataModel = new HashMap<String, Object>();
       
       TemplateMethodModel methodModel = new HelplessMethod();
       dataModel.put(varName, methodModel);
        
       Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
       expectedDumpValue.put(Key.TYPE.toString(), Type.METHOD);
       expectedDumpValue.put(Key.CLASS.toString(), methodModel.getClass().getName());
       expectedDumpValue.put(Key.HELP.toString(), null);

       Map<String, Object> expectedDump = new HashMap<String, Object>();
       expectedDump.put(varName, expectedDumpValue);
       
       test(varName, dataModel, expectedDump); 
   }

   @Test
   public void dumpHelpfulMethod() {

       String varName = "square";
       Map<String, Object> dataModel = new HashMap<String, Object>();
       
       TemplateMethodModel methodModel = new HelpfulMethod();
       dataModel.put(varName, methodModel);
        
       Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
       expectedDumpValue.put(Key.TYPE.toString(), Type.METHOD);
       expectedDumpValue.put(Key.CLASS.toString(), methodModel.getClass().getName());
       expectedDumpValue.put(Key.HELP.toString(), getMethodHelp(varName));

       Map<String, Object> expectedDump = new HashMap<String, Object>();
       expectedDump.put(varName, expectedDumpValue);
       
       test(varName, dataModel, expectedDump);
   }

   @Test
   public void dumpMethodWithStringHelp() {

       String varName = "square";
       Map<String, Object> dataModel = new HashMap<String, Object>();
       
       TemplateMethodModel methodModel = new MethodWithStringHelp();
       dataModel.put(varName, methodModel);
        
       Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
       expectedDumpValue.put(Key.TYPE.toString(), Type.METHOD);
       expectedDumpValue.put(Key.CLASS.toString(), methodModel.getClass().getName());
       expectedDumpValue.put(Key.HELP.toString(), null);

       Map<String, Object> expectedDump = new HashMap<String, Object>();
       expectedDump.put(varName, expectedDumpValue);
       
       test(varName, dataModel, expectedDump);
   }

   @Test
   public void dumpMethodWithStringStringMapHelp() {

       String varName = "square";
       Map<String, Object> dataModel = new HashMap<String, Object>();
       
       TemplateMethodModel methodModel = new MethodWithStringStringMapHelp();
       dataModel.put(varName, methodModel);
        
       Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
       expectedDumpValue.put(Key.TYPE.toString(), Type.METHOD);
       expectedDumpValue.put(Key.CLASS.toString(), methodModel.getClass().getName());
       expectedDumpValue.put(Key.HELP.toString(), new HashMap<String, Object>());

       Map<String, Object> expectedDump = new HashMap<String, Object>();
       expectedDump.put(varName, expectedDumpValue);
       
       test(varName, dataModel, expectedDump);
   }
   
   @Test
   public void dumpHelplessDirective() {

       String varName = "dump";
       Map<String, Object> dataModel = new HashMap<String, Object>();
       
       TemplateDirectiveModel directiveModel = new HelplessDirective();
       dataModel.put(varName, directiveModel);
        
       Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
       expectedDumpValue.put(Key.TYPE.toString(), Type.DIRECTIVE);
       expectedDumpValue.put(Key.CLASS.toString(), directiveModel.getClass().getName());
       expectedDumpValue.put(Key.HELP.toString(), null);

       Map<String, Object> expectedDump = new HashMap<String, Object>();
       expectedDump.put(varName, expectedDumpValue);
       
       test(varName, dataModel, expectedDump);
   }

   @Test
   public void dumpHelpfulDirective() {

       String varName = "dump";   
       Map<String, Object> dataModel = new HashMap<String, Object>();
       
       TemplateDirectiveModel directiveModel = new HelpfulDirective();
       dataModel.put(varName, directiveModel);
        
       Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
       expectedDumpValue.put(Key.TYPE.toString(), Type.DIRECTIVE);
       expectedDumpValue.put(Key.CLASS.toString(), directiveModel.getClass().getName());
       expectedDumpValue.put(Key.HELP.toString(), getDirectiveHelp(varName));

       Map<String, Object> expectedDump = new HashMap<String, Object>();
       expectedDump.put(varName, expectedDumpValue);
       
       test(varName, dataModel, expectedDump); 
   }
 
   @Test
   public void dumpDirectiveWithStringHelp() {

       String varName = "dump";
       Map<String, Object> dataModel = new HashMap<String, Object>();
       
       TemplateDirectiveModel directiveModel = new DirectiveWithStringHelp();
       dataModel.put(varName, directiveModel);
        
       Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
       expectedDumpValue.put(Key.TYPE.toString(), Type.DIRECTIVE);
       expectedDumpValue.put(Key.CLASS.toString(), directiveModel.getClass().getName());
       expectedDumpValue.put(Key.HELP.toString(), null);

       Map<String, Object> expectedDump = new HashMap<String, Object>();
       expectedDump.put(varName, expectedDumpValue);
       
       test(varName, dataModel, expectedDump);
   } 

   @Test
   public void dumpDirectiveWithStringStringMapHelp() {

       String varName = "dump";
       Map<String, Object> dataModel = new HashMap<String, Object>();
       
       TemplateDirectiveModel directiveModel = new DirectiveWithStringStringMapHelp();
       dataModel.put(varName, directiveModel);
        
       Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
       expectedDumpValue.put(Key.TYPE.toString(), Type.DIRECTIVE);
       expectedDumpValue.put(Key.CLASS.toString(), directiveModel.getClass().getName());
       expectedDumpValue.put(Key.HELP.toString(), new HashMap<String, Object>());

       Map<String, Object> expectedDump = new HashMap<String, Object>();
       expectedDump.put(varName, expectedDumpValue);
       
       test(varName, dataModel, expectedDump);
   } 
   
    @Test
    public void dumpStringList() {
        
        String varName = "fruit";        
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        List<String> myList = new ArrayList<String>();
        myList.add("apples");
        myList.add("bananas");
        myList.add("oranges");
        dataModel.put(varName, myList);
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.SEQUENCE);
        List<Map<String, Object>> myListItemsExpectedDump = new ArrayList<Map<String, Object>>(myList.size());
        for ( String str : myList) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.STRING);
            itemDump.put(Key.VALUE.toString(), str);
            myListItemsExpectedDump.add(itemDump);
        }
        expectedDumpValue.put(Key.VALUE.toString(), myListItemsExpectedDump);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);       
    }
    
    @Test
    public void dumpStringArray() {
        
        String varName = "fruit";        
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        String[] myArray = { "apples", "bananas", "oranges" };
        dataModel.put(varName, myArray);
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.SEQUENCE);
        List<Map<String, Object>> myArrayExpectedDumpValue = new ArrayList<Map<String, Object>>(myArray.length);
        for ( String str : myArray) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.STRING);
            itemDump.put(Key.VALUE.toString(), str);
            myArrayExpectedDumpValue.add(itemDump);
        }
        expectedDumpValue.put(Key.VALUE.toString(), myArrayExpectedDumpValue);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);       
    }    
    
    @Test
    public void dumpMixedList() {
        
        String varName = "stuff";        
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        List<Object> mixedList = new ArrayList<Object>();
        
        String myString = "apples";
        mixedList.add(myString);
        
        int myInt = 4;
        mixedList.add(myInt);
        
        boolean myBool = true;
        mixedList.add(myBool);
        
        List<String> myList = new ArrayList<String>();
        myList.add("dog");
        myList.add("cat");
        myList.add("elephant");
        mixedList.add(myList);
        
        dataModel.put(varName, mixedList);
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.SEQUENCE);
        
        List<Map<String, Object>> mixedListExpectedDumpValue = new ArrayList<Map<String, Object>>(mixedList.size());
        
        Map<String, Object> myStringExpectedDumpValue = new HashMap<String, Object>();        
        myStringExpectedDumpValue.put(Key.TYPE.toString(), Type.STRING);
        myStringExpectedDumpValue.put(Key.VALUE.toString(), myString);
        mixedListExpectedDumpValue.add(myStringExpectedDumpValue);
 
        Map<String, Object> myIntExpectedDumpValue = new HashMap<String, Object>();  
        myIntExpectedDumpValue.put(Key.TYPE.toString(), Type.NUMBER);
        myIntExpectedDumpValue.put(Key.VALUE.toString(), myInt);
        mixedListExpectedDumpValue.add(myIntExpectedDumpValue);
        
        Map<String, Object> myBoolExpectedDumpValue = new HashMap<String, Object>();  
        myBoolExpectedDumpValue.put(Key.TYPE.toString(), Type.BOOLEAN);
        myBoolExpectedDumpValue.put(Key.VALUE.toString(), myBool);
        mixedListExpectedDumpValue.add(myBoolExpectedDumpValue);
        
        Map<String, Object> myListExpectedDumpValue = new HashMap<String, Object>();
        myListExpectedDumpValue.put(Key.TYPE.toString(), Type.SEQUENCE); 
        List<Map<String, Object>> myListItemsExpectedDumpValue = new ArrayList<Map<String, Object>>(myList.size());
        for ( String animal : myList ) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.STRING);
            itemDump.put(Key.VALUE.toString(), animal);
            myListItemsExpectedDumpValue.add(itemDump);            
        }        
        myListExpectedDumpValue.put(Key.VALUE.toString(), myListItemsExpectedDumpValue);
        mixedListExpectedDumpValue.add(myListExpectedDumpValue);
        
        expectedDumpValue.put(Key.VALUE.toString(), mixedListExpectedDumpValue);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump);       
    }    

    @Test
    public void dumpNumberSet() {

        String varName = "oddNums";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Set<Integer> myIntSet = new HashSet<Integer>();
        for (int i=0; i <= 10; i++) {
            if (i % 2 == 1) {
                myIntSet.add(i);
            }
        }
        dataModel.put(varName, myIntSet);
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.SEQUENCE);        
        List<Map<String, Object>> myIntSetExpectedDumpValue = new ArrayList<Map<String, Object>>(myIntSet.size());
        for ( int i : myIntSet ) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.NUMBER);
            itemDump.put(Key.VALUE.toString(), i);
            myIntSetExpectedDumpValue.add(itemDump);
        }
        expectedDumpValue.put(Key.VALUE.toString(), myIntSetExpectedDumpValue);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump); 
    }
    
    @Test
    public void dumpSimpleCollection() {

        String varName = "oddNums";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Set<Integer> odds = new HashSet<Integer>();
        for (int i=0; i <= 10; i++) {
            if (i % 2 == 1) {
                odds.add(i);
            }
        }
        TemplateCollectionModel myCollection = new SimpleCollection(odds);
        dataModel.put(varName, myCollection);
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.COLLECTION);       
        List<Map<String, Object>> myCollectionExpectedDumpValue = new ArrayList<Map<String, Object>>(odds.size());
        for ( int i : odds ) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.NUMBER);
            itemDump.put(Key.VALUE.toString(), i);
            myCollectionExpectedDumpValue.add(itemDump);
        }
        expectedDumpValue.put(Key.VALUE.toString(), myCollectionExpectedDumpValue);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump); 
    }    

    @Test
    public void dumpCollectionModel() {

        String varName = "oddNums";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Set<Integer> odds = new HashSet<Integer>();
        for (int i=0; i <= 10; i++) {
            if (i % 2 == 1) {
                odds.add(i);
            }
        }
        TemplateCollectionModel myCollection = new CollectionModel(odds, new BeansWrapper());
        dataModel.put(varName, myCollection);
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.COLLECTION);       
        List<Map<String, Object>> myCollectionExpectedDumpValue = new ArrayList<Map<String, Object>>(odds.size());
        for ( int i : odds ) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.NUMBER);
            itemDump.put(Key.VALUE.toString(), i);
            myCollectionExpectedDumpValue.add(itemDump);
        }
        expectedDumpValue.put(Key.VALUE.toString(), myCollectionExpectedDumpValue);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump); 
    }  
 
    @Test
    public void dumpCollectionAndSequenceModel() {

        String varName = "oddNums";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Set<Integer> odds = new HashSet<Integer>();
        for (int i=0; i <= 10; i++) {
            if (i % 2 == 1) {
                odds.add(i);
            }
        }
        TemplateCollectionModel coll = new CollectionModel(odds, new BeansWrapper());
        TemplateCollectionModel myCollection = new CollectionAndSequence(coll);
        dataModel.put(varName, myCollection);
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.SEQUENCE);       
        List<Map<String, Object>> myCollectionExpectedDumpValue = new ArrayList<Map<String, Object>>(odds.size());
        for ( int i : odds ) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.NUMBER);
            itemDump.put(Key.VALUE.toString(), i);
            myCollectionExpectedDumpValue.add(itemDump);
        }
        expectedDumpValue.put(Key.VALUE.toString(), myCollectionExpectedDumpValue);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        test(varName, dataModel, expectedDump); 
    }  

    @Test
    public void dumpStringToStringMap() {

        String varName = "capitals";        
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Map<String, String> myMap = new HashMap<String, String>();
        myMap.put("Albany", "New York");
        myMap.put("St. Paul", "Minnesota");
        myMap.put("Austin", "Texas");
        myMap.put("Sacramento", "California");
        myMap.put("Richmond", "Virginia");
        dataModel.put(varName, myMap);
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.HASH_EX);
        SortedMap<String, Object> myMapExpectedDump = new TreeMap<String, Object>();

        for ( String key : myMap.keySet() ) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.STRING);
            itemDump.put(Key.VALUE.toString(), myMap.get(key));
            myMapExpectedDump.put(key, itemDump);
        }
        expectedDumpValue.put(Key.VALUE.toString(), (myMapExpectedDump));

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        Map<String, Object> dump = getDump(varName, dataModel);
        assertEquals(expectedDump, dump); 

        // Test the sorting of the map
        List<String> expectedKeys = new ArrayList<String>(myMapExpectedDump.keySet());
        @SuppressWarnings("unchecked")
        Map<String, Object> actualDumpValue = (Map<String, Object>) dump.get(varName);
        @SuppressWarnings("unchecked")
        SortedMap<String, Object> myMapActualDump = (SortedMap<String, Object>) actualDumpValue.get(Key.VALUE.toString());
        List<String> actualKeys = new ArrayList<String>(myMapActualDump.keySet());
        assertEquals(expectedKeys, actualKeys);               
    }
    
    @Test
    public void dumpStringToObjectMap() {

        String varName = "stuff";        
        Map<String, Object> dataModel = new HashMap<String, Object>();
        
        Map<String, Object> mixedMap = new HashMap<String, Object>();
        
        String myString = "apples";
        mixedMap.put("myString", myString);
        
        boolean myBool = true;
        mixedMap.put("myBoolean", myBool);
        
        int myInt = 4;
        mixedMap.put("myNumber", myInt);
        
        Date myDate = new Date();
        mixedMap.put("myDate", myDate);
        
        List<String> myList = new ArrayList<String>();
        myList.add("apples");
        myList.add("bananas");
        myList.add("oranges");
        mixedMap.put("myList", myList);
        
        Map<String, String> myMap = new HashMap<String, String>();
        myMap.put("Great Expectations", "Charles Dickens");
        myMap.put("Pride and Prejudice", "Jane Austen");
        myMap.put("Middlemarch", "George Eliot");
        myMap.put("Jude the Obscure", "Thomas Hardy");
        mixedMap.put("myMap", myMap);
        
        dataModel.put(varName, mixedMap);
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), Type.HASH_EX);
        SortedMap<String, Object> mixedMapExpectedDump = new TreeMap<String, Object>();
        
        Map<String, Object> myStringExpectedDump = new HashMap<String, Object>();
        myStringExpectedDump.put(Key.TYPE.toString(), Type.STRING);
        myStringExpectedDump.put(Key.VALUE.toString(), myString);
        mixedMapExpectedDump.put("myString", myStringExpectedDump);
        
        Map<String, Object> myBooleanExpectedDump = new HashMap<String, Object>();
        myBooleanExpectedDump.put(Key.TYPE.toString(), Type.BOOLEAN);
        myBooleanExpectedDump.put(Key.VALUE.toString(), myBool);
        mixedMapExpectedDump.put("myBoolean", myBooleanExpectedDump);
 
        Map<String, Object> myIntExpectedDump = new HashMap<String, Object>();
        myIntExpectedDump.put(Key.TYPE.toString(), Type.NUMBER);
        myIntExpectedDump.put(Key.VALUE.toString(), myInt);
        mixedMapExpectedDump.put("myNumber", myIntExpectedDump);
        
        Map<String, Object> myDateExpectedDump = new HashMap<String, Object>();
        myDateExpectedDump.put(Key.TYPE.toString(), Type.DATE);
        myDateExpectedDump.put(Key.DATE_TYPE.toString(), DateType.UNKNOWN);
        myDateExpectedDump.put(Key.VALUE.toString(), myDate);
        mixedMapExpectedDump.put("myDate", myDateExpectedDump);
        
        Map<String, Object> myListExpectedDump = new HashMap<String, Object>();
        myListExpectedDump.put(Key.TYPE.toString(), Type.SEQUENCE); 
        List<Map<String, Object>> myListItemsExpectedDump = new ArrayList<Map<String, Object>>(myList.size());
        for ( String item : myList ) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.STRING);
            itemDump.put(Key.VALUE.toString(), item);
            myListItemsExpectedDump.add(itemDump);            
        }        
        myListExpectedDump.put(Key.VALUE.toString(), myListItemsExpectedDump);
        mixedMapExpectedDump.put("myList", myListExpectedDump);
        
        Map<String, Object> myMapExpectedDump = new HashMap<String, Object>();
        myMapExpectedDump.put(Key.TYPE.toString(), Type.HASH_EX);
        SortedMap<String, Object> myMapItemsExpectedDump = new TreeMap<String, Object>();
        for ( String key : myMap.keySet() ) {
            Map<String, Object> itemDump = new HashMap<String, Object>();
            itemDump.put(Key.TYPE.toString(), Type.STRING);
            itemDump.put(Key.VALUE.toString(), myMap.get(key));
            myMapItemsExpectedDump.put(key, itemDump);
        }
        myMapExpectedDump.put(Key.VALUE.toString(), myMapItemsExpectedDump);
        mixedMapExpectedDump.put("myMap", myMapExpectedDump);
        
        expectedDumpValue.put(Key.VALUE.toString(), mixedMapExpectedDump);

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        Map<String, Object> dump = getDump(varName, dataModel);
        assertEquals(expectedDump, dump); 

        // Test the sorting of the outer map        
        List<String> expectedDumpValueKeys = new ArrayList<String>(mixedMapExpectedDump.keySet());
        @SuppressWarnings("unchecked")
        Map<String, Object> actualDumpValue = (Map<String, Object>) dump.get(varName);
        @SuppressWarnings("unchecked")
        SortedMap<String, Object> mixedMapActualDump = (SortedMap<String, Object>) actualDumpValue.get(Key.VALUE.toString());
        List<String> actualDumpValueKeys = new ArrayList<String>(mixedMapActualDump.keySet());
        assertEquals(expectedDumpValueKeys, actualDumpValueKeys);
        
        // Test the sorting of the inner map
        List<String> myMapItemsExpectedDumpKeys = new ArrayList<String>(myMapItemsExpectedDump.keySet());
        @SuppressWarnings("unchecked")
        Map<String, Object> myMapActualDump = (Map<String, Object>) mixedMapActualDump.get("myMap");
        @SuppressWarnings("unchecked")
        SortedMap<String, Object> myMapItemsActualDump = (SortedMap<String, Object>) myMapActualDump.get(Key.VALUE.toString());
        List<String> myMapItemsActualDumpKeys = new ArrayList<String>(myMapItemsActualDump.keySet());
        assertEquals(myMapItemsExpectedDumpKeys, myMapItemsActualDumpKeys);
        
    }    

    @Test
    public void dumpObjectWithExposeNothingWrapper() {
        dumpObject(BeansWrapper.EXPOSE_NOTHING);       
    }
 
    @Test
    public void dumpObjectWithExposePropertiesOnlyWrapper() {
        dumpObject(BeansWrapper.EXPOSE_PROPERTIES_ONLY);  
    }
    
    @Test
    public void dumpObjectWithExposeSafeWrapper() {
        dumpObject(BeansWrapper.EXPOSE_SAFE);            
    }
    
    @Test
    public void dumpObjectWithExposeAllWrapper() {
        dumpObject(BeansWrapper.EXPOSE_ALL);    
    }
    
    
    /////////////////////////// Private test classes and helper methods ///////////////////////////

    private void test(String varName, Map<String, Object> dataModel, Map<String, Object> expectedDump) {
        Map<String, Object> dump = getDump(varName, dataModel);
        assertEquals(expectedDump, dump);        
    }
    
    private void dumpObject(int exposureLevel) {
        
        String varName = "employee";
        Map<String, Object> dataModel = new HashMap<String, Object>();
        BeansWrapper wrapper = new BeansWrapper();
        wrapper.setExposureLevel(exposureLevel);
        Employee employee = getEmployee();
        try {
            dataModel.put("employee", wrapper.wrap(employee));
        } catch (TemplateModelException e) {
            // logging is suppressed, so what do we do here?
        }
        
        Map<String, Object> expectedDumpValue = new HashMap<String, Object>();
        expectedDumpValue.put(Key.TYPE.toString(), employee.getClass().getName());
        expectedDumpValue.put(Key.VALUE.toString(), getJohnDoeExpectedDump(exposureLevel));

        Map<String, Object> expectedDump = new HashMap<String, Object>();
        expectedDump.put(varName, expectedDumpValue);
        
        testObjectDump(varName, dataModel, expectedDump);         
    }

    private void testObjectDump(String varName, Map<String, Object> dataModel, Map<String, Object> expectedDump) {
        
        Map<String, Object> dump = getDump(varName, dataModel);
        assertEquals(expectedDump, dump);         
        
        // Test the sorting of the properties
        @SuppressWarnings("unchecked")
        Map<String, Object> expectedVarDump = (Map<String, Object>) expectedDump.get(varName);
        @SuppressWarnings("unchecked")
        Map<String, Object> expectedValueDump = (Map<String, Object>) expectedVarDump.get(Key.VALUE.toString());
        @SuppressWarnings("unchecked")
        SortedMap<String, Object> expectedPropertyDump = (SortedMap<String, Object>) expectedValueDump.get(Key.PROPERTIES.toString());
        List<String> expectedPropertyDumpKeys = new ArrayList<String>(expectedPropertyDump.keySet());
        
        @SuppressWarnings("unchecked")
        Map<String, Object> actualVarDump = (Map<String, Object>) dump.get(varName);
        @SuppressWarnings("unchecked")
        Map<String, Object> actualValueDump = (Map<String, Object>) actualVarDump.get(Key.VALUE.toString());
        @SuppressWarnings("unchecked")
        SortedMap<String, Object> actualPropertyDump = (SortedMap<String, Object>) actualValueDump.get(Key.PROPERTIES.toString());
        List<String> actualPropertyDumpKeys = new ArrayList<String>(actualPropertyDump.keySet());
        
        assertEquals(expectedPropertyDumpKeys, actualPropertyDumpKeys);  
   
    }
    
    private Map<String, Object> getDump(String varName, Map<String, Object> dataModel) {
        try {
            Environment env = template.createProcessingEnvironment(dataModel, new StringWriter());
            return new DumpDirective().getTemplateVariableDump(varName, env);     
        } catch (Exception e) {
            fail(e.getMessage());
            return null;
        }             
    }
    
    private class HelplessMethod implements TemplateMethodModel {

        @Override
        public Object exec(List arg0) throws TemplateModelException {
            return null;
        }        
    }

    private class HelpfulMethod implements TemplateMethodModel {

        @Override
        public Object exec(List arg0) throws TemplateModelException {
            return null;
        } 
        
        public Map<String, Object> help(String name) {
            return getMethodHelp(name);
        }
    }

    private class MethodWithStringHelp implements TemplateMethodModel {

        @Override
        public Object exec(List arg0) throws TemplateModelException {
            return null;
        } 
        
        public String help(String name) {
            return "help";
        }
    }

    private class MethodWithStringStringMapHelp implements TemplateMethodModel {

        @Override
        public Object exec(List arg0) throws TemplateModelException {
            return null;
        } 
        
        public Map<String, String> help(String name) {
            return new HashMap<String, String>();
        }
    }
    
    private class HelplessDirective implements TemplateDirectiveModel {

        @Override
        public void execute(Environment arg0, Map arg1, TemplateModel[] arg2,
                TemplateDirectiveBody arg3) throws TemplateException,
                IOException {            
        }
    }

    private class HelpfulDirective implements TemplateDirectiveModel {

        @Override
        public void execute(Environment arg0, Map arg1, TemplateModel[] arg2,
                TemplateDirectiveBody arg3) throws TemplateException,
                IOException {            
        }
        
        public Map<String, Object> help(String name) {
            return getDirectiveHelp(name);
        }
    }
    
    private class DirectiveWithStringHelp implements TemplateDirectiveModel {

        @Override
        public void execute(Environment arg0, Map arg1, TemplateModel[] arg2,
                TemplateDirectiveBody arg3) throws TemplateException,
                IOException {            
        }
        
        public String help(String name) {
            return "help";
        }
    }

    private class DirectiveWithStringStringMapHelp implements TemplateDirectiveModel {

        @Override
        public void execute(Environment arg0, Map arg1, TemplateModel[] arg2,
                TemplateDirectiveBody arg3) throws TemplateException,
                IOException {            
        }
        
        public Map<String, String> help(String name) {
            return new HashMap<String, String>();
        }
    }
    
    private Map<String, Object> getDirectiveHelp(String name) {
        Map<String, Object> map = new HashMap<String, Object>();
        
        map.put("effect", "Dump the contents of a template variable.");
        
        map.put("comments", "Sequences (lists and arrays) are enclosed in square brackets. Hashes are enclosed in curly braces.");
        
        Map<String, String> params = new HashMap<String, String>();
        params.put("var", "name of variable to dump");
        map.put("parameters", params);
        
        List<String> examples = new ArrayList<String>();
        examples.add("<@" + name + " var=\"urls\" />");
        map.put("examples", examples);
        
        return map;        
    }
 
    private Map<String, Object> getMethodHelp(String name) {
        Map<String, Object> map = new HashMap<String, Object>();
        
        map.put("returns", "The square of the argument");

        List<String>params = new ArrayList<String>();
        params.add("Integer to square");
        map.put("parameters", params);
        
        List<String> examples = new ArrayList<String>();
        examples.add(name + "(4)");
        map.put("examples", examples);
        
        return map;     
    }
    
    public static class Employee {
        
        private static int count = 0;
        
        private String firstName;
        private String lastName;
        private String nickname;
        private Date birthdate;
        private boolean married;
        private int id;
        private String middleName;
        private List<String> favoriteColors;
        // private Map<String, String> degrees;
        private Employee supervisor;
        private float salary;
        
        Employee(String firstName, String lastName, int id, Date birthdate) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.middleName = null; // test a null value
            this.birthdate = birthdate;
            this.married = true;
            this.id = id;
            this.nickname = "";
            this.favoriteColors = new ArrayList<String>();
            count++;
        }

        protected void setSupervisor(Employee supervisor) {
            this.supervisor = supervisor;
        }

        void setSalary(float salary) {
            this.salary = salary;
        }
        
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
         
        public void setFavoriteColors(String...colors) {
            Collections.addAll(favoriteColors, colors);
        }
        
//        void setDegrees(Map<String, String> degrees) {
//            this.degrees = degrees;
//        }

        float getSalary() {
            return salary;
        }
        
        public static int getEmployeeCount() {
            return count;
        }
        
        /*  Public accessor methods for templates */
        
        public String getFullName() {
            return firstName + " " + lastName;
        }
        
        public String getName(String which) {
            return "first".equals(which) ? firstName : lastName;
        }
        
        public String getMiddleName() {
            return middleName;
        }

        public String getNickname() {
            return nickname;
        }
        
        public Date getBirthdate() {
            return birthdate;
        }
        
        public int getId() {
            return id;
        }      
        
        public boolean isMarried() {
            return married;
        }
 
        @Deprecated
        public int getFormerId() {
            return id % 10000;
        }

        public Employee getSupervisor() {
            return supervisor;
        }
        
        public Employee boss() {
            return supervisor;
        }
        
        public List<String> getFavoriteColors() {
            return favoriteColors;
        }
        
//        public Map<String, String> getDegrees() {
//            return degrees;
//        }
        
        public String familyName() {
            return lastName;
        }
    }
    
    private Employee getEmployee() {

        Calendar c = Calendar.getInstance();
        c.set(1982, Calendar.MAY, 5);
        c = DateUtils.truncate(c, Calendar.DATE);
        Employee jdoe = new Employee("John", "Doe", 34523, c.getTime());
        jdoe.setFavoriteColors("blue", "green");
        jdoe.setSalary(65000);

//        Map<String, String> degrees = new HashMap<String, String>();
//        degrees.put("BA", "Mathematics");
//        degrees.put("MS", "Computer Science");
//        jdoe.setDegrees(degrees);
        
        c.clear();
        c.set(1975, Calendar.OCTOBER, 25);
        c = DateUtils.truncate(c, Calendar.DATE);
        Employee jsmith = new Employee("Jane", "Smith", 78234, c.getTime());
        jsmith.setFavoriteColors("red", "orange");

        jdoe.setSupervisor(jsmith);

        return jdoe;
    }
    
    private Map<String, Object> getJohnDoeExpectedDump(int exposureLevel) {

        Map<String, Object> expectedDump = new HashMap<String, Object>();

        Map<String, Object> supervisorExpectedDump = new HashMap<String, Object>();
        supervisorExpectedDump.put(Key.TYPE.toString(), "freemarker.ext.dump.DumpDirectiveTest$Employee");
        supervisorExpectedDump.put(Key.VALUE.toString(), getJaneSmithExpectedDump(exposureLevel));
        
        // Properties
        SortedMap<String, Object> propertiesExpectedDump = new TreeMap<String, Object>();
        
        if (exposureLevel < BeansWrapper.EXPOSE_NOTHING) {           
    
            Map<String, Object> birthdateExpectedDump = new HashMap<String, Object>();
            birthdateExpectedDump.put(Key.TYPE.toString(), Type.DATE);
            birthdateExpectedDump.put(Key.DATE_TYPE.toString(), DateType.UNKNOWN);
            Calendar c = Calendar.getInstance();
            c.set(1982, Calendar.MAY, 5);
            c = DateUtils.truncate(c, Calendar.DATE);
            birthdateExpectedDump.put(Key.VALUE.toString(), c.getTime());
            propertiesExpectedDump.put("birthdate", birthdateExpectedDump);
    
            Map<String, Object> fullNameExpectedDump = new HashMap<String, Object>();
            fullNameExpectedDump.put(Key.TYPE.toString(), Type.STRING);
            fullNameExpectedDump.put(Key.VALUE.toString(), "John Doe");
            propertiesExpectedDump.put("fullName", fullNameExpectedDump);
    
            Map<String, Object> idExpectedDump = new HashMap<String, Object>();
            idExpectedDump.put(Key.TYPE.toString(), Type.NUMBER);
            idExpectedDump.put(Key.VALUE.toString(), 34523);
            propertiesExpectedDump.put("id", idExpectedDump);
    
            Map<String, Object> nicknameExpectedDump = new HashMap<String, Object>();
            nicknameExpectedDump.put(Key.TYPE.toString(), Type.STRING);
            nicknameExpectedDump.put(Key.VALUE.toString(), "");
            propertiesExpectedDump.put("nickname", nicknameExpectedDump);
    
            Map<String, Object> middleNameExpectedDump = new HashMap<String, Object>();
            middleNameExpectedDump.put(Key.VALUE.toString(), Value.NULL.toString());
            propertiesExpectedDump.put("middleName", middleNameExpectedDump);
            
            Map<String, Object> marriedExpectedDump = new HashMap<String, Object>();
            marriedExpectedDump.put(Key.TYPE.toString(), Type.BOOLEAN);
            marriedExpectedDump.put(Key.VALUE.toString(), true);
            propertiesExpectedDump.put("married", marriedExpectedDump);
                 

            propertiesExpectedDump.put("supervisor", supervisorExpectedDump);    
            
            Map<String, Object> favoriteColorsExpectedDump = new HashMap<String, Object>(); 
            favoriteColorsExpectedDump.put(Key.TYPE.toString(), Type.SEQUENCE);
            List<Map<String, Object>> favoriteColorListExpectedDump = new ArrayList<Map<String, Object>>();
            Map<String, Object> color1ExpectedDump = new HashMap<String, Object>();
            color1ExpectedDump.put(Key.TYPE.toString(), Type.STRING);
            color1ExpectedDump.put(Key.VALUE.toString(), "blue");
            favoriteColorListExpectedDump.add(color1ExpectedDump);
            Map<String, Object> color2ExpectedDump = new HashMap<String, Object>();
            color2ExpectedDump.put(Key.TYPE.toString(), Type.STRING); 
            color2ExpectedDump.put(Key.VALUE.toString(), "green"); 
            favoriteColorListExpectedDump.add(color2ExpectedDump);
            favoriteColorsExpectedDump.put(Key.VALUE.toString(), favoriteColorListExpectedDump);        
            propertiesExpectedDump.put("favoriteColors", favoriteColorsExpectedDump);

// This test fails, don't know why
//            Map<String, Object> degreesExpectedDump = new HashMap<String, Object>();
//            degreesExpectedDump.put(Key.TYPE.toString(), Type.HASH);
//            Map<String, Map<String, Object>> degreeMapExpectedDump = new HashMap<String, Map<String, Object>>();
//            Map<String, Object> degree1ExpectedDump = new HashMap<String, Object>();
//            degree1ExpectedDump.put(Key.TYPE.toString(), Type.STRING);
//            degree1ExpectedDump.put(Key.VALUE.toString(), "Mathematics");
//            degreeMapExpectedDump.put("BA", degree1ExpectedDump);
//            Map<String, Object> degree2ExpectedDump = new HashMap<String, Object>();
//            degree2ExpectedDump.put(Key.TYPE.toString(), Type.STRING);
//            degree2ExpectedDump.put(Key.VALUE.toString(), "Computer Science");
//            degreeMapExpectedDump.put("MS", degree2ExpectedDump);
//            degreesExpectedDump.put(Key.VALUE.toString(), degreeMapExpectedDump);
//            propertiesExpectedDump.put("degrees", degreesExpectedDump);
            
        }        
        
        expectedDump.put(Key.PROPERTIES.toString(), propertiesExpectedDump);
        
        // Methods       
        SortedMap<String, Object> methodDump = getEmployeeMethodsExpectedDump(exposureLevel, "Doe");
        if ( ! methodDump.isEmpty()) {
            methodDump.put("boss()", supervisorExpectedDump);
        }
        expectedDump.put(Key.METHODS.toString(), methodDump);
        
        return expectedDump;
    }
    
    private SortedMap<String, Object> getEmployeeMethodsExpectedDump(int exposureLevel, String familyName) {
        
        SortedMap<String, Object> expectedDump = new TreeMap<String, Object>();
        
        if (exposureLevel <= BeansWrapper.EXPOSE_SAFE) {

            Map<String, Object> nameExpectedDump = new HashMap<String, Object>();
            nameExpectedDump.put(Key.TYPE.toString(), "String");
            expectedDump.put("getName(String)", nameExpectedDump);
            
            expectedDump.put("setFavoriteColors(Strings)", Collections.emptyMap());
            
            expectedDump.put("setNickname(String)", Collections.emptyMap());
            
            Map<String, Object> familyNameExpectedDump = new HashMap<String, Object>();
            familyNameExpectedDump.put(Key.TYPE.toString(), Type.STRING);
            familyNameExpectedDump.put(Key.VALUE.toString(), familyName);
            expectedDump.put("familyName()", familyNameExpectedDump);

            Map<String, Object> employeeCountExpectedDump = new HashMap<String, Object>();
            employeeCountExpectedDump.put(Key.TYPE.toString(), Type.NUMBER);
            employeeCountExpectedDump.put(Key.VALUE.toString(), Employee.getEmployeeCount());
            expectedDump.put("getEmployeeCount()", employeeCountExpectedDump);
        }   

        return expectedDump;
    }
    
    private Map<String, Object> getJaneSmithExpectedDump(int exposureLevel) {

        Map<String, Object> expectedDump = new HashMap<String, Object>();

        Map<String, Object> supervisorExpectedDump = new HashMap<String, Object>();
        supervisorExpectedDump.put(Key.VALUE.toString(), Value.NULL.toString());
        
        SortedMap<String, Object> propertiesExpectedDump = new TreeMap<String, Object>();
        
        // Properties 
        if (exposureLevel < BeansWrapper.EXPOSE_NOTHING) {
            
            Map<String, Object> birthdateExpectedDump = new HashMap<String, Object>();
            birthdateExpectedDump.put(Key.TYPE.toString(), Type.DATE);
            birthdateExpectedDump.put(Key.DATE_TYPE.toString(), DateType.UNKNOWN);
            Calendar c = Calendar.getInstance();
            c.set(1975, Calendar.OCTOBER, 25);
            c = DateUtils.truncate(c, Calendar.DATE);
            birthdateExpectedDump.put(Key.VALUE.toString(), c.getTime());
            propertiesExpectedDump.put("birthdate", birthdateExpectedDump);

            Map<String, Object> fullNameExpectedDump = new HashMap<String, Object>();
            fullNameExpectedDump.put(Key.TYPE.toString(), Type.STRING);
            fullNameExpectedDump.put(Key.VALUE.toString(), "Jane Smith");
            propertiesExpectedDump.put("fullName", fullNameExpectedDump);
        
            Map<String, Object> idExpectedDump = new HashMap<String, Object>();
            idExpectedDump.put(Key.TYPE.toString(), Type.NUMBER);
            idExpectedDump.put(Key.VALUE.toString(), 78234);
            propertiesExpectedDump.put("id", idExpectedDump);
        
            Map<String, Object> nicknameExpectedDump = new HashMap<String, Object>();
            nicknameExpectedDump.put(Key.TYPE.toString(), Type.STRING);
            nicknameExpectedDump.put(Key.VALUE.toString(), "");
            propertiesExpectedDump.put("nickname", nicknameExpectedDump);
        
            Map<String, Object> middleNameExpectedDump = new HashMap<String, Object>();
            middleNameExpectedDump.put(Key.VALUE.toString(), Value.NULL.toString());
            propertiesExpectedDump.put("middleName", middleNameExpectedDump);
              
            Map<String, Object> marriedExpectedDump = new HashMap<String, Object>();
            marriedExpectedDump.put(Key.TYPE.toString(), Type.BOOLEAN);
            marriedExpectedDump.put(Key.VALUE.toString(), true);
            propertiesExpectedDump.put("married", marriedExpectedDump);      
        
            propertiesExpectedDump.put("supervisor", supervisorExpectedDump);             
    
            Map<String, Object> favoriteColorsExpectedDump = new HashMap<String, Object>(); 
            favoriteColorsExpectedDump.put(Key.TYPE.toString(), Type.SEQUENCE);
            List<Map<String, Object>> favoriteColorListExpectedDump = new ArrayList<Map<String, Object>>();
            Map<String, Object> color1ExpectedDump = new HashMap<String, Object>();
            color1ExpectedDump.put(Key.TYPE.toString(), Type.STRING);
            color1ExpectedDump.put(Key.VALUE.toString(), "red"); 
            favoriteColorListExpectedDump.add(color1ExpectedDump);
            Map<String, Object> color2ExpectedDump = new HashMap<String, Object>();
            color2ExpectedDump.put(Key.TYPE.toString(), Type.STRING); 
            color2ExpectedDump.put(Key.VALUE.toString(), "orange"); 
            favoriteColorListExpectedDump.add(color2ExpectedDump);
            favoriteColorsExpectedDump.put(Key.VALUE.toString(), favoriteColorListExpectedDump);        
            propertiesExpectedDump.put("favoriteColors", favoriteColorsExpectedDump);
            
//            Map<String, Object> degreesExpectedDump = new HashMap<String, Object>();
//            degreesExpectedDump.put(Key.VALUE.toString(), Value.NULL.toString());
//            propertiesExpectedDump.put("degrees", degreesExpectedDump);            
        }
        expectedDump.put(Key.PROPERTIES.toString(), propertiesExpectedDump);
        
        // Methods
        SortedMap<String, Object> methodDump = getEmployeeMethodsExpectedDump(exposureLevel, "Smith");
        if ( ! methodDump.isEmpty()) {
            methodDump.put("boss()", supervisorExpectedDump);
        }
        expectedDump.put(Key.METHODS.toString(), methodDump);
        
        return expectedDump;
    }
    
}
