/*
 * 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) 2009-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.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.NANO_OF_SECOND;
import static java.time.temporal.ChronoField.YEAR;
import static org.testng.Assert.assertEquals;

import java.text.ParsePosition;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.SignStyle;
import java.time.format.TextStyle;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAccessor;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * Test DateTimeFormatterBuilder.
 */
@Test
public class TCKDateTimeFormatterBuilder {

    private DateTimeFormatterBuilder builder;

    @BeforeMethod
    public void setUp() {
        builder = new DateTimeFormatterBuilder();
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_toFormatter_empty() throws Exception {
        DateTimeFormatter f = builder.toFormatter();
        assertEquals(f.format(LocalDate.of(2012, 6, 30)), "");
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_parseDefaulting_entireDate() {
        DateTimeFormatter f = builder
            .parseDefaulting(YEAR, 2012).parseDefaulting(MONTH_OF_YEAR, 6)
            .parseDefaulting(DAY_OF_MONTH, 30).toFormatter();
        LocalDate parsed = f.parse("", LocalDate::from);  // blank string can be parsed
        assertEquals(parsed, LocalDate.of(2012, 6, 30));
    }

    @Test
    public void test_parseDefaulting_yearOptionalMonthOptionalDay() {
        DateTimeFormatter f = builder
                .appendValue(YEAR)
                .optionalStart().appendLiteral('-').appendValue(MONTH_OF_YEAR)
                .optionalStart().appendLiteral('-').appendValue(DAY_OF_MONTH)
                .optionalEnd().optionalEnd()
                .parseDefaulting(MONTH_OF_YEAR, 1)
                .parseDefaulting(DAY_OF_MONTH, 1).toFormatter();
        assertEquals(f.parse("2012", LocalDate::from), LocalDate.of(2012, 1, 1));
        assertEquals(f.parse("2012-6", LocalDate::from), LocalDate.of(2012, 6, 1));
        assertEquals(f.parse("2012-6-30", LocalDate::from), LocalDate.of(2012, 6, 30));
    }

    @Test(expectedExceptions = NullPointerException.class)
    public void test_parseDefaulting_null() {
        builder.parseDefaulting(null, 1);
    }

    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendValue_1arg_null() throws Exception {
        builder.appendValue(null);
    }

    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendValue_2arg_null() throws Exception {
        builder.appendValue(null, 3);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValue_2arg_widthTooSmall() throws Exception {
        builder.appendValue(DAY_OF_MONTH, 0);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValue_2arg_widthTooBig() throws Exception {
        builder.appendValue(DAY_OF_MONTH, 20);
    }

    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendValue_3arg_nullField() throws Exception {
        builder.appendValue(null, 2, 3, SignStyle.NORMAL);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValue_3arg_minWidthTooSmall() throws Exception {
        builder.appendValue(DAY_OF_MONTH, 0, 2, SignStyle.NORMAL);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValue_3arg_minWidthTooBig() throws Exception {
        builder.appendValue(DAY_OF_MONTH, 20, 2, SignStyle.NORMAL);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValue_3arg_maxWidthTooSmall() throws Exception {
        builder.appendValue(DAY_OF_MONTH, 2, 0, SignStyle.NORMAL);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValue_3arg_maxWidthTooBig() throws Exception {
        builder.appendValue(DAY_OF_MONTH, 2, 20, SignStyle.NORMAL);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValue_3arg_maxWidthMinWidth() throws Exception {
        builder.appendValue(DAY_OF_MONTH, 4, 2, SignStyle.NORMAL);
    }

    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendValue_3arg_nullSignStyle() throws Exception {
        builder.appendValue(DAY_OF_MONTH, 2, 3, null);
    }

    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendValueReduced_int_nullField() throws Exception {
        builder.appendValueReduced(null, 2, 2, 2000);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_int_minWidthTooSmall() throws Exception {
        builder.appendValueReduced(YEAR, 0, 2, 2000);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_int_minWidthTooBig() throws Exception {
        builder.appendValueReduced(YEAR, 11, 2, 2000);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_int_maxWidthTooSmall() throws Exception {
        builder.appendValueReduced(YEAR, 2, 0, 2000);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_int_maxWidthTooBig() throws Exception {
        builder.appendValueReduced(YEAR, 2, 11, 2000);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_int_maxWidthLessThanMin() throws Exception {
        builder.appendValueReduced(YEAR, 2, 1, 2000);
    }

    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendValueReduced_date_nullField() throws Exception {
        builder.appendValueReduced(null, 2, 2, LocalDate.of(2000, 1, 1));
    }

    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendValueReduced_date_nullDate() throws Exception {
        builder.appendValueReduced(YEAR, 2, 2, null);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_date_minWidthTooSmall() throws Exception {
        builder.appendValueReduced(YEAR, 0, 2, LocalDate.of(2000, 1, 1));
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_date_minWidthTooBig() throws Exception {
        builder.appendValueReduced(YEAR, 11, 2, LocalDate.of(2000, 1, 1));
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_date_maxWidthTooSmall() throws Exception {
        builder.appendValueReduced(YEAR, 2, 0, LocalDate.of(2000, 1, 1));
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_date_maxWidthTooBig() throws Exception {
        builder.appendValueReduced(YEAR, 2, 11, LocalDate.of(2000, 1, 1));
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendValueReduced_date_maxWidthLessThanMin() throws Exception {
        builder.appendValueReduced(YEAR, 2, 1, LocalDate.of(2000, 1, 1));
    }

    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendFraction_4arg_nullRule() throws Exception {
        builder.appendFraction(null, 1, 9, false);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendFraction_4arg_invalidRuleNotFixedSet() throws Exception {
        builder.appendFraction(DAY_OF_MONTH, 1, 9, false);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendFraction_4arg_minTooSmall() throws Exception {
        builder.appendFraction(MINUTE_OF_HOUR, -1, 9, false);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendFraction_4arg_minTooBig() throws Exception {
        builder.appendFraction(MINUTE_OF_HOUR, 10, 9, false);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendFraction_4arg_maxTooSmall() throws Exception {
        builder.appendFraction(MINUTE_OF_HOUR, 0, -1, false);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendFraction_4arg_maxTooBig() throws Exception {
        builder.appendFraction(MINUTE_OF_HOUR, 1, 10, false);
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_appendFraction_4arg_maxWidthMinWidth() throws Exception {
        builder.appendFraction(MINUTE_OF_HOUR, 9, 3, false);
    }

    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendText_1arg_null() throws Exception {
        builder.appendText(null);
    }

    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendText_2arg_nullRule() throws Exception {
        builder.appendText(null, TextStyle.SHORT);
    }

    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendText_2arg_nullStyle() throws Exception {
        builder.appendText(MONTH_OF_YEAR, (TextStyle) null);
    }

    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendTextMap_nullRule() throws Exception {
        builder.appendText(null, new HashMap<>());
    }

    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendTextMap_nullStyle() throws Exception {
        builder.appendText(MONTH_OF_YEAR, (Map<Long, String>) null);
    }

    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    @DataProvider(name="offsetPatterns")
    Object[][] data_offsetPatterns() {
        return new Object[][] {
                {"+HH", 2, 0, 0, "+02"},
                {"+HH", -2, 0, 0, "-02"},
                {"+HH", 2, 30, 0, "+02"},
                {"+HH", 2, 0, 45, "+02"},
                {"+HH", 2, 30, 45, "+02"},

                {"+HHMM", 2, 0, 0, "+0200"},
                {"+HHMM", -2, 0, 0, "-0200"},
                {"+HHMM", 2, 30, 0, "+0230"},
                {"+HHMM", 2, 0, 45, "+0200"},
                {"+HHMM", 2, 30, 45, "+0230"},

                {"+HH:MM", 2, 0, 0, "+02:00"},
                {"+HH:MM", -2, 0, 0, "-02:00"},
                {"+HH:MM", 2, 30, 0, "+02:30"},
                {"+HH:MM", 2, 0, 45, "+02:00"},
                {"+HH:MM", 2, 30, 45, "+02:30"},

                {"+HHMMss", 2, 0, 0, "+0200"},
                {"+HHMMss", -2, 0, 0, "-0200"},
                {"+HHMMss", 2, 30, 0, "+0230"},
                {"+HHMMss", 2, 0, 45, "+020045"},
                {"+HHMMss", 2, 30, 45, "+023045"},

                {"+HH:MM:ss", 2, 0, 0, "+02:00"},
                {"+HH:MM:ss", -2, 0, 0, "-02:00"},
                {"+HH:MM:ss", 2, 30, 0, "+02:30"},
                {"+HH:MM:ss", 2, 0, 45, "+02:00:45"},
                {"+HH:MM:ss", 2, 30, 45, "+02:30:45"},

                {"+HHMMSS", 2, 0, 0, "+020000"},
                {"+HHMMSS", -2, 0, 0, "-020000"},
                {"+HHMMSS", 2, 30, 0, "+023000"},
                {"+HHMMSS", 2, 0, 45, "+020045"},
                {"+HHMMSS", 2, 30, 45, "+023045"},

                {"+HH:MM:SS", 2, 0, 0, "+02:00:00"},
                {"+HH:MM:SS", -2, 0, 0, "-02:00:00"},
                {"+HH:MM:SS", 2, 30, 0, "+02:30:00"},
                {"+HH:MM:SS", 2, 0, 45, "+02:00:45"},
                {"+HH:MM:SS", 2, 30, 45, "+02:30:45"},
        };
    }

    @Test(dataProvider="offsetPatterns")
    public void test_appendOffset_format(String pattern, int h, int m, int s, String expected) throws Exception {
        builder.appendOffset(pattern, "Z");
        DateTimeFormatter f = builder.toFormatter();
        ZoneOffset offset = ZoneOffset.ofHoursMinutesSeconds(h, m, s);
        assertEquals(f.format(offset), expected);
    }

    @Test(dataProvider="offsetPatterns")
    public void test_appendOffset_parse(String pattern, int h, int m, int s, String expected) throws Exception {
        builder.appendOffset(pattern, "Z");
        DateTimeFormatter f = builder.toFormatter();
        ZoneOffset parsed = f.parse(expected, ZoneOffset::from);
        assertEquals(f.format(parsed), expected);
    }

    @DataProvider(name="badOffsetPatterns")
    Object[][] data_badOffsetPatterns() {
        return new Object[][] {
            {"HH"},
            {"HHMM"},
            {"HH:MM"},
            {"HHMMss"},
            {"HH:MM:ss"},
            {"HHMMSS"},
            {"HH:MM:SS"},
            {"+H"},
            {"+HMM"},
            {"+HHM"},
            {"+A"},
        };
    }

    @Test(dataProvider="badOffsetPatterns", expectedExceptions=IllegalArgumentException.class)
    public void test_appendOffset_badPattern(String pattern) throws Exception {
        builder.appendOffset(pattern, "Z");
    }

    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendOffset_3arg_nullText() throws Exception {
        builder.appendOffset("+HH:MM", null);
    }

    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendOffset_3arg_nullPattern() throws Exception {
        builder.appendOffset(null, "Z");
    }

    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    @Test(expectedExceptions=NullPointerException.class)
    public void test_appendZoneText_1arg_nullText() throws Exception {
        builder.appendZoneText(null);
    }

    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    @Test
    public void test_padNext_1arg() {
        builder.appendValue(MONTH_OF_YEAR).appendLiteral(':').padNext(2).appendValue(DAY_OF_MONTH);
        assertEquals(builder.toFormatter().format(LocalDate.of(2013, 2, 1)), "2: 1");
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_padNext_1arg_invalidWidth() throws Exception {
        builder.padNext(0);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_padNext_2arg_dash() throws Exception {
        builder.appendValue(MONTH_OF_YEAR).appendLiteral(':').padNext(2, '-').appendValue(DAY_OF_MONTH);
        assertEquals(builder.toFormatter().format(LocalDate.of(2013, 2, 1)), "2:-1");
    }

    @Test(expectedExceptions=IllegalArgumentException.class)
    public void test_padNext_2arg_invalidWidth() throws Exception {
        builder.padNext(0, '-');
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_padOptional() throws Exception {
        builder.appendValue(MONTH_OF_YEAR).appendLiteral(':')
                .padNext(5).optionalStart().appendValue(DAY_OF_MONTH).optionalEnd()
                .appendLiteral(':').appendValue(YEAR);
        assertEquals(builder.toFormatter().format(LocalDate.of(2013, 2, 1)), "2:    1:2013");
        assertEquals(builder.toFormatter().format(YearMonth.of(2013, 2)), "2:     :2013");
    }

    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    @Test(expectedExceptions=IllegalStateException.class)
    public void test_optionalEnd_noStart() throws Exception {
        builder.optionalEnd();
    }

    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    //-----------------------------------------------------------------------
    @DataProvider(name="validPatterns")
    Object[][] dataValid() {
        return new Object[][] {
            {"'a'"},
            {"''"},
            {"'!'"},
            {"!"},
            {"'#'"},

            {"'hello_people,][)('"},
            {"'hi'"},
            {"'yyyy'"},
            {"''''"},
            {"'o''clock'"},

            {"G"},
            {"GG"},
            {"GGG"},
            {"GGGG"},
            {"GGGGG"},

            {"y"},
            {"yy"},
            {"yyy"},
            {"yyyy"},
            {"yyyyy"},

            {"M"},
            {"MM"},
            {"MMM"},
            {"MMMM"},
            {"MMMMM"},

            {"L"},
            {"LL"},
            {"LLL"},
            {"LLLL"},
            {"LLLLL"},

            {"D"},
            {"DD"},
            {"DDD"},

            {"d"},
            {"dd"},

            {"F"},

            {"Q"},
            {"QQ"},
            {"QQQ"},
            {"QQQQ"},
            {"QQQQQ"},

            {"q"},
            {"qq"},
            {"qqq"},
            {"qqqq"},
            {"qqqqq"},

            {"E"},
            {"EE"},
            {"EEE"},
            {"EEEE"},
            {"EEEEE"},

            {"e"},
            {"ee"},
            {"eee"},
            {"eeee"},
            {"eeeee"},

            {"c"},
            {"ccc"},
            {"cccc"},
            {"ccccc"},

            {"a"},

            {"H"},
            {"HH"},

            {"K"},
            {"KK"},

            {"k"},
            {"kk"},

            {"h"},
            {"hh"},

            {"m"},
            {"mm"},

            {"s"},
            {"ss"},

            {"S"},
            {"SS"},
            {"SSS"},
            {"SSSSSSSSS"},

            {"A"},
            {"AA"},
            {"AAA"},

            {"n"},
            {"nn"},
            {"nnn"},

            {"N"},
            {"NN"},
            {"NNN"},

            {"z"},
            {"zz"},
            {"zzz"},
            {"zzzz"},

            {"VV"},

            {"Z"},
            {"ZZ"},
            {"ZZZ"},

            {"X"},
            {"XX"},
            {"XXX"},
            {"XXXX"},
            {"XXXXX"},

            {"x"},
            {"xx"},
            {"xxx"},
            {"xxxx"},
            {"xxxxx"},

            {"ppH"},
            {"pppDD"},

            {"yyyy[-MM[-dd"},
            {"yyyy[-MM[-dd]]"},
            {"yyyy[-MM[]-dd]"},

            {"yyyy-MM-dd'T'HH:mm:ss.SSS"},

            {"e"},
            {"w"},
            {"ww"},
            {"W"},
            {"W"},

        };
    }

    @Test(dataProvider="validPatterns")
    public void test_appendPattern_valid(String input) throws Exception {
        builder.appendPattern(input);  // test is for no error here
    }

    //-----------------------------------------------------------------------
    @DataProvider(name="invalidPatterns")
    Object[][] dataInvalid() {
        return new Object[][] {
            {"'"},
            {"'hello"},
            {"'hel''lo"},
            {"'hello''"},
            {"{"},
            {"}"},
            {"{}"},
            {"#"},
            {"]"},
            {"yyyy]"},
            {"yyyy]MM"},
            {"yyyy[MM]]"},

            {"aa"},
            {"aaa"},
            {"aaaa"},
            {"aaaaa"},
            {"aaaaaa"},
            {"MMMMMM"},
            {"QQQQQQ"},
            {"qqqqqq"},
            {"EEEEEE"},
            {"eeeeee"},
            {"cc"},
            {"cccccc"},
            {"ddd"},
            {"DDDD"},
            {"FF"},
            {"FFF"},
            {"hhh"},
            {"HHH"},
            {"kkk"},
            {"KKK"},
            {"mmm"},
            {"sss"},
            {"OO"},
            {"OOO"},
            {"OOOOO"},
            {"XXXXXX"},
            {"zzzzz"},
            {"ZZZZZZ"},

            {"RO"},

            {"p"},
            {"pp"},
            {"p:"},

            {"f"},
            {"ff"},
            {"f:"},
            {"fy"},
            {"fa"},
            {"fM"},

            {"www"},
            {"WW"},
        };
    }

    @Test(dataProvider="invalidPatterns", expectedExceptions=IllegalArgumentException.class)
    public void test_appendPattern_invalid(String input) throws Exception {
        builder.appendPattern(input);  // test is for error here
    }

    //-----------------------------------------------------------------------
    @DataProvider(name="patternPrint")
    Object[][] data_patternPrint() {
        return new Object[][] {
            {"Q", date(2012, 2, 10), "1"},
            {"QQ", date(2012, 2, 10), "01"},
            {"QQQ", date(2012, 2, 10), "Q1"},
            {"QQQQ", date(2012, 2, 10), "1st quarter"},
            {"QQQQQ", date(2012, 2, 10), "1"},
        };
    }

    @Test(dataProvider="patternPrint")
    public void test_appendPattern_patternPrint(String input, Temporal temporal, String expected) throws Exception {
        DateTimeFormatter f = builder.appendPattern(input).toFormatter(Locale.UK);
        String test = f.format(temporal);
        assertEquals(test, expected);
    }

    private static Temporal date(int y, int m, int d) {
        return LocalDate.of(y, m, d);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_adjacent_strict_firstFixedWidth() throws Exception {
        // succeeds because both number elements are fixed width
        DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('9').toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("12309", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 5);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 12L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L);
    }

    @Test
    public void test_adjacent_strict_firstVariableWidth_success() throws Exception {
        // succeeds greedily parsing variable width, then fixed width, to non-numeric Z
        DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('Z').toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("12309Z", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 6);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 123L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 9L);
    }

    @Test
    public void test_adjacent_strict_firstVariableWidth_fails() throws Exception {
        // fails because literal is a number and variable width parse greedily absorbs it
        DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('9').toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("12309", pp);
        assertEquals(pp.getErrorIndex(), 5);
        assertEquals(parsed, null);
    }

    @Test
    public void test_adjacent_lenient() throws Exception {
        // succeeds because both number elements are fixed width even in lenient mode
        DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('9').toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("12309", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 5);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 12L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L);
    }

    @Test
    public void test_adjacent_lenient_firstVariableWidth_success() throws Exception {
        // succeeds greedily parsing variable width, then fixed width, to non-numeric Z
        DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('Z').toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("12309Z", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 6);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 123L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 9L);
    }

    @Test
    public void test_adjacent_lenient_firstVariableWidth_fails() throws Exception {
        // fails because literal is a number and variable width parse greedily absorbs it
        DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY).appendValue(MINUTE_OF_HOUR, 2).appendLiteral('9').toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("12309", pp);
        assertEquals(pp.getErrorIndex(), 5);
        assertEquals(parsed, null);
    }

    //-----------------------------------------------------------------------
    @Test
    public void test_adjacent_strict_fractionFollows() throws Exception {
        // succeeds because hour/min are fixed width
        DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 0, 3, false).toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("1230567", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 7);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 12L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L);
        assertEquals(parsed.getLong(NANO_OF_SECOND), 567_000_000L);
    }

    @Test
    public void test_adjacent_strict_fractionFollows_2digit() throws Exception {
        // succeeds because hour/min are fixed width
        DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 0, 3, false).toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("123056", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 6);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 12L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L);
        assertEquals(parsed.getLong(NANO_OF_SECOND), 560_000_000L);
    }

    @Test
    public void test_adjacent_strict_fractionFollows_0digit() throws Exception {
        // succeeds because hour/min are fixed width
        DateTimeFormatter f = builder.appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 0, 3, false).toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("1230", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 4);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 12L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L);
    }

    @Test
    public void test_adjacent_lenient_fractionFollows() throws Exception {
        // succeeds because hour/min are fixed width
        DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 3, 3, false).toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("1230567", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 7);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 12L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L);
        assertEquals(parsed.getLong(NANO_OF_SECOND), 567_000_000L);
    }

    @Test
    public void test_adjacent_lenient_fractionFollows_2digit() throws Exception {
        // succeeds because hour/min are fixed width
        DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 3, 3, false).toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("123056", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 6);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 12L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L);
        assertEquals(parsed.getLong(NANO_OF_SECOND), 560_000_000L);
    }

    @Test
    public void test_adjacent_lenient_fractionFollows_0digit() throws Exception {
        // succeeds because hour/min are fixed width
        DateTimeFormatter f = builder.parseLenient().appendValue(HOUR_OF_DAY, 2).appendValue(MINUTE_OF_HOUR, 2).appendFraction(NANO_OF_SECOND, 3, 3, false).toFormatter(Locale.UK);
        ParsePosition pp = new ParsePosition(0);
        TemporalAccessor parsed = f.parseUnresolved("1230", pp);
        assertEquals(pp.getErrorIndex(), -1);
        assertEquals(pp.getIndex(), 4);
        assertEquals(parsed.getLong(HOUR_OF_DAY), 12L);
        assertEquals(parsed.getLong(MINUTE_OF_HOUR), 30L);
    }

}
