// Copyright 2008-2012 severally by the contributors
//
// Licensed 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 net.sf.practicalxml.converter.bean;

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TimeZone;

import javax.xml.XMLConstants;

import org.w3c.dom.Element;

import net.sf.kdgcommons.test.StringAsserts;
import net.sf.practicalxml.XmlUtil;
import net.sf.practicalxml.converter.ConversionException;
import net.sf.practicalxml.converter.ConversionConstants;
import net.sf.practicalxml.converter.bean.Xml2BeanConverter;
import net.sf.practicalxml.converter.bean.Xml2BeanOptions;


import static net.sf.practicalxml.builder.XmlBuilder.*;


public class TestXml2BeanConverter
extends AbstractBeanConverterTestCase
{
    public TestXml2BeanConverter(String name)
    {
        super(name);
    }


//----------------------------------------------------------------------------
//  Support Code
//----------------------------------------------------------------------------

    private static Element createTestData(net.sf.practicalxml.builder.Node... childNodes)
    {
        return element("root", childNodes)
               .toDOM().getDocumentElement();
    }


    private static void assertConversionFailure(
            String failureMessage, String expectedMessageRegex,
            Xml2BeanConverter driver, Element elem, Class<?> klass)
    {
        try
        {
            driver.convert(elem, klass);
            fail(failureMessage);
        }
        catch (ConversionException ex)
        {
//            System.out.println(ex);
            StringAsserts.assertContainsRegex("exception message: " + ex.getMessage(),
                                              expectedMessageRegex, ex.getMessage());
        }
    }


//----------------------------------------------------------------------------
//  Test Data -- although naming convention indicates they're static, they're
//               actually instance variables and can be modified by tests
//----------------------------------------------------------------------------

    private Element TESTDATA_STRING_LIST_WITHOUT_TYPES =
            createTestData(
                element("a", text("foo")),
                element("b", text("bar")),
                element("b", text("baz")),
                element("c", text("bar")));

    private Element TESTDATA_INT_LIST_WITHOUT_TYPES =
            createTestData(
                element("foo", text("12")),
                element("bar", text("78")),
                element("baz", text("-17")));

    private Element TESTDATA_INT_LIST_WITH_XSD_TYPES =
            createTestData(conversionType("java:" + int[].class.getName()),
                element("foo", text("12"), conversionType("xsd:int")),
                element("bar", text("78"), conversionType("xsd:int")),
                element("baz", text("-17"), conversionType("xsd:int")));

    private Element TESTDATA_MIXED_LIST_WITHOUT_TYPES =
            createTestData(
                element("a", text("foo")),
                element("b", text("123.0")),
                element("b", text("456")));

    private Element TESTDATA_MIXED_LIST_WITH_XSD_TYPES =
            createTestData(
                element("a", text("foo"),   conversionType("xsd:string")),
                element("b", text("123.0"), conversionType("xsd:decimal")),
                element("b", text("456"),   conversionType("xsd:int")));

    private Element TESTDATA_MIXED_LIST_WITH_JAVA_TYPES =
            createTestData(
                element("a", text("foo"),   conversionType("java:java.lang.String")),
                element("b", text("123.0"), conversionType("java:java.lang.Double")),
                element("b", text("456"),   conversionType("java:java.math.BigInteger")));

    private Element TESTDATA_LIST_WITH_INVALID_TYPE =
            createTestData(
                element("a", text("foo"), conversionType("java:java.lang.Foo")),
                element("b", text("foo"), conversionType("java:java.lang.String")));   // note that this is valid!


//----------------------------------------------------------------------------
//  Test Cases
//----------------------------------------------------------------------------

    public void testConvertPrimitivesDefault() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        for (PrimitiveValue value : PRIMITIVE_VALUES)
        {
            Element src = createTestData(text(value.getDefaultText()));
            Object dst = driver.convert(src, value.getKlass());
            assertEquals(value.getKlass().getName(), value.getValue(), dst);
        }
    }


    public void testConvertPrimitivesXsdFormat() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.EXPECT_XSD_FORMAT);

        for (PrimitiveValue value : PRIMITIVE_VALUES)
        {
            Element src = createTestData(text(value.getXsdText()));
            Object dst = driver.convert(src, value.getKlass());
            assertEquals(value.getKlass().getName(), value.getValue(), dst);
        }
    }


    public void testFailurePrimitiveWithChildElement() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Element invalid = createTestData(text("foo"), element("bar"));
        assertConversionFailure("converted primitive with element content",
                                "unexpected child",
                                driver, invalid, String.class);
    }


    public void testDeferredExceptionPrimitiveWithChildElement() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.DEFER_EXCEPTIONS);

        Element invalid = createTestData(text("foo"), element("bar"));
        driver.convert(invalid, String.class);

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 1, exes.size());

        ConversionException ex = exes.get(0);
        assertTrue("exception message", ex.getMessage().contains("unexpected child"));
    }


    public void testRequireXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE);

        Element valid = createTestData(text("foo"), conversionType("xsd:string"));
        Object dst = driver.convert(valid, String.class);
        assertEquals("foo", dst);
    }


    public void testFailureRequireXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE);

        Element invalid = createTestData(text("foo"));
        assertConversionFailure("converted element missing xsi:type",
                                "missing type",
                                driver, invalid, String.class);
    }


    public void testDeferredExceptionRequireXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE, Xml2BeanOptions.DEFER_EXCEPTIONS);

        Element invalid = createTestData(text("foo"));
        driver.convert(invalid, String.class);

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 1, exes.size());

        ConversionException ex = exes.get(0);
        assertTrue("exception message", ex.getMessage().contains("missing type"));
    }


    public void testFailureWrongXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE);

        Element invalid = createTestData(text("foo"), conversionType("xsd:int"));
        assertConversionFailure("converted element with incorrect xsi:type",
                                "invalid type",
                                driver, invalid, String.class);
    }


    public void testDeferredExceptionWrongXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE, Xml2BeanOptions.DEFER_EXCEPTIONS);

        Element invalid = createTestData(text("foo"), conversionType("xsd:int"));
        driver.convert(invalid, String.class);

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 1, exes.size());

        ConversionException ex = exes.get(0);
        assertTrue("exception message", ex.getMessage().contains("invalid type"));
    }


    public void testConvertEnumDefault() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Element src = createTestData(text(MyEnum.BAR.name()));
        Object dst = driver.convert(src, MyEnum.class);
        assertEquals(MyEnum.BAR, dst);
    }


    public void testConvertEnumStringValue() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.ENUM_AS_STRING_VALUE);

        Element src = createTestData(text(MyEnum.BAR.toString()));
        Object dst = driver.convert(src, MyEnum.class);
        assertEquals(MyEnum.BAR, dst);
    }


    public void testFailureEnumWrongXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE);

        Element invalid = createTestData(text("BAR"), conversionType("xsd:int"));
        assertConversionFailure("converted element with incorrect xsi:type",
                                "invalid type",
                                driver, invalid, MyEnum.class);
    }


    public void testDeferredExceptionEnumWrongXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE, Xml2BeanOptions.DEFER_EXCEPTIONS);

        Element invalid = createTestData(text("BAR"), conversionType("xsd:int"));
        driver.convert(invalid, MyEnum.class);

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 1, exes.size());

        ConversionException ex = exes.get(0);
        assertTrue("exception message", ex.getMessage().contains("invalid type"));
    }


    public void testConvertNullDefault() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Element src = createTestData();

        assertNull(driver.convert(src, String.class));
        assertNull(driver.convert(src, SimpleBean.class));
    }


    public void testConvertNullRequireXsiNil() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_XSI_NIL);

        Element valid = createTestData(xsiNil(true));
        assertNull(driver.convert(valid, String.class));
    }


    public void testFailureConvertNullRequireXsiNil() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_XSI_NIL);

        Element invalid = createTestData();
        assertConversionFailure("able to convert null data with REQUIRE_XSI_NIL set",
                                "missing.*xsi:nil",
                                driver, invalid, String.class);
    }


    public void testDeferredExceptionConvertNullRequireXsiNil() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_XSI_NIL, Xml2BeanOptions.DEFER_EXCEPTIONS);

        Element invalid = createTestData();
        driver.convert(invalid, String.class);

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 1, exes.size());

        ConversionException ex = exes.get(0);
        StringAsserts.assertContainsRegex("exception message", "missing.*xsi:nil", ex.getMessage());
    }


    public void testConvertEmptyStringDefault() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        String str = "   \n \t  ";
        Element src = createTestData(text(str));
        Object dst = driver.convert(src, String.class);
        assertEquals(str, dst);
    }


    public void testConvertEmptyStringToNull() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.EMPTY_IS_NULL);

        Element src = createTestData(text("   \n\t   "));
        assertNull(driver.convert(src, String.class));
    }


    public void testConvertEmptyStringToNullAndRequireXsiNull() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(
                                        Xml2BeanOptions.EMPTY_IS_NULL,
                                        Xml2BeanOptions.REQUIRE_XSI_NIL);

        Element valid = createTestData(text("  \t  "), xsiNil(true));
        assertNull(driver.convert(valid, String.class));
    }


    public void testFailureConvertEmptyStringToNullAndRequireXsiNull() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(
                                        Xml2BeanOptions.EMPTY_IS_NULL,
                                        Xml2BeanOptions.REQUIRE_XSI_NIL);

        Element invalid = createTestData(text("  \t  "));
        assertConversionFailure("able to convert blank string with REQUIRE_XSI_NIL set",
                                "missing.*xsi:nil",
                                driver, invalid, String.class);
    }


    public void testDeferredExceptionConvertEmptyStringToNullAndRequireXsiNull() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(
                                        Xml2BeanOptions.EMPTY_IS_NULL,
                                        Xml2BeanOptions.REQUIRE_XSI_NIL,
                                        Xml2BeanOptions.DEFER_EXCEPTIONS);

        Element invalid = createTestData();
        driver.convert(invalid, String.class);

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 1, exes.size());

        ConversionException ex = exes.get(0);
        StringAsserts.assertContainsRegex("exception message", "missing.*xsi:nil", ex.getMessage());
    }


    public void testConvertPrimitiveArray() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        int[] result = driver.convert(TESTDATA_INT_LIST_WITHOUT_TYPES, int[].class);
        assertEquals(3, result.length);
        assertEquals(12, result[0]);
        assertEquals(78, result[1]);
        assertEquals(-17, result[2]);
    }


    public void testConvertPrimitiveArrayRequireXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE);

        int[] result = driver.convert(TESTDATA_INT_LIST_WITH_XSD_TYPES, int[].class);
        assertEquals(3, result.length);
        assertEquals(12, result[0]);
        assertEquals(78, result[1]);
        assertEquals(-17, result[2]);
    }


    public void testFailurePrimitiveArrayRequireXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE);

        assertConversionFailure("able to convert data sans type with REQUIRE_XSI_TYPE set",
                                "missing type",
                                driver, TESTDATA_INT_LIST_WITHOUT_TYPES, int[].class);
    }


    public void testDeferredExceptionPrimitiveArrayRequireXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE, Xml2BeanOptions.DEFER_EXCEPTIONS);

        driver.convert(TESTDATA_INT_LIST_WITHOUT_TYPES, int[].class);

        // we'll get one exception for each element of the list, plus the root
        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 4, exes.size());

        for (ConversionException ex : exes)
        {
            StringAsserts.assertContainsRegex("exception message", "missing type", ex.getMessage());
        }
    }


    public void testConvertListAssumingString() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        List<?> result = driver.convert(TESTDATA_MIXED_LIST_WITHOUT_TYPES, List.class);
        assertEquals(3, result.size());

        Iterator<?> itx = result.iterator();
        assertEquals("foo", itx.next());
        assertEquals("123.0", itx.next());
        assertEquals("456", itx.next());
        assertFalse(itx.hasNext());
    }


    public void testConvertListWithXsiTypes() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        List<?> result = driver.convert(TESTDATA_MIXED_LIST_WITH_XSD_TYPES, List.class);
        assertEquals(3, result.size());

        Iterator<?> itx = result.iterator();
        assertEquals("foo", itx.next());
        assertEquals(new BigDecimal("123.0"), itx.next());
        assertEquals(Integer.valueOf(456), itx.next());
        assertFalse(itx.hasNext());
    }


    public void testConvertListWithJavaType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        List<?> result = driver.convert(TESTDATA_MIXED_LIST_WITH_JAVA_TYPES, List.class);
        assertEquals(3, result.size());

        Iterator<?> itx = result.iterator();
        assertEquals("foo", itx.next());
        assertEquals(new Double("123.0"), itx.next());
        assertEquals(new BigInteger("456"), itx.next());
        assertFalse(itx.hasNext());

    }


    public void testFailureListWithInvalidType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        assertConversionFailure("converted unknown type", "unable to resolve type",
                                driver, TESTDATA_LIST_WITH_INVALID_TYPE, List.class);
    }


    public void testDeferredExceptionsListWithInvalidType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.DEFER_EXCEPTIONS);

        List<?> result = driver.convert(TESTDATA_LIST_WITH_INVALID_TYPE, List.class);
        assertEquals("count of items successfully converted", 1, result.size());
        assertEquals("foo", result.get(0));

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("count of deferred exceptions", 1, exes.size());

        ConversionException ex = exes.get(0);
        assertTrue("exception message", ex.getMessage().contains("unable to resolve type"));
    }


    public void testConvertSetAssumingString() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Set<?> result = driver.convert(TESTDATA_STRING_LIST_WITHOUT_TYPES, Set.class);
        assertEquals(3, result.size());
        assertTrue(result.contains("bar"));
        assertTrue(result.contains("baz"));
        assertTrue(result.contains("foo"));
    }


    public void testConvertSortedSetAssumingString() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        SortedSet<?> result = driver.convert(TESTDATA_STRING_LIST_WITHOUT_TYPES, SortedSet.class);
        assertEquals(3, result.size());

        // unlise the previous test, we can check order
        Iterator<?> itx = result.iterator();
        assertEquals("bar", itx.next());
        assertEquals("baz", itx.next());
        assertEquals("foo", itx.next());
    }


    public void testConvertCollectionAssumingString() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Collection<?> result = driver.convert(TESTDATA_STRING_LIST_WITHOUT_TYPES, Collection.class);
        assertEquals(4, result.size());

        // arbitrary collections are requivalent to lists
        Iterator<?> itx = result.iterator();
        assertEquals("foo", itx.next());
        assertEquals("bar", itx.next());
        assertEquals("baz", itx.next());
        assertEquals("bar", itx.next());
    }


    // this handles the case where we're processing a bean that uses interfaces
    // but the source document has a concrete type
    public void testConvertCollectionRequireXsiTypeWithConcreteType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE);

        updateConversionType(TESTDATA_MIXED_LIST_WITH_XSD_TYPES, "java:java.util.ArrayList");

        Collection<?> result = driver.convert(TESTDATA_MIXED_LIST_WITH_XSD_TYPES, Collection.class);
        assertEquals(3, result.size());

        Iterator<?> itx = result.iterator();
        assertEquals("foo", itx.next());
        assertEquals(new BigDecimal("123.0"), itx.next());
        assertEquals(Integer.valueOf(456), itx.next());
        assertFalse(itx.hasNext());
    }


    public void testConvertMapDefaultKeyAssumingString() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        // we want distinct child names -- and overlapping ones -- because
        // the converter should ignore them -- also note duplicate key
        Element data = createTestData(
                                element("a", text("foo"), conversionKey("argle")),
                                element("b", text("bar"), conversionKey("bargle")),
                                element("b", text("baz"), conversionKey("argle")),
                                element("c", text("bar"), conversionKey("wargle")));

        Map<?,?> result = driver.convert(data, Map.class);
        assertEquals(3, result.size());
        assertEquals("baz", result.get("argle"));
        assertEquals("bar", result.get("bargle"));
        assertEquals("bar", result.get("wargle"));
    }


    public void testConvertSortedMapDefaultKeyAssumingString() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Element data = createTestData(
                                element("a", text("foo"), conversionKey("argle")),
                                element("b", text("bar"), conversionKey("bargle")),
                                element("c", text("arb"), conversionKey("wargle")),
                                element("b", text("baz"), conversionKey("argle")));

        SortedMap<?,?> result = driver.convert(data, SortedMap.class);
        assertEquals(3, result.size());

        Iterator<?> itx = result.keySet().iterator();
        assertEquals("argle", itx.next());
        assertEquals("bargle", itx.next());
        assertEquals("wargle", itx.next());

        assertEquals("baz", result.get("argle"));
        assertEquals("bar", result.get("bargle"));
        assertEquals("arb", result.get("wargle"));
    }


    public void testConvertMapNameAsKeyAssumingString() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        // no attributes this time, and note the duplicate element name
        Element data = createTestData(
                                element("a", text("foo")),
                                element("b", text("bar")),
                                element("b", text("baz")),
                                element("c", text("bar")));

        Map<?,?> result = driver.convert(data, Map.class);
        assertEquals(3, result.size());
        assertEquals("foo", result.get("a"));
        assertEquals("baz", result.get("b"));
        assertEquals("bar", result.get("c"));
    }


    public void testConvertMapNameAsKeyUsingXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        // this time we have unique element names
        Element data = createTestData(
                            element("a", text("foo"),   conversionType("xsd:string")),
                            element("b", text("123"),   conversionType("xsd:int")),
                            element("c", text("123.0"), conversionType("xsd:decimal")),
                            element("d", text("456"),   conversionType("xsd:string")));

        Map<?,?> result = driver.convert(data, Map.class);
        assertEquals(4, result.size());
        assertEquals("foo", result.get("a"));
        assertEquals(Integer.valueOf(123), result.get("b"));
        assertEquals(new BigDecimal("123.0"), result.get("c"));
        assertEquals("456", result.get("d"));
    }


    public void testConvertMapNameAsKeyUsingJavaType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        // this time we have unique element names
        Element data = createTestData(
                            element("b", text("123"), conversionType("java:java.lang.Integer")));

        Map<?,?> result = driver.convert(data, Map.class);
        assertEquals(1, result.size());
        assertEquals(Integer.valueOf(123), result.get("b"));
    }


    public void testConvertMapWithinMapDefault() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        // note: must specify class on child map, otherwise converter won't know
        // how to interpret data
        Element data = createTestData(
                            element("a", text("baz"), conversionKey("foo")),
                            element("b", conversionKey("bar"), conversionType(HashMap.class),
                                    element("c", text("wargle"), conversionKey("argle")),
                                    element("d", text("zargle"), conversionKey("bargle"))));

        Map<?,?> result = driver.convert(data, Map.class);
        assertEquals(2, result.size());
        assertEquals("baz", result.get("foo"));

        Map<?,?> child = (Map<?,?>)result.get("bar");
        assertEquals(2, child.size());
        assertEquals("wargle", child.get("argle"));
        assertEquals("zargle", child.get("bargle"));
    }


    public void testSimpleBeanDefault() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Element data = createTestData(
                            element("sval", text("foo")),
                            element("ival", text("123")),
                            element("dval", text("123.456")),
                            element("bval", text("true")));

        SimpleBean result = driver.convert(data, SimpleBean.class);
        assertEquals("foo", result.getSval());
        assertEquals(123, result.getIval());
        assertEquals(new BigDecimal("123.456"), result.getDval());
        assertEquals(true, result.isBval());
    }


    public void testSimpleBeanWithMissingValues() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Element data = createTestData(
                            element("sval", text("foo")),
                            element("ival", text("123")));

        SimpleBean result = driver.convert(data, SimpleBean.class);
        assertEquals("foo", result.getSval());
        assertEquals(123, result.getIval());
        assertNull(result.getDval());
        assertEquals(false, result.isBval());
    }


    public void testSimpleBeanRequireXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE);

        Element valid = createTestData(
                            conversionType("java:" + SimpleBean.class.getName()),
                            element("sval", text("foo"),    conversionType("xsd:string")),
                            element("ival", text("123"),    conversionType("xsd:int")),
                            element("dval", text("123.456"),conversionType("xsd:decimal")),
                            element("bval", text("true"),   conversionType("xsd:boolean")));

        SimpleBean result = driver.convert(valid, SimpleBean.class);
        assertEquals("foo", result.getSval());
        assertEquals(123, result.getIval());
        assertEquals(new BigDecimal("123.456"), result.getDval());
        assertEquals(true, result.isBval());

        Element invalid1 = createTestData(
                            element("sval", text("foo"),    conversionType("xsd:string")),
                            element("ival", text("123"),    conversionType("xsd:int")),
                            element("dval", text("123.456"),conversionType("xsd:decimal")),
                            element("bval", text("true"),   conversionType("xsd:boolean")));
        assertConversionFailure("didn't throw when missing xsi:type on top level",
                                "missing type",
                                driver, invalid1, SimpleBean.class);

        Element invalid2 = createTestData(
                            conversionType("java:" + SimpleBean.class.getName()),
                            element("sval", text("foo")),
                            element("ival", text("123")),
                            element("dval", text("123.456")),
                            element("bval", text("true")));
        assertConversionFailure("didn't throw when missing xsi:type on component level",
                                "missing type",
                                driver, invalid2, SimpleBean.class);
    }


    public void testSimpleBeanWithExtraValues() throws Exception
    {
        Element data = createTestData(
                            element("sval", text("foo")),
                            element("ival", text("123")),
                            element("zippy", text("pinhead")));

        Xml2BeanConverter driver1 = new Xml2BeanConverter();

        assertConversionFailure("converted bean when extra fields present in XML",
                                "can't find.*setter.*zippy",
                              driver1, data, SimpleBean.class);

        Xml2BeanConverter driver2 = new Xml2BeanConverter(Xml2BeanOptions.IGNORE_MISSING_PROPERTIES);

        SimpleBean result = driver2.convert(data, SimpleBean.class);
        assertEquals("foo", result.getSval());
        assertEquals(123, result.getIval());
        assertNull(result.getDval());
        assertEquals(false, result.isBval());
    }


    public void testBeanArray() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Element data = createTestData(
                            element("idx0",
                                element("sval", text("foo")),
                                element("ival", text("123")),
                                element("dval", text("123.456")),
                                element("bval", text("true"))),
                            element("idx1",
                                element("sval", text("bar")),
                                element("ival", text("456")),
                                element("dval", text("456.789")),
                                element("bval", text("false"))));

        SimpleBean[] result = driver.convert(data, SimpleBean[].class);
        assertEquals(2, result.length);

        assertEquals("foo", result[0].getSval());
        assertEquals(123, result[0].getIval());
        assertEquals(new BigDecimal("123.456"), result[0].getDval());
        assertEquals(true, result[0].isBval());

        assertEquals("bar", result[1].getSval());
        assertEquals(456, result[1].getIval());
        assertEquals(new BigDecimal("456.789"), result[1].getDval());
        assertEquals(false, result[1].isBval());
    }


    public void testConvertCompoundBean() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Element data = createTestData(
                            element("simple",
                                element("sval", text("foo")),
                                element("ival", text("123")),
                                element("dval", text("456")),
                                element("bval", text("true"))),
                            element("primArray",
                                element("idx1", text("1")),
                                element("idx2", text("2")),
                                element("idx3", text("3"))),
                            element("stringList",
                                element("idx1", text("foo")),
                                element("idx2", text("bar")),
                                element("idx3", text("baz"))));

        CompoundBean result = driver.convert(data, CompoundBean.class);

        assertEquals("foo",     result.getSimple().getSval());
        assertEquals(123,       result.getSimple().getIval());
        assertEquals(456,       result.getSimple().getDval().intValue());   // laziness prevails
        assertEquals(true,      result.getSimple().isBval());
        assertEquals(1,         result.getPrimArray()[0]);
        assertEquals(2,         result.getPrimArray()[1]);
        assertEquals(3,         result.getPrimArray()[2]);
        assertEquals("foo",     result.getStringList().get(0));
        assertEquals("bar",     result.getStringList().get(1));
        assertEquals("baz",     result.getStringList().get(2));
    }


    public void testConvertCompoundBeanRequireXsiType() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.REQUIRE_TYPE);

        Element data = createTestData(
                            conversionType("java:" + CompoundBean.class.getName()),
                            element("simple",
                                conversionType("java:" + SimpleBean.class.getName()),
                                element("sval", text("foo"), conversionType("xsd:string")),
                                element("ival", text("123"), conversionType("xsd:int")),
                                element("dval", text("456"), conversionType("xsd:decimal")),
                                element("bval", text("true"), conversionType("xsd:boolean"))),
                            element("primArray",
                                conversionType("java:" + int[].class.getName()),
                                element("idx1", text("1"), conversionType("xsd:int")),
                                element("idx2", text("2"), conversionType("xsd:int")),
                                element("idx3", text("3"), conversionType("xsd:int"))),
                            element("stringList",
                                conversionType("java:" + List.class.getName()),
                                element("idx1", text("foo"), conversionType("xsd:string")),
                                element("idx2", text("bar"), conversionType("xsd:string")),
                                element("idx3", text("baz"), conversionType("xsd:string"))));

        CompoundBean result = driver.convert(data, CompoundBean.class);

        assertEquals("foo",     result.getSimple().getSval());
        assertEquals(123,       result.getSimple().getIval());
        assertEquals(456,       result.getSimple().getDval().intValue());
        assertEquals(true,      result.getSimple().isBval());
        assertEquals(1,         result.getPrimArray()[0]);
        assertEquals(2,         result.getPrimArray()[1]);
        assertEquals(3,         result.getPrimArray()[2]);
        assertEquals("foo",     result.getStringList().get(0));
        assertEquals("bar",     result.getStringList().get(1));
        assertEquals("baz",     result.getStringList().get(2));
    }


    public void testReadOnlyBean() throws Exception
    {
        Element data = createTestData(
                            element("sval", text("foo")));
        Xml2BeanConverter driver = new Xml2BeanConverter();
        assertConversionFailure("converted bean without setter",
                                "can't find.*setter.*sval",
                                driver, data, ReadOnlyBean.class);
    }


    public void testDeferredExceptionsReadOnlyBean() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.DEFER_EXCEPTIONS);

        Element invalid = createTestData(
                            element("sval", text("foo")));
        driver.convert(invalid, ReadOnlyBean.class);

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 1, exes.size());

        ConversionException ex = exes.get(0);
        StringAsserts.assertContainsRegex("exception message", "can't find.*setter.*sval", ex.getMessage());
    }


    public void testNoninstantiableBean() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        Element data = createTestData(
                            element("data", text("foo")));

        assertConversionFailure("converted bean without public no-arg ctor",
                                "unable to instantiate",
                                driver, data, NonInstantiableBean.class);
    }


    public void testDeferredExceptionsNoninstantiableBean() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.DEFER_EXCEPTIONS);

        Element invalid = createTestData(
                            element("data", text("foo")));

        driver.convert(invalid, NonInstantiableBean.class);

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 1, exes.size());

        ConversionException ex = exes.get(0);
        StringAsserts.assertContainsRegex("exception message", "unable to instantiate", ex.getMessage());
    }


    public void testIgnoredAttributes() throws Exception
    {
        Element data = createTestData(
                            attribute("sval", "foo"),
                            element("ival", text("123")));

        SimpleBean bean = new Xml2BeanConverter()
                          .convert(data, SimpleBean.class);
        assertNotNull(bean);
        assertEquals(123, bean.getIval());
        assertNull(bean.getSval());
    }


    public void testIgnoredAttributesNullBean() throws Exception
    {
        Element data = createTestData(
                            attribute("sval", "foo"),
                            attribute("ival", "123"));

        SimpleBean bean = new Xml2BeanConverter()
                          .convert(data, SimpleBean.class);
        assertNull(bean);
    }


    // even if we enable attributes, we ignore attributes with the converter
    // namespace or XML schema instance namespace
    public void testIgnoredConverterAttributes() throws Exception
    {
        Element data = createTestData(
                            attribute(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "sval", "foo"),
                            attribute(ConversionConstants.NS_CONVERSION, "ival", "123"));

        SimpleBean bean = new Xml2BeanConverter()
                          .convert(data, SimpleBean.class);
        assertNull(bean);
    }


    public void testBeanFromAttributesIgnoreNamespace() throws Exception
    {
        Element data = createTestData(
                            attribute("sval", "foo"),
                            attribute("ival", "123"));

        SimpleBean bean = new Xml2BeanConverter(Xml2BeanOptions.CONVERT_ATTRIBUTES)
                          .convert(data, SimpleBean.class);
        assertNotNull(bean);
        assertEquals("foo", bean.getSval());
        assertEquals(123, bean.getIval());
    }


    public void testBeanFromAttributesMatchNamespace() throws Exception
    {
        Element data = element("ns", "root",
                            attribute("ns", "sval", "foo"),
                            attribute("ival", "123"))
                            .toDOM().getDocumentElement();

        SimpleBean bean = new Xml2BeanConverter(Xml2BeanOptions.CONVERT_ATTRIBUTES_MATCH_NAMESPACE)
                          .convert(data, SimpleBean.class);
        assertNotNull(bean);
        assertEquals("foo", bean.getSval());
        assertEquals(0, bean.getIval());
    }


    public void testBeanFromAttributesMatchNullNamespace() throws Exception
    {
        Element data = createTestData(
                            attribute("ns", "sval", "foo"),
                            attribute("ival", "123"));

        SimpleBean bean = new Xml2BeanConverter(Xml2BeanOptions.CONVERT_ATTRIBUTES_MATCH_NAMESPACE)
                          .convert(data, SimpleBean.class);
        assertNotNull(bean);
        assertNull(bean.getSval());
        assertEquals(123, bean.getIval());
    }


    // element value will take precedence over attribute value
    public void testBeanMixedAttributeAndElement() throws Exception
    {
        Element data = createTestData(
                            attribute("sval", "foo"),
                            attribute("ival", "123"),
                            element("sval", text("bar")));

        SimpleBean bean = new Xml2BeanConverter(Xml2BeanOptions.CONVERT_ATTRIBUTES)
                          .convert(data, SimpleBean.class);
        assertNotNull(bean);
        assertEquals("bar", bean.getSval());
        assertEquals(123, bean.getIval());
    }


    public void testDeferredExceptions() throws Exception
    {
        Element data = createTestData(
                            element("data", text("foo")),
                            element("throw1", text("123")),
                            element("throw2", text("456")));

        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.DEFER_EXCEPTIONS);
        ThrowingBean bean = driver.convert(data, ThrowingBean.class);

        assertEquals("value that can be populated", "foo", bean.getData());

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 2, exes.size());

        for (ConversionException ex : exes)
        {
            Throwable cause = ex.getCause();
            if (cause instanceof InvocationTargetException)
                cause = cause.getCause();

            if (ex.getMessage().contains("setThrow1"))
                assertEquals("throw1 cause", IllegalArgumentException.class, cause.getClass());
            else if (ex.getMessage().contains("setThrow2"))
                assertEquals("throw2 cause", IllegalStateException.class, cause.getClass());
            else
                fail("unexpected exception: ex");
        }
    }


    public void testDeferredExceptionsOnSuccess() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.DEFER_EXCEPTIONS);

        Element data = createTestData(text("foo"));
        driver.convert(data, Object.class);

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 0, exes.size());
    }


    public void testGetDeferredExceptionsDoesNotReturnNull() throws Exception
    {
        Xml2BeanConverter driver = new Xml2BeanConverter();

        // no need to actually do anything

        List<ConversionException> exes = driver.getDeferredExceptions();
        assertEquals("number of deferred exceptions", 0, exes.size());
    }


    public void testCalendarXsdFormat() throws Exception
    {
        long millis = 1377731547000L;
        Date theDate = new Date(millis);

        Element data = createTestData(
                element("value",
                    element("date",                 text(XmlUtil.formatXsdDatetime(theDate))),
                    element("millis",               text(String.valueOf(millis))),
                    element("timezone",             text("GMT")),
                    element("firstDayOfWeek",       text("0")),
                    element("minimumDaysInFirstWeek", text("1"))));

        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.EXPECT_XSD_FORMAT);
        CalendarBean bean = driver.convert(data, CalendarBean.class);

        assertEquals("date",                     theDate,                       bean.getValue().getTime());
        assertEquals("millis",                   millis,                        bean.getValue().getTimeInMillis());
        assertEquals("timezone",                 TimeZone.getTimeZone("GMT"),   bean.getValue().getTimeZone());
        assertEquals("firstDayOfWeek",           0,                             bean.getValue().getFirstDayOfWeek());
        assertEquals("minimumDaysInFirstWeek",   1,                             bean.getValue().getMinimalDaysInFirstWeek());
    }


    public void testDateSubclassesXsdFormat() throws Exception
    {
        Date theDate = new Date(1377731547000L);
        String strDate = XmlUtil.formatXsdDatetime(theDate);

        Element data = createTestData(
                element("date",     text(strDate)),
                element("sqlDate",  text(strDate)),
                element("sqlTime",  text(strDate)),
                element("timestamp",text(strDate)),
                element("myDate",   text(strDate)));

        Xml2BeanConverter driver = new Xml2BeanConverter(Xml2BeanOptions.EXPECT_XSD_FORMAT);
        DateBean bean = driver.convert(data, DateBean.class);

        assertEquals("java.util.Date", theDate, bean.getDate());
    }
}
