/*
 * Copyright (c) 2010-2026 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.astro.internal.calc;

import static org.junit.jupiter.api.Assertions.*;

import java.time.Instant;
import java.time.InstantSource;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.openhab.binding.astro.internal.model.Position;
import org.openhab.binding.astro.internal.model.Range;
import org.openhab.binding.astro.internal.model.Sun;
import org.openhab.binding.astro.internal.model.SunPhase;
import org.openhab.binding.astro.internal.util.DateTimeUtils;

/***
 * Specific unit tests to check if {@link SunCalc} generates correct data for
 * Amsterdam city on 27 February 2019. In particular the following cases are
 * covered:
 * <ul>
 * <li>checks if generated data are the same (with some accuracy) as produced by
 * heavens-above.com</li>
 * <li>checks if the generated {@link Sun#getAllRanges()} are consistent with
 * each other</li>
 * </ul>
 *
 * @author Witold Markowski - Initial contribution
 * @see <a href="https://github.com/openhab/openhab-addons/issues/5006">[astro]
 *      Sun Phase returns UNDEF</a>
 * @see <a href="https://www.heavens-above.com/sun.aspx">Heavens Above Sun</a>
 */
@NonNullByDefault
public class SunCalcTest {

    private static final TimeZone AMSTERDAM_TIME_ZONE = TimeZone.getTimeZone("Europe/Amsterdam");
    private static final Calendar FEB_27_2019 = SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 1, 0,
            AMSTERDAM_TIME_ZONE);
    private static final double AMSTERDAM_LATITUDE = 52.367607;
    private static final double AMSTERDAM_LONGITUDE = 4.8978293;
    private static final double AMSTERDAM_ALTITUDE = 0.0;
    private static final int ACCURACY_IN_MILLIS = 3 * 60 * 1000;

    private @Nullable SunCalc sunCalc;

    @BeforeEach
    public void init() {
        sunCalc = new SunCalc(InstantSource.fixed(Instant.ofEpochMilli(1645671600000L)));
    }

    @Test
    public void testGetSunInfoForOldDate() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        assertNotNull(sun.getRange(SunPhase.NIGHT));

        assertNotNull(sun.getRange(SunPhase.ASTRO_DAWN));
        assertNotNull(sun.getRange(SunPhase.NAUTIC_DAWN));
        assertNotNull(sun.getRange(SunPhase.CIVIL_DAWN));

        assertNotNull(sun.getRise());

        assertNotNull(sun.getRange(SunPhase.DAYLIGHT));
        assertNotNull(sun.getRange(SunPhase.NOON));
        assertNotNull(sun.getSet());

        assertNotNull(sun.getRange(SunPhase.CIVIL_DUSK));
        assertNotNull(sun.getRange(SunPhase.NAUTIC_DUSK));
        assertNotNull(sun.getRange(SunPhase.ASTRO_DUSK));
        assertNotNull(sun.getRange(SunPhase.NIGHT));

        assertNotNull(sun.getRange(SunPhase.MORNING_NIGHT));
        assertNotNull(sun.getRange(SunPhase.EVENING_NIGHT));

        // for an old date the phase should also be calculated
        assertNotNull(sun.getSunPhase());
    }

    @Test
    public void testGetSunInfoForAstronomicalDawnAccuracy() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getRange(SunPhase.ASTRO_DAWN);
        assertNotNull(range);
        Calendar cal = range.getStart();
        assertNotNull(cal);
        // expected result from heavens-above.com is 27 Feb 2019 05:39 till 06:18
        assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 5, 39, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
        cal = range.getEnd();
        assertNotNull(cal);
        assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 18, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
    }

    @Test
    public void testGetSunInfoForNauticDawnAccuracy() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getRange(SunPhase.NAUTIC_DAWN);
        assertNotNull(range);
        Calendar cal = range.getStart();
        assertNotNull(cal);
        // expected result from heavens-above.com is 27 Feb 2019 06:18 till 06:58
        assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 18, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
        cal = range.getEnd();
        assertNotNull(cal);
        assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 58, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
    }

    @Test
    public void testGetSunInfoForCivilDawnAccuracy() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getRange(SunPhase.CIVIL_DAWN);
        assertNotNull(range);
        Calendar cal = range.getStart();
        assertNotNull(cal);
        // expected result from heavens-above.com is 27 Feb 2019 06:58 till 07:32
        assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 58, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
        cal = range.getEnd();
        assertNotNull(cal);
        assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 7, 32, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
    }

    @Test
    public void testGetSunInfoForRiseAccuracy() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getRise();
        assertNotNull(range);
        Calendar cal = range.getStart();
        assertNotNull(cal);
        // expected result from heavens-above.com is 27 Feb 2019 07:32
        assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 7, 32, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
    }

    @Test
    public void testGetSunInfoForSunNoonAccuracy() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getRange(SunPhase.NOON);
        assertNotNull(range);
        Calendar cal = range.getStart();
        assertNotNull(cal);
        // expected result from heavens-above.com is 27 Feb 2019 12:54
        assertEquals(
                SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 12, 54, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
    }

    @Test
    public void testGetSunInfoForSetAccuracy() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getSet();
        assertNotNull(range);
        Calendar cal = range.getStart();
        assertNotNull(cal);
        // expected result from heavens-above.com is 27 Feb 2019 18:15
        assertEquals(
                SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 15, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
    }

    @Test
    public void testGetSunInfoForCivilDuskAccuracy() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getRange(SunPhase.CIVIL_DUSK);
        assertNotNull(range);
        Calendar cal = range.getStart();
        assertNotNull(cal);
        // expected result from heavens-above.com is 27 Feb 2019 18:15 till 18:50
        assertEquals(
                SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 15, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
        cal = range.getEnd();
        assertNotNull(cal);
        assertEquals(
                SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 50, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
    }

    @Test
    public void testGetSunInfoForNauticDuskAccuracy() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getRange(SunPhase.NAUTIC_DUSK);
        assertNotNull(range);
        Calendar cal = range.getStart();
        assertNotNull(cal);
        // expected result from heavens-above.com is 27 Feb 2019 18:50 till 19:29
        assertEquals(
                SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 50, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
        cal = range.getEnd();
        assertNotNull(cal);
        assertEquals(
                SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 19, 29, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
    }

    @Test
    public void testGetSunInfoForAstronomicalDuskAccuracy() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getRange(SunPhase.ASTRO_DUSK);
        assertNotNull(range);
        Calendar cal = range.getStart();
        assertNotNull(cal);
        // expected result from heavens-above.com is 27 Feb 2019 19:29 till 20:09
        assertEquals(
                SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 19, 29, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
        cal = range.getEnd();
        assertNotNull(cal);
        assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 20, 9, AMSTERDAM_TIME_ZONE).getTimeInMillis(),
                cal.getTimeInMillis(), ACCURACY_IN_MILLIS);
    }

    @Test
    @Disabled
    public void testRangesForCoherenceBetweenNightEndAndAstroDawnStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.NIGHT);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.ASTRO_DAWN);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenMorningNightEndAndAstroDawnStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.MORNING_NIGHT);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.ASTRO_DAWN);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenAstroDownEndAndNauticDawnStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.ASTRO_DAWN);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.NAUTIC_DAWN);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenNauticDawnEndAndCivilDawnStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.NAUTIC_DAWN);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.CIVIL_DAWN);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenCivilDawnEndAndSunRiseStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.CIVIL_DAWN);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.SUN_RISE);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenSunRiseEndAndDaylightStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.SUN_RISE);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.DAYLIGHT);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenDaylightEndAndSunSetStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.DAYLIGHT);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.SUN_SET);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenSunSetEndAndCivilDuskStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.SUN_SET);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.CIVIL_DUSK);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenCivilDuskEndAndNauticDuskStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.CIVIL_DUSK);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.NAUTIC_DUSK);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenNauticDuskEndAndAstroDuskStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.NAUTIC_DUSK);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.ASTRO_DUSK);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenAstroDuskEndAndNightStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.ASTRO_DUSK);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.NIGHT);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testRangesForCoherenceBetweenAstroDuskEndAndEveningNightStart() {
        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
                AMSTERDAM_ALTITUDE, false, AMSTERDAM_TIME_ZONE, Locale.ROOT);

        Range range = sun.getAllRanges().get(SunPhase.ASTRO_DUSK);
        assertNotNull(range);
        Range range2 = sun.getAllRanges().get(SunPhase.EVENING_NIGHT);
        assertNotNull(range2);
        assertEquals(range.getEnd(), range2.getStart());
    }

    @Test
    public void testIssue7642CivilDawnEnd() {
        TimeZone tZone = TimeZone.getTimeZone("Europe/London");
        Calendar tDate = SunCalcTest.newCalendar(2020, Calendar.MAY, 13, 5, 12, tZone);

        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(tDate, 53.524695, -2.4, 0.0, true, AMSTERDAM_TIME_ZONE,
                Locale.ROOT);
        assertEquals(SunPhase.CIVIL_DAWN, sun.getSunPhase());
    }

    @Test
    public void testIssue7642SunRiseStart() {
        // SunCalc.ranges was not sorted, causing unexpected output in corner cases.
        TimeZone tZone = TimeZone.getTimeZone("Europe/London");
        Calendar tDate = SunCalcTest.newCalendar(2020, Calendar.MAY, 13, 5, 13, tZone);
        tDate.set(Calendar.SECOND, 4);

        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(tDate, 53.524695, -2.4, 0.0, true, AMSTERDAM_TIME_ZONE,
                Locale.ROOT);
        assertEquals(SunPhase.SUN_RISE, sun.getSunPhase());
    }

    @Test
    public void testIssue7642DaylightStart() {
        TimeZone tZone = TimeZone.getTimeZone("Europe/London");
        Calendar tDate = SunCalcTest.newCalendar(2020, Calendar.MAY, 13, 5, 18, tZone);

        Sun sun = Objects.requireNonNull(sunCalc).getSunInfo(tDate, 53.524695, -2.4, 0.0, true, AMSTERDAM_TIME_ZONE,
                Locale.ROOT);
        assertEquals(SunPhase.DAYLIGHT, sun.getSunPhase());
    }

    @Test
    public void testAzimuthRange() {
        for (int hour = 0; hour < 23; hour++) {
            Calendar noonCalendar = SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, hour, 0, AMSTERDAM_TIME_ZONE);
            Position position = Objects.requireNonNull(sunCalc).getPosition(noonCalendar, AMSTERDAM_LATITUDE,
                    AMSTERDAM_LONGITUDE);
            double azimuth = position.getAzimuthAsDouble();
            assertTrue(azimuth >= 0 && azimuth < 360, "Azimuth should be >=0 and <360");
        }
    }

    /***
     * Constructs a <code>GregorianCalendar</code> with the given date and time set
     * for the provided time zone.
     *
     * @param year
     *            the value used to set the <code>YEAR</code> calendar field in the
     *            calendar.
     * @param month
     *            the value used to set the <code>MONTH</code> calendar field in the
     *            calendar. Month value is 0-based. e.g., 0 for January.
     * @param dayOfMonth
     *            the value used to set the <code>DAY_OF_MONTH</code> calendar field
     *            in the calendar.
     * @param hourOfDay
     *            the value used to set the <code>HOUR_OF_DAY</code> calendar field
     *            in the calendar.
     * @param minute
     *            the value used to set the <code>MINUTE</code> calendar field in
     *            the calendar.
     * @param zone
     *            the given time zone.
     * @return
     */
    private static Calendar newCalendar(int year, int month, int dayOfMonth, int hourOfDay, int minute, TimeZone zone) {
        Calendar result = new GregorianCalendar(zone, Locale.ROOT);
        result.set(year, month, dayOfMonth, hourOfDay, minute);

        return DateTimeUtils.truncateToMinute(result);
    }
}
