// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2012, International Business Machines Corporation and         *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.dev.test.calendar;
import java.util.Date;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import com.ibm.icu.text.DateFormat;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.DangiCalendar;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;

@RunWith(JUnit4.class)
public class DangiTest extends CalendarTestFmwk {
    /**
     * Test basic mapping to and from Gregorian.
     */
    @Test
    public void TestMapping() {
        final int[] DATA = {
            // (Note: months are 1-based)
            // Gregorian    Korean (Dan-gi)
            1964,  9,  4,   4297,  7,0, 28,
            1964,  9,  5,   4297,  7,0, 29,
            1964,  9,  6,   4297,  8,0,  1,
            1964,  9,  7,   4297,  8,0,  2,
            1961, 12, 25,   4294, 11,0, 18,
            1999,  6,  4,   4332,  4,0, 21,

            1990,  5, 23,   4323,  4,0, 29,
            1990,  5, 24,   4323,  5,0,  1,
            1990,  6, 22,   4323,  5,0, 30,
            1990,  6, 23,   4323,  5,1,  1,
            1990,  7, 20,   4323,  5,1, 28,
            1990,  7, 21,   4323,  5,1, 29,
            1990,  7, 22,   4323,  6,0,  1,

            // Some tricky dates (where GMT+8 doesn't agree with GMT+9)
            //
            // The list is from http://www.math.snu.ac.kr/~kye/others/lunar.html ('kye ref').
            // However, for some dates disagree with the above reference so KASI's
            // calculation was cross-referenced:
            //  http://astro.kasi.re.kr/Life/ConvertSolarLunarForm.aspx?MenuID=115
            1880, 11,  3,   4213, 10,0,  1, // astronomer's GMT+8 / KASI disagrees with the kye ref
            1882, 12, 10,   4215, 11,0,  1,
            1883,  7, 4,    4216,  6,0,  1,
            1884,  4, 25,   4217,  4,0,  1,
            1885,  5, 14,   4218,  4,0,  1,
            1891,  1, 10,   4223, 12,0,  1,
            1893,  4, 16,   4226,  3,0,  1,
            1894,  5,  5,   4227,  4,0,  1,
            1897,  7, 29,   4230,  7,0,  1, // astronomer's GMT+8 disagrees with all other ref (looks like our astronomer's error, see ad hoc fix at ChineseCalendar::getTimezoneOffset)
            1903, 10, 20,   4236,  9,0,  1,
            1904,  1, 17,   4236, 12,0,  1,
            1904, 11,  7,   4237, 10,0,  1,
            1905,  5,  4,   4238,  4,0,  1,
            1907,  7, 10,   4240,  6,0,  1,
            1908,  4, 30,   4241,  4,0,  1,
            1908,  9, 25,   4241,  9,0,  1,
            1909,  9, 14,   4242,  8,0,  1,
            1911, 12, 20,   4244, 11,0,  1,
            1976, 11, 22,   4309, 10,0,  1,
        };

        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
        StringBuilder buf = new StringBuilder();

        logln("Gregorian -> Korean Lunar (Dangi)");

        Calendar grego = Calendar.getInstance();
        grego.clear();
        for (int i = 0; i < DATA.length;) {
            grego.set(DATA[i++], DATA[i++] - 1, DATA[i++]);
            Date date = grego.getTime();
            cal.setTime(date);
            int y = cal.get(Calendar.EXTENDED_YEAR);
            int m = cal.get(Calendar.MONTH) + 1; // 0-based -> 1-based
            int L = cal.get(Calendar.IS_LEAP_MONTH);
            int d = cal.get(Calendar.DAY_OF_MONTH);
            int yE = DATA[i++]; // Expected y, m, isLeapMonth, d
            int mE = DATA[i++]; // 1-based
            int LE = DATA[i++];
            int dE = DATA[i++];
            buf.setLength(0);
            buf.append(date + " -> ");
            buf.append(y + "/" + m + (L == 1 ? "(leap)" : "") + "/" + d);
            if (y == yE && m == mE && L == LE && d == dE) {
                logln("OK: " + buf.toString());
            } else {
                errln("Fail: " + buf.toString() + ", expected " + yE + "/" + mE + (LE == 1 ? "(leap)" : "") + "/" + dE);
            }
        }

        logln("Korean Lunar (Dangi) -> Gregorian");
        for (int i = 0; i < DATA.length;) {
            grego.set(DATA[i++], DATA[i++] - 1, DATA[i++]);
            Date dexp = grego.getTime();
            int cyear = DATA[i++];
            int cmonth = DATA[i++];
            int cisleapmonth = DATA[i++];
            int cdayofmonth = DATA[i++];
            cal.clear();
            cal.set(Calendar.EXTENDED_YEAR, cyear);
            cal.set(Calendar.MONTH, cmonth - 1);
            cal.set(Calendar.IS_LEAP_MONTH, cisleapmonth);
            cal.set(Calendar.DAY_OF_MONTH, cdayofmonth);
            Date date = cal.getTime();
            buf.setLength(0);
            buf.append(cyear + "/" + cmonth + (cisleapmonth == 1 ? "(leap)" : "") + "/" + cdayofmonth);
            buf.append(" -> " + date);
            if (date.equals(dexp)) {
                logln("OK: " + buf.toString());
            } else {
                errln("Fail: " + buf.toString() + ", expected " + dexp);
            }
        }
    }

