/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package freemarker.template.utility;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import freemarker.template.utility.DateUtil.CalendarFieldsToDateConverter;
import freemarker.template.utility.DateUtil.DateParseException;
import freemarker.template.utility.DateUtil.DateToISO8601CalendarFactory;
import freemarker.template.utility.DateUtil.TrivialCalendarFieldsToDateConverter;
import junit.framework.TestCase;

public class DateUtilTest extends TestCase {
    
    private final TimeZone originalDefaultTZ = TimeZone.getDefault();

    @Override
    protected void setUp() throws Exception {
        TimeZone.setDefault(TimeZone.getTimeZone("Europe/Prague"));
    }

    @Override
    protected void tearDown() throws Exception {
        TimeZone.setDefault(originalDefaultTZ);
    }

    private final DateFormat df
            = new SimpleDateFormat("G yyyy-MM-dd HH:mm:ss:S Z", Locale.US);
    {
        df.setTimeZone(DateUtil.UTC);
    }
    
    private CalendarFieldsToDateConverter cf2dc = new TrivialCalendarFieldsToDateConverter();
    
    private DateToISO8601CalendarFactory calendarFactory
            = new DateUtil.TrivialDateToISO8601CalendarFactory();
    
    public DateUtilTest(String name) {
        super(name);
    }
    
    public void testDateToUTCString() throws ParseException {
        assertEquals(
                "1998-10-30T15:30:00.512Z",
                dateToISO8601UTCDateTimeMSString(
                        df.parse("AD 1998-10-30 19:30:00:512 +0400"), true));
        assertEquals(
                "1998-10-30T15:30:00.5Z",
                dateToISO8601UTCDateTimeMSString(
                        df.parse("AD 1998-10-30 19:30:00:500 +0400"), true));
        assertEquals(
                "1998-10-30T15:30:00.51Z",
                dateToISO8601UTCDateTimeMSString(
                        df.parse("AD 1998-10-30 19:30:00:510 +0400"), true));
        assertEquals(
                "1998-10-30T15:30:00.1Z",
                dateToISO8601UTCDateTimeMSString(
                        df.parse("AD 1998-10-30 19:30:00:100 +0400"), true));
        assertEquals(
                "1998-10-30T15:30:00.01Z",
                dateToISO8601UTCDateTimeMSString(
                        df.parse("AD 1998-10-30 19:30:00:10 +0400"), true));
        assertEquals(
                "1998-10-30T15:30:00.001Z",
                dateToISO8601UTCDateTimeMSString(
                        df.parse("AD 1998-10-30 19:30:00:1 +0400"), true));
        assertEquals(
                "2000-02-08T06:05:04Z",
                dateToISO8601UTCDateTimeMSString(
                        df.parse("AD 2000-02-08 09:05:04:0 +0300"), true));
        assertEquals(
                "0099-02-28T06:15:24Z",
                dateToISO8601UTCDateTimeString(
                        df.parse(
                        "AD 0099-03-02 09:15:24:0 +0300"), true));
        assertEquals(
                "0010-02-28T06:15:24Z",
                dateToISO8601UTCDateTimeString(
                        df.parse("AD 0010-03-02 09:15:24:0 +0300"), true));
        assertEquals(
                "0001-02-28T06:15:24Z",
                dateToISO8601UTCDateTimeString(
                        df.parse("AD 0001-03-02 09:15:24:0 +0300"), true));
        assertEquals(
                "0000-02-29T06:15:24Z",
                dateToISO8601UTCDateTimeString(
                        df.parse("BC 0001-03-02 09:15:24:0 +0300"), true));
        assertEquals(
                "-1-02-28T06:15:24Z",
                dateToISO8601UTCDateTimeString(
                        df.parse("BC 2-03-02 09:15:24:0 +0300"), true));
        assertEquals(
                "10000-02-28T06:15:24Z",
                dateToISO8601UTCDateTimeString(
                        df.parse("AD 10000-02-28 09:15:24:0 +0300"), true));

        Date d = df.parse("AD 1998-10-30 19:30:00:512 +0400");
        assertEquals(
                "1998-10-30",
                dateToISO8601UTCDateString(d));
        assertEquals(
                "15:30:00.512Z",
                dateToISO8601UTCTimeMSString(d, true));
        assertEquals(
                "15:30:00.512",
                dateToISO8601UTCTimeMSString(d, false));
        assertEquals(
                "1998-10-30",
                dateToISO8601UTCDateString(
                        new java.sql.Date(d.getTime())));
        assertEquals(
                "15:30:00.512Z",
                dateToISO8601UTCTimeMSString(
                        new java.sql.Time(d.getTime()), true));
    }

