/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 */

/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.
 */

/*
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

package org.opensearch.common.time;

import org.opensearch.common.joda.Joda;
import org.opensearch.test.OpenSearchTestCase;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.Locale;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;

public class DateFormattersTests extends OpenSearchTestCase {

    public void testWeekBasedDates() {
        // as per WeekFields.ISO first week starts on Monday and has minimum 4 days
        DateFormatter dateFormatter = DateFormatters.forPattern("YYYY-ww");

        // first week of 2016 starts on Monday 2016-01-04 as previous week in 2016 has only 3 days
        assertThat(
            DateFormatters.from(dateFormatter.parse("2016-01")),
            equalTo(ZonedDateTime.of(2016, 01, 04, 0, 0, 0, 0, ZoneOffset.UTC))
        );

        // first week of 2015 starts on Monday 2014-12-29 because 4days belong to 2019
        assertThat(
            DateFormatters.from(dateFormatter.parse("2015-01")),
            equalTo(ZonedDateTime.of(2014, 12, 29, 0, 0, 0, 0, ZoneOffset.UTC))
        );

        // as per WeekFields.ISO first week starts on Monday and has minimum 4 days
        dateFormatter = DateFormatters.forPattern("YYYY");

        // first week of 2016 starts on Monday 2016-01-04 as previous week in 2016 has only 3 days
        assertThat(DateFormatters.from(dateFormatter.parse("2016")), equalTo(ZonedDateTime.of(2016, 01, 04, 0, 0, 0, 0, ZoneOffset.UTC)));

        // first week of 2015 starts on Monday 2014-12-29 because 4days belong to 2019
        assertThat(DateFormatters.from(dateFormatter.parse("2015")), equalTo(ZonedDateTime.of(2014, 12, 29, 0, 0, 0, 0, ZoneOffset.UTC)));
    }

    // this is not in the duelling tests, because the epoch millis parser in joda time drops the milliseconds after the comma
    // but is able to parse the rest
    // as this feature is supported it also makes sense to make it exact
    public void testEpochMillisParser() {
        DateFormatter formatter = DateFormatters.forPattern("epoch_millis");
        {
            Instant instant = Instant.from(formatter.parse("12345"));
            assertThat(instant.getEpochSecond(), is(12L));
            assertThat(instant.getNano(), is(345_000_000));
            assertThat(formatter.format(instant), is("12345"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("0"));
            assertThat(instant.getEpochSecond(), is(0L));
            assertThat(instant.getNano(), is(0));
            assertThat(formatter.format(instant), is("0"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-123000.123456"));
            assertThat(instant.getEpochSecond(), is(-124L));
            assertThat(instant.getNano(), is(999876544));
            assertThat(formatter.format(instant), is("-123000.123456"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("123.123456"));
            assertThat(instant.getEpochSecond(), is(0L));
            assertThat(instant.getNano(), is(123123456));
            assertThat(formatter.format(instant), is("123.123456"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-123.123456"));
            assertThat(instant.getEpochSecond(), is(-1L));
            assertThat(instant.getNano(), is(876876544));
            assertThat(formatter.format(instant), is("-123.123456"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6789123.123456"));
            assertThat(instant.getEpochSecond(), is(-6790L));
            assertThat(instant.getNano(), is(876876544));
            assertThat(formatter.format(instant), is("-6789123.123456"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("6789123.123456"));
            assertThat(instant.getEpochSecond(), is(6789L));
            assertThat(instant.getNano(), is(123123456));
            assertThat(formatter.format(instant), is("6789123.123456"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6250000430768.25"));
            assertThat(instant.getEpochSecond(), is(-6250000431L));
            assertThat(instant.getNano(), is(231750000));
            assertThat(formatter.format(instant), is("-6250000430768.25"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6250000430768.75"));
            assertThat(instant.getEpochSecond(), is(-6250000431L));
            assertThat(instant.getNano(), is(231250000));
            assertThat(formatter.format(instant), is("-6250000430768.75"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6250000430768.00"));
            assertThat(instant.getEpochSecond(), is(-6250000431L));
            assertThat(instant.getNano(), is(232000000));
            assertThat(formatter.format(instant), is("-6250000430768")); // remove .00 precision
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6250000431000.250000"));
            assertThat(instant.getEpochSecond(), is(-6250000432L));
            assertThat(instant.getNano(), is(999750000));
            assertThat(formatter.format(instant), is("-6250000431000.25"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6250000431000.000001"));
            assertThat(instant.getEpochSecond(), is(-6250000432L));
            assertThat(instant.getNano(), is(999999999));
            assertThat(formatter.format(instant), is("-6250000431000.000001"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6250000431000.75"));
            assertThat(instant.getEpochSecond(), is(-6250000432L));
            assertThat(instant.getNano(), is(999250000));
            assertThat(formatter.format(instant), is("-6250000431000.75"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6250000431000.00"));
            assertThat(instant.getEpochSecond(), is(-6250000431L));
            assertThat(instant.getNano(), is(0));
            assertThat(formatter.format(instant), is("-6250000431000"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6250000431000"));
            assertThat(instant.getEpochSecond(), is(-6250000431L));
            assertThat(instant.getNano(), is(0));
            assertThat(formatter.format(instant), is("-6250000431000"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-6250000430768"));
            assertThat(instant.getEpochSecond(), is(-6250000431L));
            assertThat(instant.getNano(), is(232000000));
            assertThat(formatter.format(instant), is("-6250000430768"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("1680000430768"));
            assertThat(instant.getEpochSecond(), is(1680000430L));
            assertThat(instant.getNano(), is(768000000));
            assertThat(formatter.format(instant), is("1680000430768"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
        {
            Instant instant = Instant.from(formatter.parse("-0.12345"));
            assertThat(instant.getEpochSecond(), is(-1L));
            assertThat(instant.getNano(), is(999876550));
            assertThat(formatter.format(instant), is("-0.12345"));
            assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant));
        }
    }

    public void testInvalidEpochMilliParser() {
        DateFormatter formatter = DateFormatters.forPattern("epoch_millis");
        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> formatter.parse("invalid"));
        assertThat(e.getMessage(), containsString("failed to parse date field [invalid] with format [epoch_millis]"));

        e = expectThrows(IllegalArgumentException.class, () -> formatter.parse("123.1234567"));
        assertThat(e.getMessage(), containsString("failed to parse date field [123.1234567] with format [epoch_millis]"));
    }

    // this is not in the duelling tests, because the epoch second parser in joda time drops the milliseconds after the comma
    // but is able to parse the rest
    // as this feature is supported it also makes sense to make it exact
    public void testEpochSecondParserWithFraction() {
        DateFormatter formatter = DateFormatters.forPattern("epoch_second");

        TemporalAccessor accessor = formatter.parse("1234.1");
        Instant instant = DateFormatters.from(accessor).toInstant();
        assertThat(instant.getEpochSecond(), is(1234L));
        assertThat(DateFormatters.from(accessor).toInstant().getNano(), is(100_000_000));

        accessor = formatter.parse("1234");
        instant = DateFormatters.from(accessor).toInstant();
        assertThat(instant.getEpochSecond(), is(1234L));
        assertThat(instant.getNano(), is(0));

        IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> formatter.parse("abc"));
        assertThat(e.getMessage(), is("failed to parse date field [abc] with format [epoch_second]"));

        e = expectThrows(IllegalArgumentException.class, () -> formatter.parse("1234.abc"));
        assertThat(e.getMessage(), is("failed to parse date field [1234.abc] with format [epoch_second]"));

        e = expectThrows(IllegalArgumentException.class, () -> formatter.parse("1234.1234567890"));
        assertThat(e.getMessage(), is("failed to parse date field [1234.1234567890] with format [epoch_second]"));
    }

    public void testEpochMilliParsersWithDifferentFormatters() {
        DateFormatter formatter = DateFormatter.forPattern("strict_date_optional_time||epoch_millis");
        TemporalAccessor accessor = formatter.parse("123");
        assertThat(DateFormatters.from(accessor).toInstant().toEpochMilli(), is(123L));
        assertThat(formatter.pattern(), is("strict_date_optional_time||epoch_millis"));
    }

    public void testParsersWithMultipleInternalFormats() throws Exception {
        ZonedDateTime first = DateFormatters.from(
            DateFormatters.forPattern("strict_date_optional_time_nanos").parse("2018-05-15T17:14:56+0100")
        );
        ZonedDateTime second = DateFormatters.from(
            DateFormatters.forPattern("strict_date_optional_time_nanos").parse("2018-05-15T17:14:56+01:00")
        );
        assertThat(first, is(second));
    }

    public void testNanoOfSecondWidth() throws Exception {
        ZonedDateTime first = DateFormatters.from(
            DateFormatters.forPattern("strict_date_optional_time_nanos").parse("1970-01-01T00:00:00.1")
        );
        assertThat(first.getNano(), is(100000000));
        ZonedDateTime second = DateFormatters.from(
            DateFormatters.forPattern("strict_date_optional_time_nanos").parse("1970-01-01T00:00:00.000000001")
        );
        assertThat(second.getNano(), is(1));
    }

    public void testLocales() {
        assertThat(DateFormatters.forPattern("strict_date_optional_time").locale(), is(Locale.ROOT));
        Locale locale = randomLocale(random());
        assertThat(DateFormatters.forPattern("strict_date_optional_time").withLocale(locale).locale(), is(locale));
    }

    public void testTimeZones() {
        // zone is null by default due to different behaviours between java8 and above
        assertThat(DateFormatters.forPattern("strict_date_optional_time").zone(), is(nullValue()));
        ZoneId zoneId = randomZone();
        assertThat(DateFormatters.forPattern("strict_date_optional_time").withZone(zoneId).zone(), is(zoneId));
    }

    public void testEqualsAndHashcode() {
        assertThat(
            DateFormatters.forPattern("strict_date_optional_time"),
            sameInstance(DateFormatters.forPattern("strict_date_optional_time"))
        );
        assertThat(DateFormatters.forPattern("YYYY"), equalTo(DateFormatters.forPattern("YYYY")));
        assertThat(DateFormatters.forPattern("YYYY").hashCode(), is(DateFormatters.forPattern("YYYY").hashCode()));

        // different timezone, thus not equals
        assertThat(DateFormatters.forPattern("YYYY").withZone(ZoneId.of("CET")), not(equalTo(DateFormatters.forPattern("YYYY"))));

        // different locale, thus not equals
        DateFormatter f1 = DateFormatters.forPattern("YYYY").withLocale(Locale.CANADA);
        DateFormatter f2 = f1.withLocale(Locale.FRENCH);
        assertThat(f1, not(equalTo(f2)));

        // different pattern, thus not equals
        assertThat(DateFormatters.forPattern("YYYY"), not(equalTo(DateFormatters.forPattern("YY"))));

        DateFormatter epochSecondFormatter = DateFormatters.forPattern("epoch_second");
        assertThat(epochSecondFormatter, sameInstance(DateFormatters.forPattern("epoch_second")));
        assertThat(epochSecondFormatter, equalTo(DateFormatters.forPattern("epoch_second")));
        assertThat(epochSecondFormatter.hashCode(), is(DateFormatters.forPattern("epoch_second").hashCode()));

        DateFormatter epochMillisFormatter = DateFormatters.forPattern("epoch_millis");
        assertThat(epochMillisFormatter.hashCode(), is(DateFormatters.forPattern("epoch_millis").hashCode()));
        assertThat(epochMillisFormatter, sameInstance(DateFormatters.forPattern("epoch_millis")));
        assertThat(epochMillisFormatter, equalTo(DateFormatters.forPattern("epoch_millis")));
    }

    public void testSupportBackwardsJava8Format() {
        assertThat(DateFormatter.forPattern("8yyyy-MM-dd"), instanceOf(JavaDateFormatter.class));
        // named formats too
        assertThat(DateFormatter.forPattern("8date_optional_time"), instanceOf(JavaDateFormatter.class));
        // named formats too
        DateFormatter formatter = DateFormatter.forPattern("8date_optional_time||ww-MM-dd");
        assertThat(formatter, instanceOf(JavaDateFormatter.class));
    }

    public void testEpochFormatting() {
        long seconds = randomLongBetween(0, 130L * 365 * 86400); // from 1970 epoch till around 2100
        long nanos = randomLongBetween(0, 999_999_999L);
        Instant instant = Instant.ofEpochSecond(seconds, nanos);
        {
            DateFormatter millisFormatter = DateFormatter.forPattern("epoch_millis");
            String millis = millisFormatter.format(instant);
            Instant millisInstant = Instant.from(millisFormatter.parse(millis));
            assertThat(millisInstant.toEpochMilli(), is(instant.toEpochMilli()));
            assertThat(millisFormatter.format(Instant.ofEpochSecond(42, 0)), is("42000"));
            assertThat(millisFormatter.format(Instant.ofEpochSecond(42, 123456789L)), is("42123.456789"));

            DateFormatter secondsFormatter = DateFormatter.forPattern("epoch_second");
            String formattedSeconds = secondsFormatter.format(instant);
            Instant secondsInstant = Instant.from(secondsFormatter.parse(formattedSeconds));
            assertThat(secondsInstant.getEpochSecond(), is(instant.getEpochSecond()));

            assertThat(secondsFormatter.format(Instant.ofEpochSecond(42, 0)), is("42"));
        }
        {
            DateFormatter isoFormatter = DateFormatters.forPattern("strict_date_optional_time_nanos");
            DateFormatter millisFormatter = DateFormatter.forPattern("epoch_millis");
            String millis = millisFormatter.format(instant);
            String iso8601 = isoFormatter.format(instant);

            Instant millisInstant = Instant.from(millisFormatter.parse(millis));
            Instant isoInstant = Instant.from(isoFormatter.parse(iso8601));

            assertThat(millisInstant.toEpochMilli(), is(isoInstant.toEpochMilli()));
            assertThat(millisInstant.getEpochSecond(), is(isoInstant.getEpochSecond()));
            assertThat(millisInstant.getNano(), is(isoInstant.getNano()));
        }
    }

    public void testEpochFormattingNegativeEpoch() {
        long seconds = randomLongBetween(-130L * 365 * 86400, 0); // around 1840 till 1970 epoch
        long nanos = randomLongBetween(0, 999_999_999L);
        Instant instant = Instant.ofEpochSecond(seconds, nanos);

        {
            DateFormatter millisFormatter = DateFormatter.forPattern("epoch_millis");
            String millis = millisFormatter.format(instant);
            Instant millisInstant = Instant.from(millisFormatter.parse(millis));
            assertThat(millisInstant.toEpochMilli(), is(instant.toEpochMilli()));
            assertThat(millisFormatter.format(Instant.ofEpochSecond(-42, 0)), is("-42000"));
            assertThat(millisFormatter.format(Instant.ofEpochSecond(-42, 123456789L)), is("-41876.543211"));

            DateFormatter secondsFormatter = DateFormatter.forPattern("epoch_second");
            String formattedSeconds = secondsFormatter.format(instant);
            Instant secondsInstant = Instant.from(secondsFormatter.parse(formattedSeconds));
            assertThat(secondsInstant.getEpochSecond(), is(instant.getEpochSecond()));

            assertThat(secondsFormatter.format(Instant.ofEpochSecond(42, 0)), is("42"));
        }
        {
            DateFormatter isoFormatter = DateFormatters.forPattern("strict_date_optional_time_nanos");
            DateFormatter millisFormatter = DateFormatter.forPattern("epoch_millis");
            String millis = millisFormatter.format(instant);
            String iso8601 = isoFormatter.format(instant);

            Instant millisInstant = Instant.from(millisFormatter.parse(millis));
            Instant isoInstant = Instant.from(isoFormatter.parse(iso8601));

            assertThat(millisInstant.toEpochMilli(), is(isoInstant.toEpochMilli()));
            assertThat(millisInstant.getEpochSecond(), is(isoInstant.getEpochSecond()));
            assertThat(millisInstant.getNano(), is(isoInstant.getNano()));
        }
    }

    public void testParsingStrictNanoDates() {
        DateFormatter formatter = DateFormatters.forPattern("strict_date_optional_time_nanos");
        formatter.format(formatter.parse("2016-01-01T00:00:00.000"));
        formatter.format(formatter.parse("2018-05-15T17:14:56"));
        formatter.format(formatter.parse("2018-05-15T17:14:56Z"));
        formatter.format(formatter.parse("2018-05-15T17:14:56+0100"));
        formatter.format(formatter.parse("2018-05-15T17:14:56+01:00"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456789Z"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456789+0100"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456789+01:00"));
    }

    public void testIso8601Parsing() {
        DateFormatter formatter = DateFormatters.forPattern("iso8601");

        // timezone not allowed with just date
        formatter.format(formatter.parse("2018-05-15"));

        formatter.format(formatter.parse("2018-05-15T17"));
        formatter.format(formatter.parse("2018-05-15T17Z"));
        formatter.format(formatter.parse("2018-05-15T17+0100"));
        formatter.format(formatter.parse("2018-05-15T17+01:00"));

        formatter.format(formatter.parse("2018-05-15T17:14"));
        formatter.format(formatter.parse("2018-05-15T17:14Z"));
        formatter.format(formatter.parse("2018-05-15T17:14-0100"));
        formatter.format(formatter.parse("2018-05-15T17:14-01:00"));

        formatter.format(formatter.parse("2018-05-15T17:14:56"));
        formatter.format(formatter.parse("2018-05-15T17:14:56Z"));
        formatter.format(formatter.parse("2018-05-15T17:14:56+0100"));
        formatter.format(formatter.parse("2018-05-15T17:14:56+01:00"));

        // milliseconds can be separated using comma or decimal point
        formatter.format(formatter.parse("2018-05-15T17:14:56.123"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123Z"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123-0100"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123-01:00"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123Z"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123+0100"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123+01:00"));

        // microseconds can be separated using comma or decimal point
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456Z"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456+0100"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456+01:00"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123456"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123456Z"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123456-0100"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123456-01:00"));

        // nanoseconds can be separated using comma or decimal point
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456789"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456789Z"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456789-0100"));
        formatter.format(formatter.parse("2018-05-15T17:14:56.123456789-01:00"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123456789"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123456789Z"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123456789+0100"));
        formatter.format(formatter.parse("2018-05-15T17:14:56,123456789+01:00"));
    }

    public void testRoundupFormatterWithEpochDates() {
        assertRoundupFormatter("epoch_millis", "1234567890", 1234567890L);
        // also check nanos of the epoch_millis formatter if it is rounded up to the nano second
        JavaDateFormatter roundUpFormatter = ((JavaDateFormatter) DateFormatter.forPattern("8epoch_millis")).getRoundupParser();
        Instant epochMilliInstant = DateFormatters.from(roundUpFormatter.parse("1234567890")).toInstant();
        assertThat(epochMilliInstant.getLong(ChronoField.NANO_OF_SECOND), is(890_999_999L));

        assertRoundupFormatter("strict_date_optional_time||epoch_millis", "2018-10-10T12:13:14.123Z", 1539173594123L);
        assertRoundupFormatter("strict_date_optional_time||epoch_millis", "1234567890", 1234567890L);
        assertRoundupFormatter("strict_date_optional_time||epoch_millis", "2018-10-10", 1539215999999L);
        assertRoundupFormatter("strict_date_optional_time||epoch_millis", "2019-01-25T15:37:17.346928Z", 1548430637346L);
        assertRoundupFormatter("uuuu-MM-dd'T'HH:mm:ss.SSS||epoch_millis", "2018-10-10T12:13:14.123", 1539173594123L);
        assertRoundupFormatter("uuuu-MM-dd'T'HH:mm:ss.SSS||epoch_millis", "1234567890", 1234567890L);

        assertRoundupFormatter("epoch_second", "1234567890", 1234567890999L);
        // also check nanos of the epoch_millis formatter if it is rounded up to the nano second
        JavaDateFormatter epochSecondRoundupParser = ((JavaDateFormatter) DateFormatter.forPattern("8epoch_second")).getRoundupParser();
        Instant epochSecondInstant = DateFormatters.from(epochSecondRoundupParser.parse("1234567890")).toInstant();
        assertThat(epochSecondInstant.getLong(ChronoField.NANO_OF_SECOND), is(999_999_999L));

        assertRoundupFormatter("strict_date_optional_time||epoch_second", "2018-10-10T12:13:14.123Z", 1539173594123L);
        assertRoundupFormatter("strict_date_optional_time||epoch_second", "1234567890", 1234567890999L);
        assertRoundupFormatter("strict_date_optional_time||epoch_second", "2018-10-10", 1539215999999L);
        assertRoundupFormatter("uuuu-MM-dd'T'HH:mm:ss.SSS||epoch_second", "2018-10-10T12:13:14.123", 1539173594123L);
        assertRoundupFormatter("uuuu-MM-dd'T'HH:mm:ss.SSS||epoch_second", "1234567890", 1234567890999L);
    }

    private void assertRoundupFormatter(String format, String input, long expectedMilliSeconds) {
        JavaDateFormatter dateFormatter = (JavaDateFormatter) DateFormatter.forPattern(format);
        dateFormatter.parse(input);
        JavaDateFormatter roundUpFormatter = dateFormatter.getRoundupParser();
        long millis = DateFormatters.from(roundUpFormatter.parse(input)).toInstant().toEpochMilli();
        assertThat(millis, is(expectedMilliSeconds));
    }

    public void testRoundupFormatterZone() {
        ZoneId zoneId = randomZone();
        String format = randomFrom(
            "epoch_second",
            "epoch_millis",
            "strict_date_optional_time",
            "uuuu-MM-dd'T'HH:mm:ss.SSS",
            "strict_date_optional_time||date_optional_time"
        );
        JavaDateFormatter formatter = (JavaDateFormatter) DateFormatter.forPattern(format).withZone(zoneId);
        JavaDateFormatter roundUpFormatter = formatter.getRoundupParser();
        assertThat(roundUpFormatter.zone(), is(zoneId));
        assertThat(formatter.zone(), is(zoneId));
    }

    public void testRoundupFormatterLocale() {
        Locale locale = randomLocale(random());
        String format = randomFrom(
            "epoch_second",
            "epoch_millis",
            "strict_date_optional_time",
            "uuuu-MM-dd'T'HH:mm:ss.SSS",
            "strict_date_optional_time||date_optional_time"
        );
        JavaDateFormatter formatter = (JavaDateFormatter) DateFormatter.forPattern(format).withLocale(locale);
        JavaDateFormatter roundupParser = formatter.getRoundupParser();
        assertThat(roundupParser.locale(), is(locale));
        assertThat(formatter.locale(), is(locale));
    }

    public void test0MillisAreFormatted() {
        DateFormatter formatter = DateFormatter.forPattern("strict_date_time");
        Clock clock = Clock.fixed(ZonedDateTime.of(2019, 02, 8, 11, 43, 00, 0, ZoneOffset.UTC).toInstant(), ZoneOffset.UTC);
        String formatted = formatter.formatMillis(clock.millis());
        assertThat(formatted, is("2019-02-08T11:43:00.000Z"));
    }

    public void testFractionalSeconds() {
        DateFormatter formatter = DateFormatters.forPattern("strict_date_optional_time");
        {
            Instant instant = Instant.from(formatter.parse("2019-05-06T14:52:37.1Z"));
            assertThat(instant.getNano(), is(100_000_000));
        }
        {
            Instant instant = Instant.from(formatter.parse("2019-05-06T14:52:37.12Z"));
            assertThat(instant.getNano(), is(120_000_000));
        }
        {
            Instant instant = Instant.from(formatter.parse("2019-05-06T14:52:37.123Z"));
            assertThat(instant.getNano(), is(123_000_000));
        }
        {
            Instant instant = Instant.from(formatter.parse("2019-05-06T14:52:37.1234Z"));
            assertThat(instant.getNano(), is(123_400_000));
        }
        {
            Instant instant = Instant.from(formatter.parse("2019-05-06T14:52:37.12345Z"));
            assertThat(instant.getNano(), is(123_450_000));
        }
        {
            Instant instant = Instant.from(formatter.parse("2019-05-06T14:52:37.123456Z"));
            assertThat(instant.getNano(), is(123_456_000));
        }
        {
            Instant instant = Instant.from(formatter.parse("2019-05-06T14:52:37.1234567Z"));
            assertThat(instant.getNano(), is(123_456_700));
        }
        {
            Instant instant = Instant.from(formatter.parse("2019-05-06T14:52:37.12345678Z"));
            assertThat(instant.getNano(), is(123_456_780));
        }
        {
            Instant instant = Instant.from(formatter.parse("2019-05-06T14:52:37.123456789Z"));
            assertThat(instant.getNano(), is(123_456_789));
        }
    }

    public void testWeek_yearDeprecation() {
        DateFormatter.forPattern("week_year");
        assertWarnings(
            "Format name \"week_year\" is deprecated and will be removed in a future version. " + "Use \"weekyear\" format instead"
        );
    }

    public void testCamelCaseDeprecation() {
        String[] deprecatedNames = new String[] {
            "basicDate",
            "basicDateTime",
            "basicDateTimeNoMillis",
            "basicOrdinalDate",
            "basicOrdinalDateTime",
            "basicOrdinalDateTimeNoMillis",
            "basicTime",
            "basicTimeNoMillis",
            "basicTTime",
            "basicTTimeNoMillis",
            "basicWeekDate",
            "basicWeekDateTime",
            "basicWeekDateTimeNoMillis",
            "dateHour",
            "dateHourMinute",
            "dateHourMinuteSecond",
            "dateHourMinuteSecondFraction",
            "dateHourMinuteSecondMillis",
            "dateOptionalTime",
            "dateTime",
            "dateTimeNoMillis",
            "hourMinute",
            "hourMinuteSecond",
            "hourMinuteSecondFraction",
            "hourMinuteSecondMillis",
            "ordinalDate",
            "ordinalDateTime",
            "ordinalDateTimeNoMillis",
            "timeNoMillis",
            "tTime",
            "tTimeNoMillis",
            "weekDate",
            "weekDateTime",
            "weekDateTimeNoMillis",
            "weekyearWeek",
            "weekyearWeekDay",
            "yearMonth",
            "yearMonthDay",
            "strictBasicWeekDate",
            "strictBasicWeekDateTime",
            "strictBasicWeekDateTimeNoMillis",
            "strictDate",
            "strictDateHour",
            "strictDateHourMinute",
            "strictDateHourMinuteSecond",
            "strictDateHourMinuteSecondFraction",
            "strictDateHourMinuteSecondMillis",
            "strictDateOptionalTime",
            "strictDateOptionalTimeNanos",
            "strictDateTime",
            "strictDateTimeNoMillis",
            "strictHour",
            "strictHourMinute",
            "strictHourMinuteSecond",
            "strictHourMinuteSecondFraction",
            "strictHourMinuteSecondMillis",
            "strictOrdinalDate",
            "strictOrdinalDateTime",
            "strictOrdinalDateTimeNoMillis",
            "strictTime",
            "strictTimeNoMillis",
            "strictTTime",
            "strictTTimeNoMillis",
            "strictWeekDate",
            "strictWeekDateTime",
            "strictWeekDateTimeNoMillis",
            "strictWeekyear",
            "strictWeekyearWeek",
            "strictWeekyearWeekDay",
            "strictYear",
            "strictYearMonth",
            "strictYearMonthDay" };
        for (String name : deprecatedNames) {
            String snakeCaseName = FormatNames.forName(name).getSnakeCaseName();

            DateFormatter dateFormatter = DateFormatter.forPattern(name);
            assertThat(dateFormatter.pattern(), equalTo(name));
            assertWarnings(
                "Camel case format name "
                    + name
                    + " is deprecated and will be removed in a future version. "
                    + "Use snake case name "
                    + snakeCaseName
                    + " instead."
            );

            dateFormatter = DateFormatter.forPattern(snakeCaseName);
            assertThat(dateFormatter.pattern(), equalTo(snakeCaseName));
        }

        for (String name : deprecatedNames) {
            if (name.equals("strictDateOptionalTimeNanos") == false) {
                DateFormatter dateFormatter = Joda.forPattern(name);
                assertThat(dateFormatter.pattern(), equalTo(name));

                String snakeCaseName = FormatNames.forName(name).getSnakeCaseName();
                dateFormatter = Joda.forPattern(snakeCaseName);
                assertThat(dateFormatter.pattern(), equalTo(snakeCaseName));
            }
        }
    }
}