    /**
     * Make sure no Gregorian dates map to Chinese 1-based day of
     * month zero.  This was a problem with some of the astronomical
     * new moon determinations.
     */
    @Test
    public void TestZeroDOM() {
        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
        GregorianCalendar greg = new GregorianCalendar(1989, Calendar.SEPTEMBER, 1);
        logln("Start: " + greg.getTime());
        for (int i=0; i<1000; ++i) {
            cal.setTimeInMillis(greg.getTimeInMillis());
            if (cal.get(Calendar.DAY_OF_MONTH) == 0) {
                errln("Fail: " + greg.getTime() + " -> " +
                      cal.get(Calendar.YEAR) + "/" +
                      cal.get(Calendar.MONTH) +
                      (cal.get(Calendar.IS_LEAP_MONTH)==1?"(leap)":"") +
                      "/" + cal.get(Calendar.DAY_OF_MONTH));
            }
            greg.add(Calendar.DAY_OF_YEAR, 1);
        }
        logln("End: " + greg.getTime());
    }

    /**
     * Test minimum and maximum functions.
     */
    @Test
    public void TestLimits() {
        // The number of days and the start date can be adjusted
        // arbitrarily to either speed up the test or make it more
        // thorough, but try to test at least a full year, preferably a
        // full non-leap and a full leap year.

        // Final parameter is either number of days, if > 0, or test
        // duration in seconds, if < 0.
        Calendar tempcal = Calendar.getInstance();
        tempcal.clear();
        tempcal.set(1989, Calendar.NOVEMBER, 1);
        Calendar dangi = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
        doLimitsTest(dangi, null, tempcal.getTime());
        doTheoreticalLimitsTest(dangi, true);
    }