    public void testLocalTime() throws ParseException {
        Date dsum = df.parse("AD 2010-05-09 20:00:00:0 UTC");
        Date dwin = df.parse("AD 2010-01-01 20:00:00:0 UTC");
        
        TimeZone tzRome = TimeZone.getTimeZone("Europe/Rome");
        if (tzRome.getOffset(0) == 0) {
            throw new RuntimeException(
                    "Can't get time zone for Europe/Rome!");
        }
        assertEquals(
                "2010-05-09T22:00:00+02:00",
                dateToISO8601DateTimeString(dsum, tzRome));
        assertEquals(
                "2010-01-01T21:00:00+01:00",
                dateToISO8601DateTimeString(dwin, tzRome));
        assertEquals(
                "2010-05-09",
                dateToISO8601DateString(dsum, tzRome));
        assertEquals(
                "2010-01-01",
                dateToISO8601DateString(dwin, tzRome));
        assertEquals(
                "22:00:00+02:00",
                dateToISO8601TimeString(dsum, tzRome));
        assertEquals(
                "21:00:00+01:00",
                dateToISO8601TimeString(dwin, tzRome));
        
        TimeZone tzNY = TimeZone.getTimeZone("America/New_York");
        if (tzNY.getOffset(0) == 0) {
            throw new RuntimeException(
                    "Can't get time zone for America/New_York!");
        }
        assertEquals(
                "2010-05-09T16:00:00-04:00",
                dateToISO8601DateTimeString(dsum, tzNY));
        assertEquals(
                "2010-01-01T15:00:00-05:00",
                dateToISO8601DateTimeString(dwin, tzNY));
        assertEquals(
                "2010-05-09",
                dateToISO8601DateString(dsum, tzNY));
        assertEquals(
                "2010-01-01",
                dateToISO8601DateString(dwin, tzNY));
        assertEquals(
                "16:00:00-04:00",
                dateToISO8601TimeString(dsum, tzNY));
        assertEquals(
                "15:00:00-05:00",
                dateToISO8601TimeString(dwin, tzNY));
        
        TimeZone tzFixed = TimeZone.getTimeZone("GMT+02:30");
        assertEquals(
                "2010-05-09T22:30:00+02:30",
                dateToISO8601DateTimeString(dsum, tzFixed));
        assertEquals(
                "2010-01-01T22:30:00+02:30",
                dateToISO8601DateTimeString(dwin, tzFixed));
    }

    public void testGetTimeZone() throws Exception {
        assertTrue(DateUtil.getTimeZone("GMT") != DateUtil.UTC);
        assertTrue(DateUtil.getTimeZone("UT1") != DateUtil.UTC);
        assertEquals(DateUtil.getTimeZone("UTC"), DateUtil.UTC);
        
        assertEquals(DateUtil.getTimeZone("Europe/Rome"),
                TimeZone.getTimeZone("Europe/Rome"));
        
        assertEquals(DateUtil.getTimeZone("Iceland"), // GMT and no DST
                TimeZone.getTimeZone("Iceland"));
        
        try {
            DateUtil.getTimeZone("Europe/NoSuch");
            fail();
        } catch (UnrecognizedTimeZoneException e) {
            // good
        }
    }
    
    public void testTimeOnlyDate() throws UnrecognizedTimeZoneException {
        Date t = new Date(0L);
        SimpleDateFormat tf = new SimpleDateFormat("HH:mm:ss");
        
        tf.setTimeZone(DateUtil.UTC);
        assertEquals("00:00:00", tf.format(t));
        assertEquals("00:00:00",
                dateToISO8601UTCTimeString(t, false));
        
        TimeZone gmt1 = DateUtil.getTimeZone("GMT+01"); 
        tf.setTimeZone(gmt1);
        assertEquals("01:00:00", tf.format(t)); 
        assertEquals("01:00:00+01:00",
                dateToISO8601TimeString(t, gmt1));
    }
    
