package org.xbib.files.ftp.test.parser;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.xbib.files.ftp.FTPClientConfig;
import java.text.Format;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import org.xbib.files.ftp.parser.FTPTimestampParser;
import org.xbib.files.ftp.parser.FTPTimestampParserImpl;

/**
 * Test the FTPTimestampParser class.
 */
public class FTPTimestampParserImplTest {

    private static final int TWO_HOURS_OF_MILLISECONDS = 2 * 60 * 60 * 1000;

    @Test
    public void testParseTimestamp() {
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.HOUR_OF_DAY, 1);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        Date anHourFromNow = cal.getTime();
        FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
        SimpleDateFormat sdf = new SimpleDateFormat(parser.getRecentDateFormatString());
        String fmtTime = sdf.format(anHourFromNow);
        try {
            Calendar parsed = parser.parseTimestamp(fmtTime);
            // since the timestamp is ahead of now (by one hour),
            // this must mean the file's date refers to a year ago.
            assertEquals(1, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR), "test.roll.back.year");
        } catch (ParseException e) {
            fail("Unable to parse");
        }
    }

    @Test
    public void testParseTimestampWithSlop() {
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        Calendar caltemp = (Calendar) cal.clone();
        caltemp.add(Calendar.HOUR_OF_DAY, 1);
        Date anHourFromNow = caltemp.getTime();
        caltemp.add(Calendar.DAY_OF_MONTH, 1);
        Date anHourFromNowTomorrow = caltemp.getTime();
        FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
        // set the "slop" factor on
        parser.setLenientFutureDates(true);
        SimpleDateFormat sdf = new SimpleDateFormat(parser.getRecentDateFormatString());
        try {
            String fmtTime = sdf.format(anHourFromNow);
            Calendar parsed = parser.parseTimestamp(fmtTime);
            // the timestamp is ahead of now (by one hour), but
            // that's within range of the "slop" factor.
            // so the date is still considered this year.
            assertEquals(0, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR), "test.slop.no.roll.back.year");
            // add a day to get beyond the range of the slop factor.
            // this must mean the file's date refers to a year ago.
            fmtTime = sdf.format(anHourFromNowTomorrow);
            parsed = parser.parseTimestamp(fmtTime);
            assertEquals(1, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR), "test.slop.roll.back.year");
        } catch (ParseException e) {
            fail("Unable to parse");
        }
    }

    @Test
    public void testNET444() throws Exception {
        FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
        parser.setLenientFutureDates(true);
        SimpleDateFormat sdf = new SimpleDateFormat(parser.getRecentDateFormatString());
        GregorianCalendar now = new GregorianCalendar(2012, Calendar.FEBRUARY, 28, 12, 0);
        GregorianCalendar nowplus1 = new GregorianCalendar(2012, Calendar.FEBRUARY, 28, 13, 0);
        // Create a suitable short date
        String future1 = sdf.format(nowplus1.getTime());
        Calendar parsed1 = parser.parseTimestamp(future1, now);
        assertEquals(nowplus1.get(Calendar.YEAR), parsed1.get(Calendar.YEAR));
        GregorianCalendar nowplus25 = new GregorianCalendar(2012, Calendar.FEBRUARY, 29, 13, 0);
        // Create a suitable short date
        String future25 = sdf.format(nowplus25.getTime());
        Calendar parsed25 = parser.parseTimestamp(future25, now);
        assertEquals(nowplus25.get(Calendar.YEAR) - 1, parsed25.get(Calendar.YEAR));
    }

    @Test
    public void testParseTimestampAcrossTimeZones() {
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        cal.add(Calendar.HOUR_OF_DAY, 1);
        Date anHourFromNow = cal.getTime();
        cal.add(Calendar.HOUR_OF_DAY, 2);
        Date threeHoursFromNow = cal.getTime();
        cal.add(Calendar.HOUR_OF_DAY, -2);
        FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
        // assume we are FTPing a server in Chicago, two hours ahead of
        // L. A.
        FTPClientConfig config =
                new FTPClientConfig(FTPClientConfig.SYST_UNIX);
        config.setDefaultDateFormatStr(FTPTimestampParser.DEFAULT_DATE_FORMAT);
        config.setRecentDateFormatStr(FTPTimestampParser.DEFAULT_RECENT_DATE_FORMAT);
        // 2 hours difference
        config.setServerTimeZoneId("America/Chicago");
        config.setLenientFutureDates(false); // NET-407
        parser.configure(config);
        SimpleDateFormat sdf = (SimpleDateFormat) parser.getRecentDateFormat().clone();
        // assume we're in the US Pacific Time Zone
        TimeZone tzla = TimeZone.getTimeZone("America/Los_Angeles");
        sdf.setTimeZone(tzla);
        // get formatted versions of time in L.A.
        String fmtTimePlusOneHour = sdf.format(anHourFromNow);
        String fmtTimePlusThreeHours = sdf.format(threeHoursFromNow);
        try {
            Calendar parsed = parser.parseTimestamp(fmtTimePlusOneHour);
            // the only difference should be the two hours
            // difference, no rolling back a year should occur.
            assertEquals(TWO_HOURS_OF_MILLISECONDS, cal.getTime().getTime() - parsed.getTime().getTime(), "no.rollback.because.of.time.zones");
        } catch (ParseException e) {
            fail("Unable to parse " + fmtTimePlusOneHour);
        }
        //but if the file's timestamp is THREE hours ahead of now, that should
        //cause a rollover even taking the time zone difference into account.
        //Since that time is still later than ours, it is parsed as occurring
        //on this date last year.
        try {
            Calendar parsed = parser.parseTimestamp(fmtTimePlusThreeHours);
            // rollback should occur here.
            assertEquals(1, cal.get(Calendar.YEAR) - parsed.get(Calendar.YEAR), "rollback.even.with.time.zones");
        } catch (ParseException e) {
            fail("Unable to parse" + fmtTimePlusThreeHours);
        }
    }

    @Test
    public void testParser() {
        // This test requires an English Locale
        Locale locale = Locale.getDefault();
        try {
            Locale.setDefault(Locale.ENGLISH);
            FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
            try {
                parser.parseTimestamp("feb 22 2002");
            } catch (ParseException e) {
                fail("failed.to.parse.default");
            }
            try {
                Calendar c = parser.parseTimestamp("f\u00e9v 22 2002");
                fail("should.have.failed.to.parse.default, but was: " + c.getTime().toString());
            } catch (ParseException e) {
                // this is the success case
            }
            FTPClientConfig config = new FTPClientConfig();
            config.setDefaultDateFormatStr("d MMM yyyy");
            config.setRecentDateFormatStr("d MMM HH:mm");
            config.setServerLanguageCode("fr");
            parser.configure(config);
            try {
                parser.parseTimestamp("d\u00e9c 22 2002");
                fail("incorrect.field.order");
            } catch (ParseException e) {
                // this is the success case
            }
            try {
                parser.parseTimestamp("22 d\u00e9c 2002");
            } catch (ParseException e) {
                fail("failed.to.parse.french");
            }

            try {
                parser.parseTimestamp("22 dec 2002");
                fail("incorrect.language");
            } catch (ParseException e) {
                // this is the success case
            }
            try {
                parser.parseTimestamp("29 f\u00e9v 2002");
                fail("nonexistent.date");
            } catch (ParseException e) {
                // this is the success case
            }

            try {
                parser.parseTimestamp("22 ao\u00fb 30:02");
                fail("bad.hour");
            } catch (ParseException e) {
                // this is the success case
            }

            try {
                parser.parseTimestamp("22 ao\u00fb 20:74");
                fail("bad.minute");
            } catch (ParseException e) {
                // this is the success case
            }
            try {
                parser.parseTimestamp("28 ao\u00fb 20:02");
            } catch (ParseException e) {
                fail("failed.to.parse.french.recent");
            }
        } finally {
            Locale.setDefault(locale);
        }
    }

    /*
     * Check how short date is interpreted at a given time.
     * Check both with and without lenient future dates
     */
    private void checkShortParse(String msg, Calendar serverTime, Calendar input) throws ParseException {
        checkShortParse(msg, serverTime, input, false);
        checkShortParse(msg, serverTime, input, true);
    }

    /*
     * Check how short date is interpreted at a given time.
     * Check both with and without lenient future dates
     */
    private void checkShortParse(String msg, Calendar serverTime, Calendar input, Calendar expected) throws ParseException {
        checkShortParse(msg, serverTime, input, expected, false);
        checkShortParse(msg, serverTime, input, expected, true);
    }

    /**
     * Check how short date is interpreted at a given time
     * Check only using specified lenient future dates setting
     *
     * @param msg        identifying message
     * @param servertime the time at the server
     * @param input      the time to be converted to a short date, parsed and tested against the full time
     * @param lenient    whether to use lenient mode or not.
     */
    private void checkShortParse(String msg, Calendar servertime, Calendar input, boolean lenient) throws ParseException {
        checkShortParse(msg, servertime, input, input, lenient);
    }

    /**
     * Check how short date is interpreted at a given time
     * Check only using specified lenient future dates setting
     *
     * @param msg        identifying message
     * @param servertime the time at the server
     * @param input      the time to be converted to a short date and parsed
     * @param expected   the expected result from parsing
     * @param lenient    whether to use lenient mode or not.
     */
    private void checkShortParse(String msg, Calendar servertime, Calendar input, Calendar expected, boolean lenient)
            throws ParseException {
        FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
        parser.setLenientFutureDates(lenient);
        SimpleDateFormat shortFormat = parser.getRecentDateFormat(); // It's expecting this format
        final String shortDate = shortFormat.format(input.getTime());
        Calendar output = parser.parseTimestamp(shortDate, servertime);
        int outyear = output.get(Calendar.YEAR);
        int outdom = output.get(Calendar.DAY_OF_MONTH);
        int outmon = output.get(Calendar.MONTH);
        int inyear = expected.get(Calendar.YEAR);
        int indom = expected.get(Calendar.DAY_OF_MONTH);
        int inmon = expected.get(Calendar.MONTH);
        if (indom != outdom || inmon != outmon || inyear != outyear) {
            Format longFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm");
            fail("Test: '" + msg + "' Server=" + longFormat.format(servertime.getTime())
                    + ". Failed to parse " + shortDate + (lenient ? " (lenient)" : " (non-lenient)")
                    + " using " + shortFormat.toPattern()
                    + ". Actual " + longFormat.format(output.getTime())
                    + ". Expected " + longFormat.format(expected.getTime()));
        }
    }

    @Test
    public void testParseShortPastDates1() throws Exception {
        GregorianCalendar now = new GregorianCalendar(2001, Calendar.MAY, 30, 12, 0);
        checkShortParse("2001-5-30", now, now); // should always work
        GregorianCalendar target = (GregorianCalendar) now.clone();
        target.add(Calendar.WEEK_OF_YEAR, -1);
        checkShortParse("2001-5-30 -1 week", now, target);
        target.add(Calendar.WEEK_OF_YEAR, -12);
        checkShortParse("2001-5-30 -13 weeks", now, target);
        target.add(Calendar.WEEK_OF_YEAR, -13);
        checkShortParse("2001-5-30 -26 weeks", now, target);
    }

    @Test
    public void testParseShortPastDates2() throws Exception {
        GregorianCalendar now = new GregorianCalendar(2004, Calendar.AUGUST, 1, 12, 0);
        checkShortParse("2004-8-1", now, now); // should always work
        GregorianCalendar target = (GregorianCalendar) now.clone();
        target.add(Calendar.WEEK_OF_YEAR, -1);
        checkShortParse("2004-8-1 -1 week", now, target);
        target.add(Calendar.WEEK_OF_YEAR, -12);
        checkShortParse("2004-8-1 -13 weeks", now, target);
        target.add(Calendar.WEEK_OF_YEAR, -13);
        checkShortParse("2004-8-1 -26 weeks", now, target);
    }

    @Test
    public void testParseShortFutureDates1() throws Exception {
        GregorianCalendar now = new GregorianCalendar(2001, Calendar.MAY, 30, 12, 0);
        checkShortParse("2001-5-30", now, now); // should always work
        GregorianCalendar target = (GregorianCalendar) now.clone();
        target.add(Calendar.DAY_OF_MONTH, 1);
        checkShortParse("2001-5-30 +1 day", now, target, true);
        try {
            checkShortParse("2001-5-30 +1 day", now, target, false);
            fail("Expected AssertionFailedError");
        } catch (Throwable pe) {
            if (pe.getMessage().startsWith("Expected AssertionFailedError")) { // don't swallow our failure
                throw pe;
            }
        }
        target.add(Calendar.WEEK_OF_YEAR, 1);
    }

    @Test
    public void testParseShortFutureDates2() throws Exception {
        GregorianCalendar now = new GregorianCalendar(2004, Calendar.AUGUST, 1, 12, 0);
        checkShortParse("2004-8-1", now, now); // should always work
        GregorianCalendar target = (GregorianCalendar) now.clone();
        target.add(Calendar.DAY_OF_MONTH, 1);
        checkShortParse("2004-8-1 +1 day", now, target, true);
        try {
            checkShortParse("2004-8-1 +1 day", now, target, false);
            fail("Expected AssertionFailedError");
        } catch (Throwable pe) {
            if (pe.getMessage().startsWith("Expected AssertionFailedError")) { // don't swallow our failure
                throw pe;
            }
        }
    }

    @Test
    public void testFeb29IfLeapYear() throws Exception {
        GregorianCalendar now = new GregorianCalendar();
        final int thisYear = now.get(Calendar.YEAR);
        final GregorianCalendar target = new GregorianCalendar(thisYear, Calendar.FEBRUARY, 29);
        if (now.isLeapYear(thisYear) && now.after(target) && now.before(new GregorianCalendar(thisYear, Calendar.AUGUST, 29))) {
            checkShortParse("Feb 29th", now, target);
        }
    }

    @Test
    public void testFeb29LeapYear() throws Exception {
        int year = 2000; // Use same year for current and short date
        GregorianCalendar now = new GregorianCalendar(year, Calendar.APRIL, 1, 12, 0);
        checkShortParse("Feb 29th 2000", now, new GregorianCalendar(year, Calendar.FEBRUARY, 29));
    }

    @Test
    public void testFeb29LeapYear2() throws Exception {
        int year = 2000; // Use same year for current and short date
        GregorianCalendar now = new GregorianCalendar(year, Calendar.MARCH, 1, 12, 0);
        checkShortParse("Feb 29th 2000", now, new GregorianCalendar(year, Calendar.FEBRUARY, 29));
    }

    @Test
    public void testFeb29LeapYear3() throws Exception {
        int year = 2000; // Use same year for current and short date
        GregorianCalendar now = new GregorianCalendar(year, Calendar.FEBRUARY, 29, 12, 0);
        checkShortParse("Feb 29th 2000", now, new GregorianCalendar(year, Calendar.FEBRUARY, 29));
    }

    @Test
    public void testFeb29LeapYear4() throws Exception {
        int year = 2000; // Use same year for current and short date
        GregorianCalendar now = new GregorianCalendar(year, Calendar.FEBRUARY, 28, 12, 0);
        // Must allow lenient future date here
        checkShortParse("Feb 29th 2000", now, new GregorianCalendar(year, Calendar.FEBRUARY, 29), true);
    }

    @Test
    public void testFeb29NonLeapYear() {
        GregorianCalendar server = new GregorianCalendar(1999, Calendar.APRIL, 1, 12, 0);
        // Note: we use a known leap year for the target date to avoid rounding up
        GregorianCalendar input = new GregorianCalendar(2000, Calendar.FEBRUARY, 29);
        GregorianCalendar expected = new GregorianCalendar(1999, Calendar.FEBRUARY, 29);
        try {
            checkShortParse("Feb 29th 1999", server, input, expected, true);
            fail("Should have failed to parse Feb 29th 1999");
        } catch (ParseException pe) {
            // expected
        }
        try {
            checkShortParse("Feb 29th 1999", server, input, expected, false);
            fail("Should have failed to parse Feb 29th 1999");
        } catch (ParseException pe) {
            // expected
        }
    }

    @Test
    @Disabled
    public void testNET446() throws Exception {
        GregorianCalendar server = new GregorianCalendar(2001, Calendar.JANUARY, 1, 12, 0);
        // Note: we use a known leap year for the target date to avoid rounding up
        GregorianCalendar input = new GregorianCalendar(2000, Calendar.FEBRUARY, 29);
        GregorianCalendar expected = new GregorianCalendar(2000, Calendar.FEBRUARY, 29);
        checkShortParse("Feb 29th 2000", server, input, expected);
    }

    @Test
    public void testParseDec31Lenient() throws Exception {
        GregorianCalendar now = new GregorianCalendar(2007, Calendar.DECEMBER, 30, 12, 0);
        checkShortParse("2007-12-30", now, now); // should always work
        GregorianCalendar target = (GregorianCalendar) now.clone();
        target.add(Calendar.DAY_OF_YEAR, +1); // tomorrow
        checkShortParse("2007-12-31", now, target, true);
    }

    @Test
    public void testParseJan01Lenient() throws Exception {
        GregorianCalendar now = new GregorianCalendar(2007, Calendar.DECEMBER, 31, 12, 0);
        checkShortParse("2007-12-31", now, now); // should always work
        GregorianCalendar target = (GregorianCalendar) now.clone();
        target.add(Calendar.DAY_OF_YEAR, +1); // tomorrow
        checkShortParse("2008-1-1", now, target, true);
    }

    @Test
    public void testParseJan01() throws Exception {
        GregorianCalendar now = new GregorianCalendar(2007, Calendar.JANUARY, 1, 12, 0);
        checkShortParse("2007-01-01", now, now); // should always work
        GregorianCalendar target = new GregorianCalendar(2006, Calendar.DECEMBER, 31, 12, 0);
        checkShortParse("2006-12-31", now, target, true);
        checkShortParse("2006-12-31", now, target, false);
    }
}