    /**
     * Make sure IS_LEAP_MONTH participates in field resolution.
     */
    @Test
    public void TestResolution() {
        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
        DateFormat fmt = DateFormat.getDateInstance(cal, DateFormat.DEFAULT);

        // May 22 4334 = y4334 m4 d30 doy119
        // May 23 4334 = y4334 m4* d1 doy120

        final int THE_YEAR = 4334;
        final int END = -1;

        int[] DATA = {
            // Format:
            // (field, value)+, END, exp.month, exp.isLeapMonth, exp.DOM
            // Note: exp.month is ONE-BASED

            // If we set DAY_OF_YEAR only, that should be used
            Calendar.DAY_OF_YEAR, 1,
            END,
            1,0,1, // Expect 1-1

            // If we set MONTH only, that should be used
            Calendar.IS_LEAP_MONTH, 1,
            Calendar.DAY_OF_MONTH, 1,
            Calendar.MONTH, 3,
            END,
            4,1,1, // Expect 4*-1

            // If we set the DOY last, that should take precedence
            Calendar.MONTH, 1, // Should ignore
            Calendar.IS_LEAP_MONTH, 1, // Should ignore
            Calendar.DAY_OF_MONTH, 1, // Should ignore
            Calendar.DAY_OF_YEAR, 121,
            END,
            4,1,2, // Expect 4*-2

            // If we set IS_LEAP_MONTH last, that should take precedence
            Calendar.MONTH, 3,
            Calendar.DAY_OF_MONTH, 1,
            Calendar.DAY_OF_YEAR, 5, // Should ignore
            Calendar.IS_LEAP_MONTH, 1,
            END,
            4,1,1, // Expect 4*-1
        };

        StringBuilder buf = new StringBuilder();
        for (int i=0; i<DATA.length; ) {
            cal.clear();
            cal.set(Calendar.EXTENDED_YEAR, THE_YEAR);
            buf.setLength(0);
            buf.append("EXTENDED_YEAR=" + THE_YEAR);
            while (DATA[i] != END) {
                cal.set(DATA[i++], DATA[i++]);
                buf.append(" " + fieldName(DATA[i-2]) + "=" + DATA[i-1]);
            }
            ++i; // Skip over END mark
            int expMonth = DATA[i++]-1;
            int expIsLeapMonth = DATA[i++];
            int expDOM = DATA[i++];
            int month = cal.get(Calendar.MONTH);
            int isLeapMonth = cal.get(Calendar.IS_LEAP_MONTH);
            int dom = cal.get(Calendar.DAY_OF_MONTH);
            if (expMonth == month && expIsLeapMonth == isLeapMonth &&
                dom == expDOM) {
                logln("OK: " + buf + " => " + fmt.format(cal.getTime()));
            } else {
                String s = fmt.format(cal.getTime());
                cal.clear();
                cal.set(Calendar.EXTENDED_YEAR, THE_YEAR);
                cal.set(Calendar.MONTH, expMonth);
                cal.set(Calendar.IS_LEAP_MONTH, expIsLeapMonth);
                cal.set(Calendar.DAY_OF_MONTH, expDOM);
                errln("Fail: " + buf + " => " + s +
                      "=" + (month+1) + "," + isLeapMonth + "," + dom +
                      ", expected " + fmt.format(cal.getTime()) +
                      "=" + (expMonth+1) + "," + expIsLeapMonth + "," + expDOM);
            }
        }
    }

    /**
     * Test the behavior of fields that are out of range.
     */
    @Test
    public void TestOutOfRange() {
        int[] DATA = new int[] {
            // Input       Output
            4334, 13,  1,   4335,  1,  1,
            4334, 18,  1,   4335,  6,  1,
            4335,  0,  1,   4334, 12,  1,
            4335, -6,  1,   4334,  6,  1,
            4334,  1, 32,   4334,  2,  2, // 1-4334 has 30 days
            4334,  2, -1,   4334,  1, 29,
        };
        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
        for (int i = 0; i < DATA.length;) {
            int y1 = DATA[i++];
            int m1 = DATA[i++] - 1;
            int d1 = DATA[i++];
            int y2 = DATA[i++];
            int m2 = DATA[i++] - 1;
            int d2 = DATA[i++];
            cal.clear();
            cal.set(Calendar.EXTENDED_YEAR, y1);
            cal.set(MONTH, m1);
            cal.set(DATE, d1);
            int y = cal.get(Calendar.EXTENDED_YEAR);
            int m = cal.get(MONTH);
            int d = cal.get(DATE);
            if (y != y2 || m != m2 || d != d2) {
                errln("Fail: " + y1 + "/" + (m1 + 1) + "/" + d1 + " resolves to " + y + "/" + (m + 1) + "/" + d
                        + ", expected " + y2 + "/" + (m2 + 1) + "/" + d2);
            } else if (isVerbose()) {
                logln("OK: " + y1 + "/" + (m1 + 1) + "/" + d1 + " resolves to " + y + "/" + (m + 1) + "/" + d);
            }
        }
    }