    public void testAccuracy() throws ParseException {
        Date d = df.parse("AD 2000-02-08 09:05:04:250 UTC"); 
        assertEquals("2000-02-08T09:05:04Z",
                dateToISO8601UTCDateTimeString(d, true));
        assertEquals("2000-02-08T09:05:04.25Z",
                dateToISO8601String(d, true, true, true,
                        DateUtil.ACCURACY_MILLISECONDS, null));
        assertEquals("2000-02-08T09:05:04Z",
                dateToISO8601String(d, true, true, true,
                        DateUtil.ACCURACY_SECONDS, null));
        assertEquals("2000-02-08T09:05Z",
                dateToISO8601String(d, true, true, true,
                        DateUtil.ACCURACY_MINUTES, null));
        assertEquals("2000-02-08T09Z",
                dateToISO8601String(d, true, true, true,
                        DateUtil.ACCURACY_HOURS, null));
        
        d = df.parse("AD 1998-10-30 19:30:00:000 +0400");
        assertEquals(
                "15:30:00Z",
                dateToISO8601UTCTimeMSString(d, true));
        assertEquals(
                "15:30:00.000Z",
                dateToISO8601UTCTimeMSFString(d, true));
        assertEquals(
                "1998-10-30T15:30:00Z",
                dateToISO8601UTCDateTimeMSString(d, true));
        assertEquals(
                "1998-10-30T15:30:00.000Z",
                dateToISO8601UTCDateTimeMSFString(d, true));
                
        d = df.parse("AD 1998-10-30 19:30:00:100 +0400");
        assertEquals(
                "15:30:00.1Z",
                dateToISO8601UTCTimeMSString(d, true));
        assertEquals(
                "15:30:00.100Z",
                dateToISO8601UTCTimeMSFString(d, true));
        assertEquals(
                "1998-10-30T15:30:00.1Z",
                dateToISO8601UTCDateTimeMSString(d, true));
        assertEquals(
                "1998-10-30T15:30:00.100Z",
                dateToISO8601UTCDateTimeMSFString(d, true));
        
        d = df.parse("AD 1998-10-30 19:30:00:010 +0400");
        assertEquals(
                "15:30:00.01Z",
                dateToISO8601UTCTimeMSString(d, true));
        assertEquals(
                "15:30:00.010Z",
                dateToISO8601UTCTimeMSFString(d, true));
        assertEquals(
                "1998-10-30T15:30:00.01Z",
                dateToISO8601UTCDateTimeMSString(d, true));
        assertEquals(
                "1998-10-30T15:30:00.010Z",
                dateToISO8601UTCDateTimeMSFString(d, true));
        
        d = df.parse("AD 1998-10-30 19:30:00:001 +0400");
        assertEquals(
                "15:30:00.001Z",
                dateToISO8601UTCTimeMSString(d, true));
        assertEquals(
                "15:30:00.001Z",
                dateToISO8601UTCTimeMSFString(d, true));
        assertEquals(
                "1998-10-30T15:30:00.001Z",
                dateToISO8601UTCDateTimeMSString(d, true));
        assertEquals(
                "1998-10-30T15:30:00.001Z",
                dateToISO8601UTCDateTimeMSFString(d, true));
    }

