/*
 * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * This file is available under and governed by the GNU General Public
 * License version 2 only, as published by the Free Software Foundation.
 * However, the following notice accompanied the original version of this
 * file:
 *
 * Copyright (c) 2008-2012, Stephen Colebourne & Michael Nascimento Santos
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  * Neither the name of JSR-310 nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package tck.java.time.format;

import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.DAY_OF_WEEK;
import static java.time.temporal.ChronoField.DAY_OF_YEAR;
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.YEAR;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.time.DateTimeException;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.chrono.ChronoZonedDateTime;
import java.time.chrono.Chronology;
import java.time.chrono.IsoChronology;
/* J2ObjC removed: Only "gregorian" and "julian" calendars are supported.
import java.time.chrono.ThaiBuddhistChronology;
import java.time.chrono.ThaiBuddhistDate; */
import java.time.format.DecimalStyle;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.SignStyle;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.time.temporal.TemporalQuery;
import java.time.temporal.UnsupportedTemporalTypeException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

import org.junit.Before;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import org.junit.runner.RunWith;
import org.junit.Test;

/**
 * Test DateTimeFormatter.
 */
@RunWith(DataProviderRunner.class)
public class TCKDateTimeFormatter {

    private static final ZoneOffset OFFSET_PONE = ZoneOffset.ofHours(1);
    private static final ZoneOffset OFFSET_PTHREE = ZoneOffset.ofHours(3);
    private static final ZoneId ZONE_PARIS = ZoneId.of("Europe/Paris");

    private static final DateTimeFormatter BASIC_FORMATTER = DateTimeFormatter.ofPattern("'ONE'd");
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("'ONE'yyyy MM dd");

    private DateTimeFormatter fmt;