    /**
     * Test the behavior of KoreanLunarCalendar.add().  The only real
     * nastiness with roll is the MONTH field around leap months.
     */
    @Test
    public void TestAdd() {
        int[][] tests = new int[][] {
            // MONTHS ARE 1-BASED HERE
            // input               add           output
            // year  mon    day    field amount  year  mon    day
            {  4338,   3,0,  15,   MONTH,   3,   4338,   6,0,  15 }, // normal
            {  4335,  12,0,  15,   MONTH,   1,   4336,   1,0,  15 }, // across year
            {  4336,   1,0,  15,   MONTH,  -1,   4335,  12,0,  15 }, // across year
            {  4334,   3,0,  15,   MONTH,   3,   4334,   5,0,  15 }, // 4=leap
            {  4334,   3,0,  15,   MONTH,   2,   4334,   4,1,  15 }, // 4=leap
            {  4334,   4,0,  15,   MONTH,   1,   4334,   4,1,  15 }, // 4=leap
            {  4334,   4,1,  15,   MONTH,   1,   4334,   5,0,  15 }, // 4=leap
            {  4334,   3,0,  30,   MONTH,   2,   4334,   4,1,  29 }, // dom should pin
            {  4334,   3,0,  30,   MONTH,   3,   4334,   5,0,  30 }, // no dom pin
            {  4334,   3,0,  30,   MONTH,   4,   4334,   6,0,  29 }, // dom should pin
        };

        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
        doRollAddDangi(ADD, cal, tests);
    }

    /**
     * Test the behavior of KoreanLunarCalendar.roll().  The only real
     * nastiness with roll is the MONTH field around leap months.
     */
    @Test
    public void TestRoll() {
        int[][] tests = new int[][] {
            // MONTHS ARE 1-BASED HERE
            // input               add           output
            // year  mon    day    field amount  year  mon    day
            {  4338,   3,0,  15,   MONTH,   3,   4338,   6,0,  15 }, // normal
            {  4338,   3,0,  15,   MONTH,  11,   4338,   2,0,  15 }, // normal
            {  4335,  12,0,  15,   MONTH,   1,   4335,   1,0,  15 }, // across year
            {  4336,   1,0,  15,   MONTH,  -1,   4336,  12,0,  15 }, // across year
            {  4334,   3,0,  15,   MONTH,   3,   4334,   5,0,  15 }, // 4=leap
            {  4334,   3,0,  15,   MONTH,  16,   4334,   5,0,  15 }, // 4=leap
            {  4334,   3,0,  15,   MONTH,   2,   4334,   4,1,  15 }, // 4=leap
            {  4334,   3,0,  15,   MONTH,  28,   4334,   4,1,  15 }, // 4=leap
            {  4334,   4,0,  15,   MONTH,   1,   4334,   4,1,  15 }, // 4=leap
            {  4334,   4,0,  15,   MONTH, -12,   4334,   4,1,  15 }, // 4=leap
            {  4334,   4,1,  15,   MONTH,   1,   4334,   5,0,  15 }, // 4=leap
            {  4334,   4,1,  15,   MONTH, -25,   4334,   5,0,  15 }, // 4=leap
            {  4334,   3,0,  30,   MONTH,   2,   4334,   4,1,  29 }, // dom should pin
            {  4334,   3,0,  30,   MONTH,  15,   4334,   4,1,  29 }, // dom should pin
            {  4334,   3,0,  30,   MONTH,  16,   4334,   5,0,  30 }, // no dom pin
            {  4334,   3,0,  30,   MONTH,  -9,   4334,   6,0,  29 }, // dom should pin
        };

        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
        doRollAddDangi(ROLL, cal, tests);
    }

