/*
 * Copyright (C) 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package libcore.java.util;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import junit.framework.TestCase;

public class GregorianCalendarTest extends TestCase {

    private static final TimeZone LOS_ANGELES = TimeZone.getTimeZone("America/Los_Angeles");

    private static final TimeZone LONDON = TimeZone.getTimeZone("Europe/London");

    private static final int HOUR_IN_MILLIS = 3600000;

    private static final SimpleTimeZone CUSTOM_LOS_ANGELES_TIME_ZONE = new SimpleTimeZone(-28800000,
            "Custom America/Los_Angeles",
            Calendar.MARCH, 9, 0, hours(2),
            Calendar.NOVEMBER, 2, 0, hours(2),
            hours(1));

    private Locale defaultLocale;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        defaultLocale = Locale.getDefault();
        // Most tests are locale independent, but locale does affect start-of-week.
        Locale.setDefault(Locale.US);
    }

    @Override
    public void tearDown() throws Exception {
        Locale.setDefault(defaultLocale);
        super.tearDown();
    }

    // Documented a previous difference in behavior between this and the RI, see
    // https://code.google.com/p/android/issues/detail?id=61993 for more details.
    // Switching to OpenJDK has fixed that issue and so this test has been changed to reflect
    // the correct behavior.
    public void test_computeFields_dayOfWeekAndWeekOfYearSet() {
        Calendar greg = new GregorianCalendar(LOS_ANGELES, Locale.ENGLISH);

        // Ensure we use different values to the default ones.
        int differentWeekOfYear = greg.get(Calendar.WEEK_OF_YEAR) == 1 ? 2 : 1;
        int differentDayOfWeek = greg.get(Calendar.DAY_OF_WEEK) == Calendar.MONDAY
                ? Calendar.TUESDAY : Calendar.MONDAY;

        // Setting WEEK_OF_YEAR and DAY_OF_WEEK with an intervening
        // call to computeFields will work.
        greg.set(Calendar.WEEK_OF_YEAR, differentWeekOfYear);
        assertEquals(differentWeekOfYear, greg.get(Calendar.WEEK_OF_YEAR));
        greg.set(Calendar.DAY_OF_WEEK, differentDayOfWeek);
        assertEquals(differentWeekOfYear, greg.get(Calendar.WEEK_OF_YEAR));

        // Setting WEEK_OF_YEAR after DAY_OF_WEEK with no intervening
        // call to computeFields will work.
        greg = new GregorianCalendar(LOS_ANGELES, Locale.ENGLISH);
        greg.set(Calendar.DAY_OF_WEEK, differentDayOfWeek);
        greg.set(Calendar.WEEK_OF_YEAR, differentWeekOfYear);
        assertEquals(differentWeekOfYear, greg.get(Calendar.WEEK_OF_YEAR));
        assertEquals(differentDayOfWeek, greg.get(Calendar.DAY_OF_WEEK));

        // Setting DAY_OF_WEEK after WEEK_OF_YEAR with no intervening
        // call to computeFields will work.
        greg = new GregorianCalendar(LOS_ANGELES, Locale.ENGLISH);
        greg.set(Calendar.WEEK_OF_YEAR, differentWeekOfYear);
        greg.set(Calendar.DAY_OF_WEEK, differentDayOfWeek);
        assertEquals(differentWeekOfYear, greg.get(Calendar.WEEK_OF_YEAR));
        assertEquals(differentDayOfWeek, greg.get(Calendar.DAY_OF_WEEK));
    }

    /**
     * Specialized tests for those fields affected by GregorianCalendar cut over date.
     *
     * <p>Expands on a regression test created for harmony-2947.
     */
    public void test_fieldsAffectedByGregorianCutOver() {

        Date date = new Date(Date.parse("Jan 1 00:00:01 GMT 2000"));
        assertEquals(946684801000L, date.getTime());

        GregorianCalendar gc;

        // Test in America/Los_Angeles
        gc = new GregorianCalendar(LOS_ANGELES, Locale.ENGLISH);
        gc.setGregorianChange(date);
        gc.setTime(date);

        // Check the date to ensure that it is 18th Dec 1999. The reason this is not 1st Jan 2000
        // is that the offset for Los Angeles is GMT-08:00. setGregorianChange() is interpreted as
        // a wall time, not an instant. So, the instant that corresponds to
        // "1st Jan 2000 00:00:01 GMT" is 8 hours before the Julian/Gregorian switch in LA. The day
        // before 1st Jan 2000 (Gregorian Calendar) would be 18th Dec 1999 in the Julian calendar.
        // The reason it is not the 31st Dec 1999 is simply a result of the discontinuity that
        // occurred when switching calendars. That happened for real when the calendars were
        // switched in 1582.
        //
        // A different year explains why we get very different results for the methods being tested.
        assertEquals(1999, gc.get(Calendar.YEAR));
        assertEquals(Calendar.DECEMBER, gc.get(Calendar.MONTH));
        assertEquals(18, gc.get(Calendar.DAY_OF_MONTH));

        assertEquals(50, gc.getActualMaximum(Calendar.WEEK_OF_YEAR));
        assertEquals(50, gc.getLeastMaximum(Calendar.WEEK_OF_YEAR));
        assertEquals(3, gc.getActualMaximum(Calendar.WEEK_OF_MONTH));
        assertEquals(3, gc.getLeastMaximum(Calendar.WEEK_OF_MONTH));
        assertEquals(18, gc.getActualMaximum(Calendar.DAY_OF_MONTH));
        assertEquals(18, gc.getLeastMaximum(Calendar.DAY_OF_MONTH));
        assertEquals(352, gc.getActualMaximum(Calendar.DAY_OF_YEAR));
        assertEquals(352, gc.getLeastMaximum(Calendar.DAY_OF_YEAR));
        assertEquals(3, gc.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH));
        assertEquals(3, gc.getLeastMaximum(Calendar.DAY_OF_WEEK_IN_MONTH));

        // Test in Europe/London
        gc = new GregorianCalendar(LONDON, Locale.ENGLISH);
        gc.setGregorianChange(date);
        gc.setTime(date);

        // Check the date is actually 1st Jan 2000.
        assertEquals(2000, gc.get(Calendar.YEAR));
        assertEquals(Calendar.JANUARY, gc.get(Calendar.MONTH));
        assertEquals(1, gc.get(Calendar.DAY_OF_MONTH));

        assertEquals(53, gc.getActualMaximum(Calendar.WEEK_OF_YEAR));
        assertEquals(52, gc.getLeastMaximum(Calendar.WEEK_OF_YEAR));
        assertEquals(5, gc.getActualMaximum(Calendar.WEEK_OF_MONTH));
        assertEquals(4, gc.getLeastMaximum(Calendar.WEEK_OF_MONTH));
        assertEquals(31, gc.getActualMaximum(Calendar.DAY_OF_MONTH));
        assertEquals(28, gc.getLeastMaximum(Calendar.DAY_OF_MONTH));
        assertEquals(366, gc.getActualMaximum(Calendar.DAY_OF_YEAR));
        assertEquals(365, gc.getLeastMaximum(Calendar.DAY_OF_YEAR));
        assertEquals(5, gc.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH));
        assertEquals(4, gc.getLeastMaximum(Calendar.DAY_OF_WEEK_IN_MONTH));
    }

    public void test_computeTime_enteringDst_TimeZone_LosAngeles_2014() {
        TimeZone timeZone = TimeZone.getTimeZone("America/Los_Angeles");
        checkDstLosAngeles2014(timeZone);
    }

    /**
     * This test will fail in the RI.
     *
     * <p>The AOSP behavior is different for backwards compatibility with previous versions of
     * Android.
     *
     * <p>Search in this file for 'OpenJDK Failure' to see more details.
     */
    public void test_computeTime_enteringDst_DelegatingTimeZone_LosAngeles_2014() {
        TimeZone timeZone = TimeZone.getTimeZone("America/Los_Angeles");
        timeZone = new DelegatingTimeZone(timeZone);
        checkDstLosAngeles2014(timeZone);
    }

    /**
     * This test will fail in the RI.
     *
     * <p>The AOSP behavior is different for backwards compatibility with previous versions of
     * Android.
     *
     * <p>Search in this file for 'OpenJDK Failure' to see more details.
     */
    public void test_computeTime_enteringDst_SimpleTimeZone_LosAngeles_2014() {
        checkDstLosAngeles2014(CUSTOM_LOS_ANGELES_TIME_ZONE);
    }

    public void test_computeTime_enteringDst() {
        // Get the DST entry time with a ZoneInfo implementation of TimeZone.
        TimeZone zoneInfo = TimeZone.getTimeZone("America/Los_Angeles");
        long zoneInfoTime = getDstLosAngeles2014(zoneInfo);

        // Check that the time is correct.
        assertTrue(zoneInfo.inDaylightTime(new Date(zoneInfoTime)));
        assertFalse(zoneInfo.inDaylightTime(new Date(zoneInfoTime - 1)));

        // Get the DST entry time with a SimpleTimeZone implementation of TimeZone.
        SimpleTimeZone simpleTimeZone = new SimpleTimeZone(-28800000,
                "Custom America/Los_Angeles",
                Calendar.MARCH, 9, 0, 7200000,
                Calendar.NOVEMBER, 2, 0, 7200000,
                3600000);
        long simpleTimeZoneTime = getDstLosAngeles2014(simpleTimeZone);

    }

    public void test_isWeekDateSupported() {
        assertTrue(new GregorianCalendar().isWeekDateSupported());
    }

    public void test_setWeekDate() {
        GregorianCalendar cal = new GregorianCalendar();

        // When first week should have at least 4 days
        cal.setMinimalDaysInFirstWeek(4);
        cal.setWeekDate(2016, 13, Calendar.TUESDAY);
        assertEquals(Calendar.TUESDAY, cal.get(Calendar.DAY_OF_WEEK));
        assertEquals(29, cal.get(Calendar.DAY_OF_MONTH));

        // When first week can have single day
        cal.setMinimalDaysInFirstWeek(1);
        cal.setWeekDate(2016, 13, Calendar.TUESDAY);
        assertEquals(Calendar.TUESDAY, cal.get(Calendar.DAY_OF_WEEK));
        assertEquals(22, cal.get(Calendar.DAY_OF_MONTH));

        try {
            cal.setWeekDate(2016, 13, Calendar.SATURDAY + 1);
            fail();
        } catch (IllegalArgumentException expected) {
        }

        try {
            cal.setWeekDate(2016, 13, Calendar.SUNDAY - 1);
            fail();
        } catch (IllegalArgumentException expected) {
        }

        // in non-lenient mode
        cal.setLenient(false);
        try {
            cal.setWeekDate(2016, 60, Calendar.SATURDAY);
            fail();
        } catch (IllegalArgumentException expected) {}

        try {
            cal.setWeekDate(-1, 60, Calendar.SATURDAY);
            fail();
        } catch (IllegalArgumentException expected) {}
    }

    public void test_getWeekYear() {
        GregorianCalendar cal = new GregorianCalendar();

        cal.set(2016, Calendar.MARCH, 29);
        assertEquals(2016, cal.getWeekYear());

        // With minimal days in first week is set to 4
        cal.setMinimalDaysInFirstWeek(4);
        cal.set(2016, Calendar.JANUARY, 1);
        assertEquals(2015, cal.getWeekYear());
        cal.set(2015, Calendar.DECEMBER, 31);
        assertEquals(2015, cal.getWeekYear());

        // With minimal days in first week is set to 1
        cal.setMinimalDaysInFirstWeek(1);
        cal.set(2016, Calendar.JANUARY, 1);
        assertEquals(2016, cal.getWeekYear());
        cal.set(2015, Calendar.DECEMBER, 31);
        assertEquals(2016, cal.getWeekYear());
    }

    public void test_getWeeksInWeekYear() {
        GregorianCalendar cal = new GregorianCalendar();

        // With minimal days in first week is set to 1
        cal.setMinimalDaysInFirstWeek(1);
        cal.set(2016, Calendar.JANUARY, 10);
        assertEquals(53, cal.getWeeksInWeekYear());

        // With minimal days in first week is set to 4
        cal.setMinimalDaysInFirstWeek(4);
        cal.set(2016, Calendar.JANUARY, 10);
        assertEquals(52, cal.getWeeksInWeekYear());
    }

    public void test_fromZonedDateTime() {
        ZonedDateTime zdt = ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");
        GregorianCalendar calendar = GregorianCalendar.from(zdt);
        TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
        assertEquals(timeZone, calendar.getTimeZone());
        assertEquals(2007, calendar.get(Calendar.YEAR));
        assertEquals(Calendar.DECEMBER, calendar.get(Calendar.MONTH));
        assertEquals(3, calendar.get(Calendar.DAY_OF_MONTH));
        assertEquals(10, calendar.get(Calendar.HOUR_OF_DAY));
        assertEquals(15, calendar.get(Calendar.MINUTE));
        assertEquals(3600 * 1000, calendar.getTimeZone().getRawOffset()); // in milliseconds
    }

    public void test_fromZonedDateTime_invalidValues() {
        ZoneId gmt = ZoneId.of("GMT");
        ZonedDateTime[] invalidValues = {
                ZonedDateTime.of(LocalDateTime.MAX, gmt),
                ZonedDateTime.ofInstant(Instant.ofEpochMilli(Long.MAX_VALUE).plusMillis(1), gmt),
                ZonedDateTime.ofInstant(Instant.ofEpochMilli(Long.MIN_VALUE).minusMillis(1), gmt),
                ZonedDateTime.of(LocalDateTime.MAX, gmt) };
        for (ZonedDateTime invalidValue : invalidValues) {
            try {
                GregorianCalendar.from(invalidValue);
                fail("GregorianCalendar.from() should have failed with " + invalidValue);
            } catch (IllegalArgumentException expected) {}
        }
    }

    public void test_toZonedDateTime() {
        TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
        GregorianCalendar calendar = new GregorianCalendar(timeZone);
        calendar.set(2007, Calendar.DECEMBER, 3, 10, 15, 30);
        calendar.set(Calendar.MILLISECOND, 0);
        ZonedDateTime zdt = calendar.toZonedDateTime();
        assertEquals(ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), zdt);
    }

    private long getDstLosAngeles2014(TimeZone timeZone) {
        GregorianCalendar cal = new GregorianCalendar(timeZone, Locale.ENGLISH);
        cal.set(Calendar.MILLISECOND, 0);
        cal.set(2014, Calendar.MARCH, 9, 2, 0, 0);

        return cal.getTimeInMillis();
    }

    private void checkDstLosAngeles2014(TimeZone timeZone) {
        Calendar cal = new GregorianCalendar(timeZone, Locale.ENGLISH);
        // Clear the milliseconds field.
        cal.set(Calendar.MILLISECOND, 0);

        String description;

        // Check milliseconds one second before the transition.
        description = "01:59:59 - March 9th 2014";
        cal.set(2014, Calendar.MARCH, 9, 1, 59, 59);
        checkMillis(cal, description, 1394359199000L);

        // Outside DST time.
        checkOutsideDst(cal, description);

        // Check milliseconds at the transition point but using an invalid wall clock
        // (02:00 - 02:59:59.999) do not actually exist.
        description = "02:00:00 - March 9th 2014";
        cal.set(2014, Calendar.MARCH, 9, 2, 0, 0);

        // OpenJDK Failure:
        //   This fails on OpenJDK when running with SimpleTimeZone (or any custom TimeZone
        //   implementation). It incorrectly calculates the time in millis to be 1394355600000.
        //   That is because GregorianCalendar treats the implementation that underpins
        //   TimeZone.getTimeZone(String) specially and the code that runs for other classes does
        //   not handle the invalid wall clock period on entry to DST properly.
        checkMillis(cal, description, 1394359200000L);

        // Invalid wall clock but treated as being inside DST time.
        checkInsideDst(cal, description);

        // Check milliseconds at the first valid wall clock time after transition, 03:00 - should
        // be treated the same as 02:00.
        description = "03:00:00 - March 9th 2014";
        cal.set(2014, Calendar.MARCH, 9, 3, 0, 0);
        checkMillis(cal, description, 1394359200000L);

        // Valid wall clock treated as being inside DST time.
        checkInsideDst(cal, description);

        // Check milliseconds at the last invalid wall clock time, 02:59:59.999.
        description = "02:59:59.999 - March 9th 2014";
        cal.set(2014, Calendar.MARCH, 9, 2, 59, 59);
        cal.set(Calendar.MILLISECOND, 999);
        checkMillis(cal, description, 1394362799999L);

        // Invalid wall clock but treated as being inside DST time.
        checkInsideDst(cal, description);

        // Check milliseconds at 03:59:59.999 - should be treated the same as 02:59:59.999
        description = "03:59:59.999 - March 9th 2014";
        cal.set(2014, Calendar.MARCH, 9, 3, 59, 59);
        cal.set(Calendar.MILLISECOND, 999);
        checkMillis(cal, description, 1394362799999L);

        // Valid wall clock treated as being inside DST time.
        checkInsideDst(cal, description);
    }

    private void checkMillis(Calendar cal, String description, long expectedMillis) {
        assertEquals("Incorrect millis: " + description, expectedMillis, cal.getTimeInMillis());
    }

    private void checkOutsideDst(Calendar cal, String description) {
        TimeZone timeZone = cal.getTimeZone();
        checkOutsideDst(cal, description, timeZone.getRawOffset());
    }

    private void checkOutsideDst(Calendar cal, String description, int expectedZoneOffset) {
        checkDstFields(cal, description, expectedZoneOffset, 0);
    }

    private void checkInsideDst(Calendar cal, String description) {
        TimeZone timeZone = cal.getTimeZone();
        checkDstFields(cal, description, timeZone.getRawOffset(), timeZone.getDSTSavings());
    }

    private void checkDstFields(Calendar cal, String description, int expectedZoneOffset, int expectedDstOffset) {
        assertEquals("Incorrect ZONE_OFFSET: " + description, expectedZoneOffset, cal.get(Calendar.ZONE_OFFSET));
        assertEquals("Incorrect DST_OFFSET: " + description, expectedDstOffset, cal.get(Calendar.DST_OFFSET));
    }

    /**
     * A custom {@link TimeZone} implementation.
     *
     * <p>Used to show the behavior of {@link GregorianCalendar} when provided with a custom
     * implementation of {@link TimeZone}, i.e. one that is unknown to the runtime,
     */
    private static class DelegatingTimeZone extends TimeZone {

        private final TimeZone timeZone;

        public DelegatingTimeZone(TimeZone timeZone) {
            this.timeZone = timeZone;
        }

        @Override
        public int getOffset(int era, int year, int month, int day, int dayOfWeek,
                int milliseconds) {
            return timeZone.getOffset(era, year, month, day, dayOfWeek, milliseconds);
        }

        @Override
        public int getOffset(long date) {
            return timeZone.getOffset(date);
        }

        @Override
        public void setRawOffset(int offsetMillis) {
            timeZone.setRawOffset(offsetMillis);
        }

        @Override
        public int getRawOffset() {
            return timeZone.getRawOffset();
        }

        @Override
        public String getID() {
            return timeZone.getID();
        }

        @Override
        public void setID(String ID) {
            timeZone.setID(ID);
        }

        @Override
        public String getDisplayName(boolean daylightTime, int style, Locale locale) {
            return timeZone.getDisplayName(daylightTime, style, locale);
        }

        @Override
        public int getDSTSavings() {
            return timeZone.getDSTSavings();
        }

        @Override
        public boolean useDaylightTime() {
            return timeZone.useDaylightTime();
        }

        @Override
        public boolean observesDaylightTime() {
            return timeZone.observesDaylightTime();
        }

        @Override
        public boolean inDaylightTime(Date date) {
            return timeZone.inDaylightTime(date);
        }

        @Override
        public boolean hasSameRules(TimeZone other) {
            return timeZone.hasSameRules(other);
        }
    }

    private static int hours(int count) {
        return HOUR_IN_MILLIS * count;
    }
}