    @Before
    public void setUp() {
        fmt = new DateTimeFormatterBuilder().appendLiteral("ONE")
                                            .appendValue(DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE)
                                            .toFormatter();
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_withLocale() {
        DateTimeFormatter base = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        DateTimeFormatter test = base.withLocale(Locale.GERMAN);
        assertEquals(test.getLocale(), Locale.GERMAN);
    }

    @Test(expected=NullPointerException.class)
    public void test_withLocale_null() {
        DateTimeFormatter base = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        base.withLocale((Locale) null);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_withChronology() {
        DateTimeFormatter test = fmt;
        assertEquals(test.getChronology(), null);
        test = test.withChronology(IsoChronology.INSTANCE);
        assertEquals(test.getChronology(), IsoChronology.INSTANCE);
        test = test.withChronology(null);
        assertEquals(test.getChronology(), null);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_withZone() {
        DateTimeFormatter test = fmt;
        assertEquals(test.getZone(), null);
        test = test.withZone(ZoneId.of("Europe/Paris"));
        assertEquals(test.getZone(), ZoneId.of("Europe/Paris"));
        test = test.withZone(ZoneOffset.UTC);
        assertEquals(test.getZone(), ZoneOffset.UTC);
        test = test.withZone(null);
        assertEquals(test.getZone(), null);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_resolverFields_selectOneDateResolveYMD() throws Exception {
        DateTimeFormatter base = new DateTimeFormatterBuilder()
                .appendValue(YEAR).appendLiteral('-').appendValue(MONTH_OF_YEAR).appendLiteral('-')
                .appendValue(DAY_OF_MONTH).appendLiteral('-').appendValue(DAY_OF_YEAR).toFormatter();
        DateTimeFormatter f = base.withResolverFields(YEAR, MONTH_OF_YEAR, DAY_OF_MONTH);
        try {
            base.parse("2012-6-30-321", LocalDate::from);  // wrong day-of-year
            fail();
        } catch (DateTimeException ex) {
            // expected, fails as it produces two different dates
        }
        LocalDate parsed = f.parse("2012-6-30-321", LocalDate::from);  // ignored day-of-year
        assertEquals(parsed, LocalDate.of(2012, 6, 30));
    }

    @Test
    public void test_resolverFields_selectOneDateResolveYD() throws Exception {
        DateTimeFormatter base = new DateTimeFormatterBuilder()
                .appendValue(YEAR).appendLiteral('-').appendValue(MONTH_OF_YEAR).appendLiteral('-')
                .appendValue(DAY_OF_MONTH).appendLiteral('-').appendValue(DAY_OF_YEAR).toFormatter();
        DateTimeFormatter f = base.withResolverFields(YEAR, DAY_OF_YEAR);
        Set<TemporalField> expected = new HashSet<>(Arrays.asList(YEAR, DAY_OF_YEAR));
        // Use set.equals();  testNG comparison of Collections is ordered
        assertTrue("ResolveFields: " + f.getResolverFields(), f.getResolverFields().equals(expected));
        try {
            base.parse("2012-6-30-321", LocalDate::from);  // wrong month/day-of-month
            fail();
        } catch (DateTimeException ex) {
            // expected, fails as it produces two different dates
        }
        LocalDate parsed = f.parse("2012-6-30-321", LocalDate::from);  // ignored month/day-of-month
        assertEquals(parsed, LocalDate.of(2012, 11, 16));
    }

    @Test
    public void test_resolverFields_ignoreCrossCheck() throws Exception {
        DateTimeFormatter base = new DateTimeFormatterBuilder()
                .appendValue(YEAR).appendLiteral('-').appendValue(DAY_OF_YEAR).appendLiteral('-')
                .appendValue(DAY_OF_WEEK).toFormatter();
        DateTimeFormatter f = base.withResolverFields(YEAR, DAY_OF_YEAR);
        try {
            base.parse("2012-321-1", LocalDate::from);  // wrong day-of-week
            fail();
        } catch (DateTimeException ex) {
            // expected, should fail in cross-check of day-of-week
        }
        LocalDate parsed = f.parse("2012-321-1", LocalDate::from);  // ignored wrong day-of-week
        assertEquals(parsed, LocalDate.of(2012, 11, 16));
    }

    @Test
    public void test_resolverFields_emptyList() throws Exception {
        DateTimeFormatter f = new DateTimeFormatterBuilder()
                .appendValue(YEAR).toFormatter().withResolverFields();
        TemporalAccessor parsed = f.parse("2012");
        assertEquals(parsed.isSupported(YEAR), false);  // not in the list of resolverFields
    }

    @Test
    public void test_resolverFields_listOfOneMatching() throws Exception {
        DateTimeFormatter f = new DateTimeFormatterBuilder()
                .appendValue(YEAR).toFormatter().withResolverFields(YEAR);
        TemporalAccessor parsed = f.parse("2012");
        assertEquals(parsed.isSupported(YEAR), true);
    }

    @Test
    public void test_resolverFields_listOfOneNotMatching() throws Exception {
        DateTimeFormatter f = new DateTimeFormatterBuilder()
                .appendValue(YEAR).toFormatter().withResolverFields(MONTH_OF_YEAR);
        TemporalAccessor parsed = f.parse("2012");
        assertEquals(parsed.isSupported(YEAR), false);  // not in the list of resolverFields
        assertEquals(parsed.isSupported(MONTH_OF_YEAR), false);
    }

    @Test
    public void test_resolverFields_listOfOneNull() throws Exception {
        DateTimeFormatter f = new DateTimeFormatterBuilder()
                .appendValue(YEAR).toFormatter().withResolverFields((TemporalField) null);
        TemporalAccessor parsed = f.parse("2012");
        assertEquals(parsed.isSupported(YEAR), false);  // not in the list of resolverFields
    }

    @Test
    public void test_resolverFields_Array_null() throws Exception {
        DateTimeFormatter f = DateTimeFormatter.ISO_DATE.withResolverFields(MONTH_OF_YEAR);
        assertEquals(f.getResolverFields().size(), 1);
        f = f.withResolverFields((TemporalField[]) null);
        assertEquals(f.getResolverFields(), null);
    }

    @Test
    public void test_resolverFields_Set_null() throws Exception {
        DateTimeFormatter f = DateTimeFormatter.ISO_DATE.withResolverFields(MONTH_OF_YEAR);
        assertEquals(f.getResolverFields().size(), 1);
        f = f.withResolverFields((Set<TemporalField>) null);
        assertEquals(f.getResolverFields(), null);
    }

    //-----------------------------------------------------------------------
    // format
    //-----------------------------------------------------------------------
    @DataProvider
    public static Object[][] data_format_withZone_withChronology() {
        YearMonth ym = YearMonth.of(2008, 6);
        LocalDate ld = LocalDate.of(2008, 6, 30);
        LocalTime lt = LocalTime.of(11, 30);
        LocalDateTime ldt = LocalDateTime.of(2008, 6, 30, 11, 30);
        OffsetTime ot = OffsetTime.of(LocalTime.of(11, 30), OFFSET_PONE);
        OffsetDateTime odt = OffsetDateTime.of(LocalDateTime.of(2008, 6, 30, 11, 30), OFFSET_PONE);
        ZonedDateTime zdt = ZonedDateTime.of(LocalDateTime.of(2008, 6, 30, 11, 30), ZONE_PARIS);
        /* J2ObjC removed: Only "gregorian" and "julian" calendars are supported.
        ChronoZonedDateTime<ThaiBuddhistDate> thaiZdt = ThaiBuddhistChronology.INSTANCE.zonedDateTime(zdt); */
        Instant instant = Instant.ofEpochSecond(3600);
        return new Object[][] {
                {null, null, DayOfWeek.MONDAY, "::::"},
                {null, null, ym, "2008::::ISO"},
                {null, null, ld, "2008::::ISO"},
                {null, null, lt, ":11:::"},
                {null, null, ldt, "2008:11:::ISO"},
                {null, null, ot, ":11:+01:00::"},
                {null, null, odt, "2008:11:+01:00::ISO"},
                {null, null, zdt, "2008:11:+02:00:Europe/Paris:ISO"},
                {null, null, instant, "::::"},

                {IsoChronology.INSTANCE, null, DayOfWeek.MONDAY, "::::ISO"},
                {IsoChronology.INSTANCE, null, ym, "2008::::ISO"},
                {IsoChronology.INSTANCE, null, ld, "2008::::ISO"},
                {IsoChronology.INSTANCE, null, lt, ":11:::ISO"},
                {IsoChronology.INSTANCE, null, ldt, "2008:11:::ISO"},
                {IsoChronology.INSTANCE, null, ot, ":11:+01:00::ISO"},
                {IsoChronology.INSTANCE, null, odt, "2008:11:+01:00::ISO"},
                {IsoChronology.INSTANCE, null, zdt, "2008:11:+02:00:Europe/Paris:ISO"},
                {IsoChronology.INSTANCE, null, instant, "::::ISO"},

                {null, ZONE_PARIS, DayOfWeek.MONDAY, ":::Europe/Paris:"},
                {null, ZONE_PARIS, ym, "2008:::Europe/Paris:ISO"},
                {null, ZONE_PARIS, ld, "2008:::Europe/Paris:ISO"},
                {null, ZONE_PARIS, lt, ":11::Europe/Paris:"},
                {null, ZONE_PARIS, ldt, "2008:11::Europe/Paris:ISO"},
                {null, ZONE_PARIS, ot, ":11:+01:00:Europe/Paris:"},
                {null, ZONE_PARIS, odt, "2008:12:+02:00:Europe/Paris:ISO"},
                {null, ZONE_PARIS, zdt, "2008:11:+02:00:Europe/Paris:ISO"},
                {null, ZONE_PARIS, instant, "1970:02:+01:00:Europe/Paris:ISO"},

                {null, OFFSET_PTHREE, DayOfWeek.MONDAY, ":::+03:00:"},
                {null, OFFSET_PTHREE, ym, "2008:::+03:00:ISO"},
                {null, OFFSET_PTHREE, ld, "2008:::+03:00:ISO"},
                {null, OFFSET_PTHREE, lt, ":11::+03:00:"},
                {null, OFFSET_PTHREE, ldt, "2008:11::+03:00:ISO"},
                {null, OFFSET_PTHREE, ot, null},  // offset and zone clash
                {null, OFFSET_PTHREE, odt, "2008:13:+03:00:+03:00:ISO"},
                {null, OFFSET_PTHREE, zdt, "2008:12:+03:00:+03:00:ISO"},
                {null, OFFSET_PTHREE, instant, "1970:04:+03:00:+03:00:ISO"},

                /* J2ObjC removed: Only "gregorian" and "julian" calendars are supported.
                {ThaiBuddhistChronology.INSTANCE, null, DayOfWeek.MONDAY, null},  // not a complete date
                {ThaiBuddhistChronology.INSTANCE, null, ym, null},  // not a complete date
                {ThaiBuddhistChronology.INSTANCE, null, ld, "2551::::ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, null, lt, ":11:::ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, null, ldt, "2551:11:::ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, null, ot, ":11:+01:00::ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, null, odt, "2551:11:+01:00::ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, null, zdt, "2551:11:+02:00:Europe/Paris:ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, null, instant, "::::ThaiBuddhist"},

                {ThaiBuddhistChronology.INSTANCE, null, DayOfWeek.MONDAY, null},  // not a complete date
                {ThaiBuddhistChronology.INSTANCE, ZONE_PARIS, ym, null},  // not a complete date
                {ThaiBuddhistChronology.INSTANCE, ZONE_PARIS, ld, "2551:::Europe/Paris:ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, ZONE_PARIS, lt, ":11::Europe/Paris:ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, ZONE_PARIS, ldt, "2551:11::Europe/Paris:ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, ZONE_PARIS, ot, ":11:+01:00:Europe/Paris:ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, ZONE_PARIS, odt, "2551:12:+02:00:Europe/Paris:ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, ZONE_PARIS, zdt, "2551:11:+02:00:Europe/Paris:ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, ZONE_PARIS, instant, "2513:02:+01:00:Europe/Paris:ThaiBuddhist"},

                {null, ZONE_PARIS, thaiZdt, "2551:11:+02:00:Europe/Paris:ThaiBuddhist"},
                {ThaiBuddhistChronology.INSTANCE, ZONE_PARIS, thaiZdt, "2551:11:+02:00:Europe/Paris:ThaiBuddhist"},
                {IsoChronology.INSTANCE, ZONE_PARIS, thaiZdt, "2008:11:+02:00:Europe/Paris:ISO"},
                 */
        };
    }

    @Test
    @UseDataProvider("data_format_withZone_withChronology")
    public void test_format_withZone_withChronology(Chronology overrideChrono, ZoneId overrideZone, TemporalAccessor temporal, String expected) {
        DateTimeFormatter test = new DateTimeFormatterBuilder()
                .optionalStart().appendValue(YEAR, 4).optionalEnd()
                .appendLiteral(':').optionalStart().appendValue(HOUR_OF_DAY, 2).optionalEnd()
                .appendLiteral(':').optionalStart().appendOffsetId().optionalEnd()
                .appendLiteral(':').optionalStart().appendZoneId().optionalEnd()
                .appendLiteral(':').optionalStart().appendChronologyId().optionalEnd()
                .toFormatter(Locale.ENGLISH)
                .withChronology(overrideChrono).withZone(overrideZone);
        if (expected != null) {
            String result = test.format(temporal);
            assertEquals(result, expected);
        } else {
            try {
                test.format(temporal);
                fail("Formatting should have failed");
            } catch (DateTimeException ex) {
                // expected
            }
        }
    }

    @Test
    public void test_format_withChronology_nonChronoFieldMapLink() {
        TemporalAccessor temporal = new TemporalAccessor() {
            @Override
            public boolean isSupported(TemporalField field) {
                return field == IsoFields.WEEK_BASED_YEAR;
            }
            @Override
            public long getLong(TemporalField field) {
                if (field == IsoFields.WEEK_BASED_YEAR) {
                    return 2345;
                }
                throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
            }
        };
        DateTimeFormatter test = new DateTimeFormatterBuilder()
                .appendValue(IsoFields.WEEK_BASED_YEAR, 4)
                .toFormatter(Locale.ENGLISH)
                .withChronology(IsoChronology.INSTANCE);
        String result = test.format(temporal);
        assertEquals(result, "2345");
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_format_TemporalAccessor_simple() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        String result = test.format(LocalDate.of(2008, 6, 30));
        assertEquals(result, "ONE30");
    }

    @Test(expected = DateTimeException.class)
    public void test_format_TemporalAccessor_noSuchField() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.format(LocalTime.of(11, 30));
    }

    @Test(expected = NullPointerException.class)
    public void test_format_TemporalAccessor_null() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.format((TemporalAccessor) null);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_print_TemporalAppendable() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        StringBuilder buf = new StringBuilder();
        test.formatTo(LocalDate.of(2008, 6, 30), buf);
        assertEquals(buf.toString(), "ONE30");
    }

    @Test(expected=DateTimeException.class)
    public void test_print_TemporalAppendable_noSuchField() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        StringBuilder buf = new StringBuilder();
        test.formatTo(LocalTime.of(11, 30), buf);
    }

    @Test(expected=NullPointerException.class)
    public void test_print_TemporalAppendable_nullTemporal() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        StringBuilder buf = new StringBuilder();
        test.formatTo((TemporalAccessor) null, buf);
    }