    void doRollAddDangi(boolean roll, Calendar cal, int[][] tests) {
        String name = roll ? "rolling" : "adding";

        for (int i = 0; i < tests.length; i++) {
            int[] test = tests[i];

            cal.clear();
            cal.set(Calendar.EXTENDED_YEAR, test[0]);
            cal.set(Calendar.MONTH, test[1] - 1);
            cal.set(Calendar.IS_LEAP_MONTH, test[2]);
            cal.set(Calendar.DAY_OF_MONTH, test[3]);
            if (roll) {
                cal.roll(test[4], test[5]);
            } else {
                cal.add(test[4], test[5]);
            }
            if (cal.get(Calendar.EXTENDED_YEAR) != test[6] || cal.get(MONTH) != (test[7] - 1)
                    || cal.get(Calendar.IS_LEAP_MONTH) != test[8] || cal.get(DATE) != test[9]) {
                errln("Fail: " + name + " " + ymdToString(test[0], test[1] - 1, test[2], test[3]) + " "
                        + fieldName(test[4]) + " by " + test[5] + ": expected "
                        + ymdToString(test[6], test[7] - 1, test[8], test[9]) + ", got " + ymdToString(cal));
            } else if (isVerbose()) {
                logln("OK: " + name + " " + ymdToString(test[0], test[1] - 1, test[2], test[3]) + " "
                        + fieldName(test[4]) + " by " + test[5] + ": got " + ymdToString(cal));
            }
        }
    }

    /**
     * Convert year,month,day values to the form "year/month/day".
     * On input the month value is zero-based, but in the result string it is one-based.
     */
    static public String ymdToString(int year, int month, int isLeapMonth, int day) {
        return "" + year + "/" + (month + 1) + ((isLeapMonth != 0) ? "(leap)" : "") + "/" + day;
    }

    @Test
    public void TestCoverage() {
        // DangiCalendar()
        // DangiCalendar(Date)
        // DangiCalendar(TimeZone, ULocale)
        Date d = new Date();

        DangiCalendar cal1 = new DangiCalendar();
        cal1.setTime(d);

        DangiCalendar cal2 = new DangiCalendar(d);

        DangiCalendar cal3 = new DangiCalendar(TimeZone.getDefault(), ULocale.getDefault());
        cal3.setTime(d);

        assertEquals("DangiCalendar() and DangiCalendar(Date)", cal1, cal2);
        assertEquals("DangiCalendar() and DangiCalendar(TimeZone,ULocale)", cal1, cal3);

        // String getType()
        String type = cal1.getType();
        assertEquals("getType()", "dangi", type);
    }

    @Test
    public void TestInitWithCurrentTime() {
        // If the chinese calendar current millis isn't called, the default year is wrong.
        // this test is assuming the 'year' is the current cycle
        // so when we cross a cycle boundary, the target will need to change
        // that shouldn't be for awhile yet...

        Calendar cc = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
        cc.set(Calendar.EXTENDED_YEAR, 4338);
        cc.set(Calendar.MONTH, 0);
         // need to set leap month flag off, otherwise, the test case always fails when
         // current time is in a leap month
        cc.set(Calendar.IS_LEAP_MONTH, 0);
        cc.set(Calendar.DATE, 19);
        cc.set(Calendar.HOUR_OF_DAY, 0);
        cc.set(Calendar.MINUTE, 0);
        cc.set(Calendar.SECOND, 0);
        cc.set(Calendar.MILLISECOND, 0);

        cc.add(Calendar.DATE, 1);

        Calendar cal = new GregorianCalendar(2005, Calendar.FEBRUARY, 28);
        Date target = cal.getTime();
        Date result = cc.getTime();

        assertEquals("chinese and gregorian date should match", target, result);
    }
}