    public void testXSFormatISODeviations() throws ParseException, UnrecognizedTimeZoneException {
        Date dsum = df.parse("AD 2010-05-09 20:00:00:0 UTC");
        Date dwin = df.parse("AD 2010-01-01 20:00:00:0 UTC");
        
        TimeZone tzRome = DateUtil.getTimeZone("Europe/Rome");
        
        assertEquals(
                "2010-01-01T21:00:00+01:00",
                DateUtil.dateToXSString(dwin, true, true, true, DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
        assertEquals(
                "2010-05-09T22:00:00+02:00",
                DateUtil.dateToXSString(dsum, true, true, true, DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
        assertEquals(
                "2010-01-01+01:00",  // ISO doesn't allow date-only with TZ
                DateUtil.dateToXSString(dwin, true, false, true, DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
        assertEquals(
                "2010-05-09+02:00",  // ISO doesn't allow date-only with TZ
                DateUtil.dateToXSString(dsum, true, false, true, DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
        assertEquals(
                "21:00:00+01:00",
                DateUtil.dateToXSString(dwin, false, true, true, DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
        assertEquals(
                "22:00:00+02:00",
                DateUtil.dateToXSString(dsum, false, true, true, DateUtil.ACCURACY_SECONDS, tzRome, calendarFactory));
        
        assertEquals(
                "-1-02-29T06:15:24Z",  // ISO uses 0 for BC 1
                DateUtil.dateToXSString(
                        df.parse("BC 0001-03-02 09:15:24:0 +0300"),
                        true, true, true, DateUtil.ACCURACY_SECONDS, DateUtil.UTC, calendarFactory));
        assertEquals(
                "-2-02-28T06:15:24Z",  // ISO uses -1 for BC 2
                DateUtil.dateToXSString(
                        df.parse("BC 2-03-02 09:15:24:0 +0300"),
                        true, true, true, DateUtil.ACCURACY_SECONDS, DateUtil.UTC, calendarFactory));
    }
    
    private String dateToISO8601DateTimeString(
            Date date, TimeZone tz) {
        return dateToISO8601String(date, true, true, true,
                DateUtil.ACCURACY_SECONDS, tz);
    }
    
    private String dateToISO8601UTCDateTimeString(
            Date date, boolean offsetPart) {
        return dateToISO8601String(date, true, true, offsetPart,
                DateUtil.ACCURACY_SECONDS, DateUtil.UTC);
    }

    private String dateToISO8601UTCDateTimeMSString(
            Date date, boolean offsetPart) {
        return dateToISO8601String(date, true, true, offsetPart,
                DateUtil.ACCURACY_MILLISECONDS, DateUtil.UTC);
    }

    private String dateToISO8601UTCDateTimeMSFString(
            Date date, boolean offsetPart) {
        return dateToISO8601String(date, true, true, offsetPart,
                DateUtil.ACCURACY_MILLISECONDS_FORCED, DateUtil.UTC);
    }
        
    private String dateToISO8601DateString(Date date, TimeZone tz) {
        return dateToISO8601String(date, true, false, false,
                DateUtil.ACCURACY_SECONDS, tz);
    }

    private String dateToISO8601UTCDateString(Date date) {
        return dateToISO8601String(date, true, false, false,
                DateUtil.ACCURACY_SECONDS, DateUtil.UTC);
    }
    
    private String dateToISO8601TimeString(
            Date date, TimeZone tz) {
        return dateToISO8601String(date, false, true, true,
                DateUtil.ACCURACY_SECONDS, tz);
    }
    
    private String dateToISO8601UTCTimeString(
            Date date, boolean offsetPart) {
        return dateToISO8601String(date, false, true, offsetPart,
                DateUtil.ACCURACY_SECONDS, DateUtil.UTC);
    }

    private String dateToISO8601UTCTimeMSString(
            Date date, boolean offsetPart) {
        return dateToISO8601String(date, false, true, offsetPart,
                DateUtil.ACCURACY_MILLISECONDS, DateUtil.UTC);
    }

    private String dateToISO8601UTCTimeMSFString(
            Date date, boolean offsetPart) {
        return dateToISO8601String(date, false, true, offsetPart,
                DateUtil.ACCURACY_MILLISECONDS_FORCED, DateUtil.UTC);
    }
    
    private String dateToISO8601String(
            Date date,
            boolean datePart, boolean timePart, boolean offsetPart,
            int accuracy,
            TimeZone timeZone) {
        return DateUtil.dateToISO8601String(
                date,
                datePart, timePart, offsetPart,
                accuracy,
                timeZone,
                calendarFactory);        
    }
    
    public void testParseDate() throws DateParseException {
        assertDateParsing(
                "AD 1998-10-29 20:00:00:0 +0000",
                null,
                "1998-10-30+04:00", DateUtil.UTC);
        assertDateParsing(
                "AD 1998-10-30 02:00:00:0 +0000",
                null,
                "1998-10-30-02:00", DateUtil.UTC);
        assertDateParsing(
                "AD 1998-10-30 02:00:00:0 +0000",
                "1998-10-30", DateUtil.parseXSTimeZone("-02:00"));
        assertDateParsing(
                null,
                "AD 1998-10-30 02:00:00:0 +0000",
                "19981030", DateUtil.parseXSTimeZone("-02:00"));
        assertDateParsing(
                "AD 1998-10-30 00:00:00:0 +0000",
                null,
                "1998-10-30Z", DateUtil.UTC);
        assertDateParsing(
                "AD 1998-10-30 00:00:00:0 +0000",
                "1998-10-30", DateUtil.UTC);
        assertDateParsing(
                null,
                "AD 1998-10-30 00:00:00:0 +0000",
                "19981030", DateUtil.UTC);

        assertDateParsing(
                "AD 1998-10-29 20:00:00:0 +0000",
                null,
                "1998-10-30+04:00", DateUtil.UTC);
        assertDateParsing(
                "AD 1998-10-30 04:00:00:0 +0000",
                null,
                "1998-10-30-04:00", DateUtil.UTC);
        assertDateParsing(
                "AD 1998-10-30 00:00:00:0 +0000",
                null,
                "1998-10-30Z", DateUtil.UTC);
        
        try {
            // XS doesn't have year 0
            assertDateParsing(
                    "BC 0000-02-05 00:00:00:0 +0000",
                    null,
                    "0000-02-03Z", DateUtil.UTC);
            fail();
        } catch (DateParseException e) {
            echo(e);
        }
        assertDateParsing(
                null,
                "BC 0001-02-05 00:00:00:0 +0000",
                "0000-02-03", DateUtil.UTC);
        assertDateParsing(
                null,
                "BC 0001-02-05 00:00:00:0 +0000",
                "00000203", DateUtil.UTC);
        
        assertDateParsing(
                "BC 0001-02-05 00:00:00:0 +0000",  // Julian
                "BC 0002-02-05 00:00:00:0 +0000",  // Julian
                "-0001-02-03", DateUtil.UTC);  // Proleptic Gregorian
        assertDateParsing(
                null,
                "BC 0002-02-05 00:00:00:0 +0000",  // Julian
                "-00010203", DateUtil.UTC);  // Proleptic Gregorian

        assertDateParsing(
                "AD 0001-02-05 00:00:00:0 +0000",  // Julian
                null,
                "0001-02-03Z", DateUtil.UTC);  // Proleptic Gregorian
        assertDateParsing(
                "AD 0001-02-05 00:00:00:0 +0000",  // Julian
                "0001-02-03", DateUtil.UTC);  // Proleptic Gregorian
        assertDateParsing(
                null,
                "AD 0001-02-05 00:00:00:0 +0000",  // Julian
                "00010203", DateUtil.UTC);  // Proleptic Gregorian
        assertDateParsing(
                "AD 1001-12-07 00:00:00:0 +0000",  // Julian
                null,
                "1001-12-13Z", DateUtil.UTC);  // Proleptic Gregorian
        assertDateParsing(
                "AD 1001-12-07 00:00:00:0 +0000",  // Julian
                "1001-12-13", DateUtil.UTC);  // Proleptic Gregorian
        
        assertDateParsing(
                "AD 2006-12-31 00:00:00:0 +0000",
                null,
                "2006-12-31Z", DateUtil.UTC);
        assertDateParsing(
                "AD 2006-12-31 00:00:00:0 +0000",
                "2006-12-31", DateUtil.UTC);
        assertDateParsing(
                "AD 2006-01-01 00:00:00:0 +0000",
                null,
                "2006-01-01Z", DateUtil.UTC);
        assertDateParsing(
                "AD 2006-01-01 00:00:00:0 +0000",
                "2006-01-01", DateUtil.UTC);
        assertDateParsing(
                "AD 12006-01-01 00:00:00:0 +0000",
                "12006-01-01", DateUtil.UTC);
        assertDateParsing(
                null,
                "AD 12006-01-01 00:00:00:0 +0000",
                "120060101", DateUtil.UTC);
    }

    public void testParseDateMalformed() {
        assertDateMalformed("1998-10-30x");
        assertDateMalformed("+1998-10-30");
        assertDateMalformed("1998-10-");
        assertDateMalformed("1998-1-30");
        assertDateMalformed("1998-10-30+01");
        assertDateMalformed("1998-00-01");
        assertDateMalformed("1998-13-01");
        assertDateMalformed("1998-10-00");
        assertDateMalformed("1998-10-32");
        assertDateMalformed("1998-02-31");
        
        assertISO8601DateMalformed("2100103");
        assertISO8601DateMalformed("210-01-03");
        assertISO8601DateMalformed("2012-0301");
        assertISO8601DateMalformed("201203-01");
        assertISO8601DateMalformed("2012-01-01+01:00");
    }
    
    public void testParseTime() throws DateParseException {
        assertTimeParsing(
                "AD 1970-01-01 17:30:05:0 +0000",
                "17:30:05", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 17:30:05:0 +0000",
                "173005", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-01 07:30:00:100 +0000",
                "07:30:00.1", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-01 07:30:00:120 +0000",
                "07:30:00.12", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-01 07:30:00:123 +0000",
                "07:30:00.123", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-01 07:30:00:123 +0000",
                "07:30:00.1235", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-01 07:30:00:123 +0000",
                "07:30:00.12346", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 07:30:00:123 +0000",
                "073000.12346", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 07:30:00:123 +0000",
                "073000,12346", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-01 07:30:00:120 +0000",
                "07:30:00.12", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-01 07:30:00:500 +0000",
                "07:30:00.5", DateUtil.UTC);

        assertTimeParsing(
                "AD 1970-01-01 16:30:05:0 +0000",
                "17:30:05+01:00", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 16:30:05:0 +0000",
                "173005+01", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-01 19:00:05:0 +0000",
                "17:30:05-01:30", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 19:00:05:0 +0000",
                "173005-0130", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-01 16:30:05:500 +0000",
                "17:30:05.5+01:00", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 16:30:05:500 +0000",
                "173005.5+0100", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 16:30:05:500 +0000",
                "173005.5+01", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 16:00:00:0 +0000",
                "170000+01", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 16:00:00:0 +0000",
                "1700+01", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-01 16:00:00:0 +0000",
                "17+01", DateUtil.UTC);
        
        assertTimeParsing(
                "AD 1970-01-01 00:00:00:0 +0000",
                "00:00:00", DateUtil.UTC);
        assertTimeParsing(
                "AD 1970-01-02 00:00:00:0 +0000",
                "24:00:00", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-02 00:00:00:0 +0000",
                "240000", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-02 00:00:00:0 +0000",
                "2400", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-02 00:00:00:0 +0000",
                "24:00", DateUtil.UTC);
        assertTimeParsing(
                null,
                "AD 1970-01-02 00:00:00:0 +0000",
                "24", DateUtil.UTC);
        
        assertTimeParsing(
                "AD 1970-01-01 23:59:59:999 +0000",
                "23:59:59.999", DateUtil.UTC);
    }

    public void testParseTimeMalformed() {
        assertTimeMalformed("00:0000");
        assertTimeMalformed("00:00:00-01:60");
        assertTimeMalformed("24:00:01");
        assertTimeMalformed("00:00:61");
        assertTimeMalformed("00:60:00");
        assertTimeMalformed("25:00:00");
        assertTimeMalformed("2:00:00");
        assertTimeMalformed("02:0:00");
        assertTimeMalformed("02:00:0");
        
        assertISO8601TimeMalformed("1010101");
        assertISO8601TimeMalformed("10101");
        assertISO8601TimeMalformed("101");
        assertISO8601TimeMalformed("1");
        assertISO8601TimeMalformed("101010-1");
        assertISO8601TimeMalformed("101010-100");
        assertISO8601TimeMalformed("101010-10000");
        assertISO8601TimeMalformed("101010+1");
        assertISO8601TimeMalformed("101010+100");
        assertISO8601TimeMalformed("101010+10000");
    }
    
    public void testParseDateTime() throws DateParseException {
        assertDateTimeParsing( 
                "AD 1998-10-30 11:30:00:0 +0000",
                "1998-10-30T15:30:00+04:00", DateUtil.UTC);
        assertDateTimeParsing(
                null,
                "AD 1998-10-30 11:30:00:0 +0000",
                "19981030T153000+0400", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1998-10-30 11:30:00:500 +0000",
                "1998-10-30T15:30:00.5+04:00", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1998-10-30 15:30:00:0 +0000",
                "1998-10-30T15:30:00Z", DateUtil.UTC);
        assertDateTimeParsing(
                null,
                "AD 1998-10-30 15:30:00:0 +0000",
                "19981030T1530Z", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1998-10-30 15:30:00:500 +0000",
                "1998-10-30T15:30:00.5Z", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1998-10-30 11:30:00:0 +0000",
                "1998-10-30T15:30:00+04:00", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1998-10-30 15:30:00:0 +0000",
                "1998-10-30T15:30:00Z", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1998-10-30 15:30:00:0 +0000",
                "1998-10-30T15:30:00", DateUtil.UTC);
        assertDateTimeParsing(
                null,
                "AD 1998-10-30 15:30:00:0 +0000",
                "1998-10-30T15:30", DateUtil.UTC);
        
        assertDateTimeParsing(
                "AD 1998-10-29 20:00:00:0 +0000",
                "1998-10-30T00:00:00+04:00", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1998-10-30 02:00:00:0 +0000",
                "1998-10-30T00:00:00-02:00", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1998-10-30 00:00:00:0 +0000",
                "1998-10-30T00:00:00Z", DateUtil.UTC);

        assertDateTimeParsing(
                "AD 1998-10-29 20:00:00:0 +0000",
                "1998-10-30T00:00:00+04:00", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1998-10-30 00:00:00:0 +0000",
                "1998-10-30T00:00:00Z", DateUtil.UTC);
        assertDateTimeParsing(
                null,
                "AD 1998-10-30 00:00:00:0 +0000",
                "1998-10-30T00:00Z", DateUtil.UTC);
        assertDateTimeParsing(
                null,
                "AD 1998-10-30 00:00:00:0 +0000",
                "1998-10-30T00:00", DateUtil.UTC);
        assertDateTimeParsing(
                null,
                "AD 1998-10-30 00:00:00:0 +0000",
                "19981030T00Z", DateUtil.UTC);
        
        // BC years
        try {
            assertDateTimeParsing(
                        "",
                        null,
                        "0000-02-03T00:00:00Z", DateUtil.UTC);
            fail();
        } catch (DateParseException e) {
            echo(e);
        }
        assertDateTimeParsing(
                null,
                "BC 0001-02-05 00:00:00:0 +0000",
                "0000-02-03T00:00:00Z", DateUtil.UTC);
        
        assertDateTimeParsing(
                "BC 0001-02-05 00:00:00:0 +0000",  // Julian
                "BC 0002-02-05 00:00:00:0 +0000",  // Julian
                "-0001-02-03T00:00:00Z", DateUtil.UTC);  // Proleptic Gregorian

        assertDateTimeParsing(
                "AD 0001-02-05 00:00:00:0 +0000",  // Julian
                "0001-02-03T00:00:00Z", DateUtil.UTC);  // Proleptic Gregorian
        assertDateTimeParsing(
                "AD 1001-12-07 00:00:00:0 +0000",  // Julian
                "1001-12-13T00:00:00Z", DateUtil.UTC);  // Proleptic Gregorian
        assertDateTimeParsing(
                "AD 11001-12-13 00:00:00:0 +0000",
                "11001-12-13T00:00:00Z", DateUtil.UTC);
        assertDateTimeParsing(
                null,
                "AD 11001-12-13 00:00:00:0 +0000",
                "110011213T00Z", DateUtil.UTC);
        
        assertDateTimeParsing(
                "AD 2006-12-31 00:00:00:0 +0000",
                "2006-12-31T00:00:00Z", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 2006-01-01 00:00:00:0 +0000",
                "2006-01-01T00:00:00Z", DateUtil.UTC);
        
        assertDateTimeParsing(
                "AD 1970-01-01 07:30:00:123 +0000",
                "1970-01-01T07:30:00.123", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1970-01-01 07:30:00:123 +0000",
                "1970-01-01T07:30:00.1235", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1970-01-01 07:30:00:123 +0000",
                "1970-01-01T07:30:00.12346", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1970-01-01 07:30:00:120 +0000",
                "1970-01-01T07:30:00.12", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1970-01-01 07:30:00:500 +0000",
                "1970-01-01T07:30:00.5", DateUtil.UTC);

        assertDateTimeParsing(
                "AD 1970-01-01 16:30:05:0 +0000",
                "1970-01-01T17:30:05+01:00", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1970-01-01 16:30:05:500 +0000",
                "1970-01-01T17:30:05.5+01:00", DateUtil.UTC);
        
        assertDateTimeParsing(
                "AD 1970-01-01 00:00:00:0 +0000",
                "1970-01-01T00:00:00", DateUtil.UTC);
        assertDateTimeParsing(
                "AD 1970-01-02 00:00:00:0 +0000",
                "1970-01-01T24:00:00", DateUtil.UTC);
        
        assertDateTimeParsing(
                "AD 1970-01-01 23:59:59:999 +0000",
                "1970-01-01T23:59:59.999", DateUtil.UTC);
    }

    public void testParseDateTimeMalformed() throws DateParseException {
        assertDateTimeMalformed("1998-00-01T00:00:00");
        assertDateTimeMalformed("1998-13-01T00:00:00");
        assertDateTimeMalformed("1998-10-00T00:00:00");
        assertDateTimeMalformed("1998-10-32T00:00:00");
        assertDateTimeMalformed("1998-02-31T00:00:00");
        assertDateTimeMalformed("1970-01-02T24:00:01");
        assertDateTimeMalformed("1970-01-01T00:00:61");
        assertDateTimeMalformed("1970-01-01T00:60:00");
        assertDateTimeMalformed("1970-01-01T25:00:00");
        
        assertISO8601DateTimeMalformed("197-01-01T20:00:00");
    }
    
    public void testParseXSTimeZone() throws DateParseException {
        assertEquals(0,
                DateUtil.parseXSTimeZone("Z").getOffset(0));
        assertEquals(0,
                DateUtil.parseXSTimeZone("-00:00").getOffset(0));
        assertEquals(0,
                DateUtil.parseXSTimeZone("+00:00").getOffset(0));
        assertEquals(90 * 60 * 1000,
                DateUtil.parseXSTimeZone("+01:30").getOffset(0));
        assertEquals(-4 * 60 * 60 * 1000,
                DateUtil.parseXSTimeZone("-04:00").getOffset(0));
        assertEquals(((-23 * 60) - 59) * 60 * 1000,
                DateUtil.parseXSTimeZone("-23:59").getOffset(0));
        assertEquals(((23 * 60) + 59) * 60 * 1000,
                DateUtil.parseXSTimeZone("+23:59").getOffset(0));
    }

    public void testParseXSTimeZoneWrong() {
        try {
            DateUtil.parseXSTimeZone("04:00").getOffset(0);
            fail();
        } catch (DateParseException e) {
            echo(e);
        }
        try {
            DateUtil.parseXSTimeZone("-04:00x").getOffset(0);
            fail();
        } catch (DateParseException e) {
            echo(e);
        }
        try {
            DateUtil.parseXSTimeZone("-04").getOffset(0);
            fail();
        } catch (DateParseException e) {
            echo(e);
        }
        try {
            DateUtil.parseXSTimeZone("+24:00").getOffset(0);
            fail();
        } catch (DateParseException e) {
            echo(e);
        }
        try {
            DateUtil.parseXSTimeZone("-24:00").getOffset(0);
            fail();
        } catch (DateParseException e) {
            echo(e);
        }
        try {
            DateUtil.parseXSTimeZone("-01:60").getOffset(0);
            fail();
        } catch (DateParseException e) {
            echo(e);
        }
    }
    
    public void testParseXSDateTimeFTLAndJavax() throws DateParseException {
        // Explicit time zone:
        assertJavaxAndFTLXSDateTimesSame("2014-01-01T13:35:08Z");
        assertJavaxAndFTLXSDateTimesSame("2014-01-01T13:35:08+02:00");
        
        // Default time zone:
        assertJavaxAndFTLXSDateTimesSame("2014-01-01T13:35:08"); // winter
        assertJavaxAndFTLXSDateTimesSame("2014-07-01T13:35:08"); // summer
        
        // Proleptic Gregorian
        assertJavaxAndFTLXSDateTimesSame("1500-01-01T13:35:08Z");
        assertJavaxAndFTLXSDateTimesSame("0200-01-01T13:35:08Z");
        assertJavaxAndFTLXSDateTimesSame("0001-01-01T00:00:00+05:00");
        
        // BC
        assertJavaxAndFTLXSDateTimesSame("0001-01-01T13:35:08Z");
        assertJavaxAndFTLXSDateTimesSame("-0001-01-01T13:35:08Z");
        
        // Hour 24
        assertJavaxAndFTLXSDateTimesSame("2014-01-01T23:59:59");
        if (isAtLeastJava6()) { // Java 5 has broken parser that doesn't allow 24.
            assertJavaxAndFTLXSDateTimesSame("2014-01-31T24:00:00");
            assertJavaxAndFTLXSDateTimesSame("2014-01-01T24:00:00");
        }
        assertJavaxAndFTLXSDateTimesSame("2014-01-02T00:00:00");  // same as the previous
        assertJavaxAndFTLXSDateTimesSame("2014-02-01T00:00:00");  // same as the previous
        
        // Under ms
        assertJavaxAndFTLXSDateTimesSame("2014-01-01T23:59:59.123456789");
        assertJavaxAndFTLXSDateTimesSame("2014-01-01T23:59:59.1235");
    }
    
    private boolean isAtLeastJava6() {
        try {
            Class.forName("java.lang.management.LockInfo");
        } catch (ClassNotFoundException e) {
            return false;
        }
        return true;
    }

    private final DatatypeFactory datetypeFactory;
    {
        try {
            datetypeFactory = DatatypeFactory.newInstance();
        } catch (DatatypeConfigurationException e) {
            throw new RuntimeException(e);
        } 
    }
    
    private void assertJavaxAndFTLXSDateTimesSame(String s) throws DateParseException {
        XMLGregorianCalendar xgc = datetypeFactory.newXMLGregorianCalendar(s);
        Date javaxDate = xgc.toGregorianCalendar().getTime();
        Date ftlDate = DateUtil.parseXSDateTime(s, TimeZone.getDefault(), cf2dc);
        assertEquals(javaxDate, ftlDate);
    }

    private void assertDateParsing(String expected, String parsed, TimeZone tz) throws DateParseException {
        assertDateParsing(expected, expected, parsed, tz);
    }

    private void assertDateParsing(String expectedXS, String expectedISO8601, String parsed, TimeZone tz)
            throws DateParseException {
        if (expectedXS != null) {
            assertEquals(
                    expectedXS,
                    df.format(DateUtil.parseXSDate(parsed, tz, cf2dc)));
        }
        if (expectedISO8601 != null) {
            assertEquals(
                    expectedISO8601,
                    df.format(DateUtil.parseISO8601Date(parsed, tz, cf2dc)));
        }
    }

    private void assertDateTimeParsing(String expected, String parsed, TimeZone tz) throws DateParseException {
        assertDateTimeParsing(expected, expected, parsed, tz);
    }

    private void assertDateTimeParsing(String expectedXS, String expectedISO8601, String parsed, TimeZone tz)
            throws DateParseException {
        if (expectedXS != null) {
            assertEquals(
                    expectedXS,
                    df.format(DateUtil.parseXSDateTime(parsed, tz, cf2dc)));
        }
        if (expectedISO8601 != null) {
            assertEquals(
                    expectedISO8601,
                    df.format(DateUtil.parseISO8601DateTime(parsed, tz, cf2dc)));
        }
    }

    private void assertTimeParsing(String expected, String parsed, TimeZone tz) throws DateParseException {
        assertTimeParsing(expected, expected, parsed, tz);
    }

    private void assertTimeParsing(String expectedXS, String expectedISO8601, String parsed, TimeZone tz)
            throws DateParseException {
        if (expectedXS != null) {
            assertEquals(
                    expectedXS,
                    df.format(DateUtil.parseXSTime(parsed, tz, cf2dc)));
        }
        if (expectedISO8601 != null) {
            assertEquals(
                    expectedISO8601,
                    df.format(DateUtil.parseISO8601Time(parsed, tz, cf2dc)));
        }
    }
    
    private void assertDateMalformed(String parsed) {
        try {
            DateUtil.parseXSDate(parsed, DateUtil.UTC, cf2dc);
            fail();
        } catch (DateParseException e) {
            // Expected
            echo(e);
        }
        try {
            DateUtil.parseISO8601Date(parsed, DateUtil.UTC, cf2dc);
            fail();
        } catch (DateParseException e) {
            // Expected
            echo(e);
        }
    }

    private void assertTimeMalformed(String parsed) {
        try {
            DateUtil.parseXSTime(parsed, DateUtil.UTC, cf2dc);
            fail();
        } catch (DateParseException e) {
            // Expected
            echo(e);
        }
        try {
            DateUtil.parseISO8601Time(parsed, DateUtil.UTC, cf2dc);
            fail();
        } catch (DateParseException e) {
            // Expected
            echo(e);
        }
    }

    private void assertDateTimeMalformed(String parsed) {
        try {
            DateUtil.parseXSDateTime(parsed, DateUtil.UTC, cf2dc);
            fail();
        } catch (DateParseException e) {
            // Expected
            echo(e);
        }
        try {
            DateUtil.parseISO8601DateTime(parsed, DateUtil.UTC, cf2dc);
            fail();
        } catch (DateParseException e) {
            // Expected
            echo(e);
        }
    }

    private void assertISO8601DateMalformed(String parsed) {
        try {
            DateUtil.parseISO8601Date(parsed, DateUtil.UTC, cf2dc);
            fail();
        } catch (DateParseException e) {
            // Expected
            echo(e);
        }
    }
    
    private void assertISO8601TimeMalformed(String parsed) {
        try {
            DateUtil.parseISO8601Time(parsed, DateUtil.UTC, cf2dc);
            fail();
        } catch (DateParseException e) {
            // Expected
            echo(e);
        }
    }
    
    private void assertISO8601DateTimeMalformed(String parsed) {
        try {
            DateUtil.parseISO8601DateTime(parsed, DateUtil.UTC, cf2dc);
            fail();
        } catch (DateParseException e) {
            // Expected
            echo(e);
        }
    }
    
    private void echo(@SuppressWarnings("unused") DateParseException e) {
        // System.out.println(e);
    }

}