    @Test(expected=NullPointerException.class)
    public void test_print_TemporalAppendable_nullAppendable() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.formatTo(LocalDate.of(2008, 6, 30), (Appendable) null);
    }

    //-----------------------------------------------------------------------
    // parse(CharSequence)
    //-----------------------------------------------------------------------
    @Test
    public void test_parse_CharSequence() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        TemporalAccessor result = test.parse("ONE30");
        assertEquals(result.isSupported(DAY_OF_MONTH), true);
        assertEquals(result.getLong(DAY_OF_MONTH), 30L);
        assertEquals(result.isSupported(HOUR_OF_DAY), false);
    }

    @Test
    public void test_parse_CharSequence_resolved() {
        DateTimeFormatter test = DateTimeFormatter.ISO_DATE;
        TemporalAccessor result = test.parse("2012-06-30");
        assertEquals(result.isSupported(YEAR), true);
        assertEquals(result.isSupported(MONTH_OF_YEAR), true);
        assertEquals(result.isSupported(DAY_OF_MONTH), true);
        assertEquals(result.isSupported(HOUR_OF_DAY), false);
        assertEquals(result.getLong(YEAR), 2012L);
        assertEquals(result.getLong(MONTH_OF_YEAR), 6L);
        assertEquals(result.getLong(DAY_OF_MONTH), 30L);
        assertEquals(result.query(LocalDate::from), LocalDate.of(2012, 6, 30));
    }

    @Test(expected=NullPointerException.class)
    public void test_parse_CharSequence_null() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.parse((String) null);
    }

    //-----------------------------------------------------------------------
    // parse(CharSequence)
    //-----------------------------------------------------------------------
    @Test
    public void test_parse_CharSequence_ParsePosition() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        ParsePosition pos = new ParsePosition(3);
        TemporalAccessor result = test.parse("XXXONE30XXX", pos);
        assertEquals(pos.getIndex(), 8);
        assertEquals(pos.getErrorIndex(), -1);
        assertEquals(result.isSupported(DAY_OF_MONTH), true);
        assertEquals(result.getLong(DAY_OF_MONTH), 30L);
        assertEquals(result.isSupported(HOUR_OF_DAY), false);
    }

    @Test
    public void test_parse_CharSequence_ParsePosition_resolved() {
        DateTimeFormatter test = DateTimeFormatter.ISO_DATE;
        ParsePosition pos = new ParsePosition(3);
        TemporalAccessor result = test.parse("XXX2012-06-30XXX", pos);
        assertEquals(pos.getIndex(), 13);
        assertEquals(pos.getErrorIndex(), -1);
        assertEquals(result.isSupported(YEAR), true);
        assertEquals(result.isSupported(MONTH_OF_YEAR), true);
        assertEquals(result.isSupported(DAY_OF_MONTH), true);
        assertEquals(result.isSupported(HOUR_OF_DAY), false);
        assertEquals(result.getLong(YEAR), 2012L);
        assertEquals(result.getLong(MONTH_OF_YEAR), 6L);
        assertEquals(result.getLong(DAY_OF_MONTH), 30L);
        assertEquals(result.query(LocalDate::from), LocalDate.of(2012, 6, 30));
    }

    @Test(expected=DateTimeParseException.class)
    public void test_parse_CharSequence_ParsePosition_parseError() {
        DateTimeFormatter test = DateTimeFormatter.ISO_DATE;
        ParsePosition pos = new ParsePosition(3);
        try {
            test.parse("XXX2012XXX", pos);
            fail();
        } catch (DateTimeParseException ex) {
            assertEquals(ex.getErrorIndex(), 7);
            throw ex;
        }
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void test_parse_CharSequence_ParsePosition_indexTooBig() {
        DateTimeFormatter test = DateTimeFormatter.ISO_DATE;
        test.parse("Text", new ParsePosition(5));
    }

    @Test(expected=NullPointerException.class)
    public void test_parse_CharSequence_ParsePosition_nullText() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.parse((CharSequence) null, new ParsePosition(0));
    }

    @Test(expected=NullPointerException.class)
    public void test_parse_CharSequence_ParsePosition_nullParsePosition() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.parse("Text", (ParsePosition) null);
    }

    //-----------------------------------------------------------------------
    // parse(Query)
    //-----------------------------------------------------------------------
    @Test
    public void test_parse_Query_String() throws Exception {
        LocalDate result = DATE_FORMATTER.parse("ONE2012 07 27", LocalDate::from);
        assertEquals(result, LocalDate.of(2012, 7, 27));
    }

    @Test
    public void test_parse_Query_CharSequence() throws Exception {
        LocalDate result = DATE_FORMATTER.parse(new StringBuilder("ONE2012 07 27"), LocalDate::from);
        assertEquals(result, LocalDate.of(2012, 7, 27));
    }

    @Test(expected=DateTimeParseException.class)
    public void test_parse_Query_String_parseError() throws Exception {
        try {
            DATE_FORMATTER.parse("ONE2012 07 XX", LocalDate::from);
        } catch (DateTimeParseException ex) {
            assertEquals(ex.getMessage().contains("could not be parsed"), true);
            assertEquals(ex.getMessage().contains("ONE2012 07 XX"), true);
            assertEquals(ex.getParsedString(), "ONE2012 07 XX");
            assertEquals(ex.getErrorIndex(), 11);
            throw ex;
        }
    }

    @Test(expected=DateTimeParseException.class)
    public void test_parse_Query_String_parseErrorLongText() throws Exception {
        try {
            DATE_FORMATTER.parse("ONEXXX67890123456789012345678901234567890123456789012345678901234567890123456789", LocalDate::from);
        } catch (DateTimeParseException ex) {
            assertEquals(ex.getMessage().contains("could not be parsed"), true);
            assertEquals(ex.getMessage().contains("ONEXXX6789012345678901234567890123456789012345678901234567890123..."), true);
            assertEquals(ex.getParsedString(), "ONEXXX67890123456789012345678901234567890123456789012345678901234567890123456789");
            assertEquals(ex.getErrorIndex(), 3);
            throw ex;
        }
    }

    @Test(expected=DateTimeParseException.class)
    public void test_parse_Query_String_parseIncomplete() throws Exception {
        try {
            DATE_FORMATTER.parse("ONE2012 07 27SomethingElse", LocalDate::from);
        } catch (DateTimeParseException ex) {
            assertEquals(ex.getMessage().contains("could not be parsed"), true);
            assertEquals(ex.getMessage().contains("ONE2012 07 27SomethingElse"), true);
            assertEquals(ex.getParsedString(), "ONE2012 07 27SomethingElse");
            assertEquals(ex.getErrorIndex(), 13);
            throw ex;
        }
    }

    @Test(expected=NullPointerException.class)
    public void test_parse_Query_String_nullText() throws Exception {
        DATE_FORMATTER.parse((String) null, LocalDate::from);
    }

    @Test(expected=NullPointerException.class)
    public void test_parse_Query_String_nullRule() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.parse("30", (TemporalQuery<?>) null);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_parseBest_firstOption() throws Exception {
        DateTimeFormatter test = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm[XXX]");
        TemporalAccessor result = test.parseBest("2011-06-30 12:30+03:00", ZonedDateTime::from, LocalDateTime::from);
        LocalDateTime ldt = LocalDateTime.of(2011, 6, 30, 12, 30);
        assertEquals(result, ZonedDateTime.of(ldt, ZoneOffset.ofHours(3)));
    }

    @Test
    public void test_parseBest_secondOption() throws Exception {
        DateTimeFormatter test = DateTimeFormatter.ofPattern("yyyy-MM-dd[ HH:mm[XXX]]");
        TemporalAccessor result = test.parseBest("2011-06-30", ZonedDateTime::from, LocalDate::from);
        assertEquals(result, LocalDate.of(2011, 6, 30));
    }

    @Test(expected=DateTimeParseException.class)
    public void test_parseBest_String_parseError() throws Exception {
        DateTimeFormatter test = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm[XXX]");
        try {
            test.parseBest("2011-06-XX", ZonedDateTime::from, LocalDateTime::from);
        } catch (DateTimeParseException ex) {
            assertEquals(ex.getMessage().contains("could not be parsed"), true);
            assertEquals(ex.getMessage().contains("XX"), true);
            assertEquals(ex.getParsedString(), "2011-06-XX");
            assertEquals(ex.getErrorIndex(), 8);
            throw ex;
        }
    }

    @Test(expected=DateTimeParseException.class)
    public void test_parseBest_String_parseErrorLongText() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        try {
            test.parseBest("ONEXXX67890123456789012345678901234567890123456789012345678901234567890123456789", ZonedDateTime::from, LocalDate::from);
        } catch (DateTimeParseException ex) {
            assertEquals(ex.getMessage().contains("could not be parsed"), true);
            assertEquals(ex.getMessage().contains("ONEXXX6789012345678901234567890123456789012345678901234567890123..."), true);
            assertEquals(ex.getParsedString(), "ONEXXX67890123456789012345678901234567890123456789012345678901234567890123456789");
            assertEquals(ex.getErrorIndex(), 3);
            throw ex;
        }
    }

    @Test(expected=DateTimeParseException.class)
    public void test_parseBest_String_parseIncomplete() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        try {
            test.parseBest("ONE30SomethingElse", ZonedDateTime::from, LocalDate::from);
        } catch (DateTimeParseException ex) {
            assertEquals(ex.getMessage().contains("could not be parsed"), true);
            assertEquals(ex.getMessage().contains("ONE30SomethingElse"), true);
            assertEquals(ex.getParsedString(), "ONE30SomethingElse");
            assertEquals(ex.getErrorIndex(), 5);
            throw ex;
        }
    }

    @Test(expected=NullPointerException.class)
    public void test_parseBest_String_nullText() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.parseBest((String) null, ZonedDateTime::from, LocalDate::from);
    }

    @Test(expected=NullPointerException.class)
    public void test_parseBest_String_nullRules() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.parseBest("30", (TemporalQuery<?>[]) null);
    }

    @Test(expected=IllegalArgumentException.class)
    public void test_parseBest_String_zeroRules() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.parseBest("30", new TemporalQuery<?>[0]);
    }

    @Test(expected=IllegalArgumentException.class)
    public void test_parseBest_String_oneRule() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.parseBest("30", LocalDate::from);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_parseUnresolved_StringParsePosition() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        ParsePosition pos = new ParsePosition(0);
        TemporalAccessor result = test.parseUnresolved("ONE30XXX", pos);
        assertEquals(pos.getIndex(), 5);
        assertEquals(pos.getErrorIndex(), -1);
        assertEquals(result.getLong(DAY_OF_MONTH), 30L);
    }

    @Test
    public void test_parseUnresolved_StringParsePosition_parseError() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        ParsePosition pos = new ParsePosition(0);
        TemporalAccessor result = test.parseUnresolved("ONEXXX", pos);
        assertEquals(pos.getIndex(), 0);
        assertEquals(pos.getErrorIndex(), 3);
        assertEquals(result, null);
    }

    @Test
    public void test_parseUnresolved_StringParsePosition_duplicateFieldSameValue() {
        DateTimeFormatter test = new DateTimeFormatterBuilder()
                .appendValue(MONTH_OF_YEAR).appendLiteral('-').appendValue(MONTH_OF_YEAR).toFormatter();
        ParsePosition pos = new ParsePosition(3);
        TemporalAccessor result = test.parseUnresolved("XXX6-6", pos);
        assertEquals(pos.getIndex(), 6);
        assertEquals(pos.getErrorIndex(), -1);
        assertEquals(result.getLong(MONTH_OF_YEAR), 6);
    }

    @Test
    public void test_parseUnresolved_StringParsePosition_duplicateFieldDifferentValue() {
        DateTimeFormatter test = new DateTimeFormatterBuilder()
                .appendValue(MONTH_OF_YEAR).appendLiteral('-').appendValue(MONTH_OF_YEAR).toFormatter();
        ParsePosition pos = new ParsePosition(3);
        TemporalAccessor result = test.parseUnresolved("XXX6-7", pos);
        assertEquals(pos.getIndex(), 3);
        assertEquals(pos.getErrorIndex(), 5);
        assertEquals(result, null);
    }

    @Test(expected=NullPointerException.class)
    public void test_parseUnresolved_StringParsePosition_nullString() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        ParsePosition pos = new ParsePosition(0);
        test.parseUnresolved((String) null, pos);
    }

    @Test(expected=NullPointerException.class)
    public void test_parseUnresolved_StringParsePosition_nullParsePosition() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        test.parseUnresolved("ONE30", (ParsePosition) null);
    }

    @Test(expected=IndexOutOfBoundsException.class)
    public void test_parseUnresolved_StringParsePosition_invalidPosition() {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        ParsePosition pos = new ParsePosition(6);
        test.parseUnresolved("ONE30", pos);
    }

    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    @Test
    public void test_toFormat_format() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        String result = format.format(LocalDate.of(2008, 6, 30));
        assertEquals(result, "ONE30");
    }

    @Test(expected=NullPointerException.class)
    public void test_toFormat_format_null() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        format.format(null);
    }

    @Test(expected=IllegalArgumentException.class)
    public void test_toFormat_format_notTemporal() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        format.format("Not a Temporal");
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_toFormat_parseObject_String() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        TemporalAccessor result = (TemporalAccessor) format.parseObject("ONE30");
        assertEquals(result.isSupported(DAY_OF_MONTH), true);
        assertEquals(result.getLong(DAY_OF_MONTH), 30L);
    }

    @Test(expected=ParseException.class)
    public void test_toFormat_parseObject_String_parseError() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        try {
            format.parseObject("ONEXXX");
        } catch (ParseException ex) {
            assertEquals(ex.getMessage().contains("ONEXXX"), true);
            assertEquals(ex.getErrorOffset(), 3);
            throw ex;
        }
    }

    @Test(expected=ParseException.class)
    public void test_toFormat_parseObject_String_parseErrorLongText() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        try {
            format.parseObject("ONEXXX67890123456789012345678901234567890123456789012345678901234567890123456789");
        } catch (DateTimeParseException ex) {
            assertEquals(ex.getMessage().contains("ONEXXX6789012345678901234567890123456789012345678901234567890123..."), true);
            assertEquals(ex.getParsedString(), "ONEXXX67890123456789012345678901234567890123456789012345678901234567890123456789");
            assertEquals(ex.getErrorIndex(), 3);
            throw ex;
        }
    }

    @Test(expected=NullPointerException.class)
    public void test_toFormat_parseObject_String_null() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        format.parseObject((String) null);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_toFormat_parseObject_StringParsePosition() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        ParsePosition pos = new ParsePosition(0);
        TemporalAccessor result = (TemporalAccessor) format.parseObject("ONE30XXX", pos);
        assertEquals(pos.getIndex(), 5);
        assertEquals(pos.getErrorIndex(), -1);
        assertEquals(result.isSupported(DAY_OF_MONTH), true);
        assertEquals(result.getLong(DAY_OF_MONTH), 30L);
    }

    @Test
    public void test_toFormat_parseObject_StringParsePosition_parseError() throws Exception {
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        ParsePosition pos = new ParsePosition(0);
        TemporalAccessor result = (TemporalAccessor) format.parseObject("ONEXXX", pos);
        assertEquals(pos.getIndex(), 0);  // TODO: is this right?
        assertEquals(pos.getErrorIndex(), 3);
        assertEquals(result, null);
    }

    @Test(expected=NullPointerException.class)
    public void test_toFormat_parseObject_StringParsePosition_nullString() throws Exception {
        // SimpleDateFormat has this behavior
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        ParsePosition pos = new ParsePosition(0);
        format.parseObject((String) null, pos);
    }

    @Test(expected=NullPointerException.class)
    public void test_toFormat_parseObject_StringParsePosition_nullParsePosition() throws Exception {
        // SimpleDateFormat has this behavior
        DateTimeFormatter test = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        Format format = test.toFormat();
        format.parseObject("ONE30", (ParsePosition) null);
    }

    @Test
    public void test_toFormat_parseObject_StringParsePosition_invalidPosition_tooBig() throws Exception {
        // SimpleDateFormat has this behavior
        DateTimeFormatter dtf = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        ParsePosition pos = new ParsePosition(6);
        Format test = dtf.toFormat();
        assertNull(test.parseObject("ONE30", pos));
        assertTrue(pos.getErrorIndex() >= 0);
    }

    @Test
    public void test_toFormat_parseObject_StringParsePosition_invalidPosition_tooSmall() throws Exception {
        // SimpleDateFormat throws StringIndexOutOfBoundException
        DateTimeFormatter dtf = fmt.withLocale(Locale.ENGLISH).withDecimalStyle(DecimalStyle.STANDARD);
        ParsePosition pos = new ParsePosition(-1);
        Format test = dtf.toFormat();
        assertNull(test.parseObject("ONE30", pos));
        assertTrue(pos.getErrorIndex() >= 0);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_toFormat_Query_format() throws Exception {
        Format format = BASIC_FORMATTER.toFormat();
        String result = format.format(LocalDate.of(2008, 6, 30));
        assertEquals(result, "ONE30");
    }

    @Test
    public void test_toFormat_Query_parseObject_String() throws Exception {
        Format format = DATE_FORMATTER.toFormat(LocalDate::from);
        LocalDate result = (LocalDate) format.parseObject("ONE2012 07 27");
        assertEquals(result, LocalDate.of(2012, 7, 27));
    }

    @Test(expected=ParseException.class)
    public void test_toFormat_parseObject_StringParsePosition_dateTimeError() throws Exception {
        Format format = DATE_FORMATTER.toFormat(LocalDate::from);
        format.parseObject("ONE2012 07 32");
    }

    @Test(expected=NullPointerException.class)
    public void test_toFormat_Query() throws Exception {
        BASIC_FORMATTER.toFormat(null);
    }

}
