package de.danoeh.antennapod.storage.database;

import org.junit.Test;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;

import static de.danoeh.antennapod.storage.database.ReleaseScheduleGuesser.ONE_DAY;
import static de.danoeh.antennapod.storage.database.ReleaseScheduleGuesser.ONE_HOUR;
import static de.danoeh.antennapod.storage.database.ReleaseScheduleGuesser.ONE_MINUTE;
import static de.danoeh.antennapod.storage.database.ReleaseScheduleGuesser.performGuess;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class ReleaseScheduleGuesserTest {
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ROOT);

    private Date makeDate(String dateStr) {
        try {
            return DATE_FORMAT.parse(dateStr);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }

    private void assertClose(Date expected, Date actual, long tolerance) {
        assertTrue("Date should differ at most " + tolerance / 60000 + " minutes from "
                + DATE_FORMAT.format(expected) + ", but is " + DATE_FORMAT.format(actual),
                Math.abs(expected.getTime() - actual.getTime()) < tolerance);
    }

    @Test
    public void testEdgeCases() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        assertEquals(ReleaseScheduleGuesser.Schedule.UNKNOWN, performGuess(releaseDates).schedule);
        releaseDates.add(makeDate("2024-01-01 16:30"));
        assertEquals(ReleaseScheduleGuesser.Schedule.UNKNOWN, performGuess(releaseDates).schedule);
    }

    @Test
    public void testMultipleTimesPerDayEveryDay() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-01 12:00"));
        releaseDates.add(makeDate("2024-01-01 16:00"));
        releaseDates.add(makeDate("2024-01-02 12:00"));
        releaseDates.add(makeDate("2024-01-02 16:00"));
        releaseDates.add(makeDate("2024-01-03 12:00"));
        releaseDates.add(makeDate("2024-01-03 16:00"));
        releaseDates.add(makeDate("2024-01-04 12:00"));
        releaseDates.add(makeDate("2024-01-04 16:00"));
        releaseDates.add(makeDate("2024-01-06 12:00"));
        releaseDates.add(makeDate("2024-01-06 16:00"));
        releaseDates.add(makeDate("2024-01-07 12:00"));
        releaseDates.add(makeDate("2024-01-07 16:00"));
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.SPECIFIC_DAYS, guess.schedule);
        assertTrue(guess.multipleReleasesPerDay);
        ArrayList<Integer> expectedDays = new ArrayList<>();
        expectedDays.add(1);
        expectedDays.add(2);
        expectedDays.add(3);
        expectedDays.add(4);
        expectedDays.add(5);
        expectedDays.add(7);
        assertEquals(expectedDays, guess.days);
        assertClose(makeDate("2024-01-08 12:00"), guess.nextExpectedDate, ONE_DAY);
    }

    @Test
    public void testMultipleTimesPerDayWeekdays() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-01 00:00"));
        releaseDates.add(makeDate("2024-01-01 12:00"));
        releaseDates.add(makeDate("2024-01-02 00:00"));
        releaseDates.add(makeDate("2024-01-02 12:00"));
        releaseDates.add(makeDate("2024-01-03 00:00"));
        releaseDates.add(makeDate("2024-01-03 12:00"));
        releaseDates.add(makeDate("2024-01-04 00:00"));
        releaseDates.add(makeDate("2024-01-04 12:00"));
        releaseDates.add(makeDate("2024-01-05 00:00"));
        releaseDates.add(makeDate("2024-01-05 12:00"));
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.WEEKDAYS, guess.schedule);
        assertTrue(guess.multipleReleasesPerDay);
        assertClose(makeDate("2024-01-08 12:00"), guess.nextExpectedDate, ONE_DAY);
    }

    @Test
    public void testMultipleTimesPerDaySpecificDays() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-01 00:00"));
        releaseDates.add(makeDate("2024-01-01 12:00"));
        releaseDates.add(makeDate("2024-01-03 00:00"));
        releaseDates.add(makeDate("2024-01-03 12:00"));
        releaseDates.add(makeDate("2024-01-08 00:00"));
        releaseDates.add(makeDate("2024-01-08 12:00"));
        releaseDates.add(makeDate("2024-01-10 00:00"));
        releaseDates.add(makeDate("2024-01-10 12:00"));
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.SPECIFIC_DAYS, guess.schedule);
        assertTrue(guess.multipleReleasesPerDay);
        assertClose(makeDate("2024-01-15 12:00"), guess.nextExpectedDate, ONE_DAY);
    }

    @Test
    public void testDaily() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-01 16:30")); // Monday
        releaseDates.add(makeDate("2024-01-02 16:25"));
        releaseDates.add(makeDate("2024-01-03 16:35"));
        releaseDates.add(makeDate("2024-01-04 16:40"));
        releaseDates.add(makeDate("2024-01-05 16:20"));
        releaseDates.add(makeDate("2024-01-06 16:10"));
        releaseDates.add(makeDate("2024-01-07 16:32")); // Sunday

        // Next day
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.DAILY, guess.schedule);
        assertClose(makeDate("2024-01-08 16:30"), guess.nextExpectedDate, 10 * ONE_MINUTE);

        // One-off early release
        releaseDates.add(makeDate("2024-01-08 10:00"));
        guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.DAILY, guess.schedule);
        assertClose(makeDate("2024-01-09 16:30"), guess.nextExpectedDate, 10 * ONE_MINUTE);
    }

    @Test
    public void testWeekdays() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-01 16:30")); // Monday
        releaseDates.add(makeDate("2024-01-02 16:25"));
        releaseDates.add(makeDate("2024-01-03 16:35"));
        releaseDates.add(makeDate("2024-01-04 16:40"));
        releaseDates.add(makeDate("2024-01-05 16:20")); // Friday
        releaseDates.add(makeDate("2024-01-08 16:20")); // Monday
        releaseDates.add(makeDate("2024-01-09 16:30"));
        releaseDates.add(makeDate("2024-01-10 16:40"));
        releaseDates.add(makeDate("2024-01-11 16:45")); // Thursday

        // Next day
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.WEEKDAYS, guess.schedule);
        assertClose(makeDate("2024-01-12 16:30"), guess.nextExpectedDate, ONE_HOUR);

        // After weekend
        releaseDates.add(makeDate("2024-01-12 16:30")); // Friday
        guess = performGuess(releaseDates);
        assertClose(makeDate("2024-01-15 16:30"), guess.nextExpectedDate, ONE_HOUR);
    }

    @Test
    public void testWeekly() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-07 16:30")); // Sunday
        releaseDates.add(makeDate("2024-01-14 16:25"));
        releaseDates.add(makeDate("2024-01-21 14:25"));
        releaseDates.add(makeDate("2024-01-28 16:15"));

        // Next week
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.WEEKLY, guess.schedule);
        assertClose(makeDate("2024-02-04 16:30"), guess.nextExpectedDate, 2 * ONE_HOUR);

        // One-off early release
        releaseDates.add(makeDate("2024-02-02 16:35"));
        guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.WEEKLY, guess.schedule);
        assertClose(makeDate("2024-02-11 16:30"), guess.nextExpectedDate, 2 * ONE_HOUR);

        // One-off late release
        releaseDates.add(makeDate("2024-02-13 16:35"));
        guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.WEEKLY, guess.schedule);
        assertClose(makeDate("2024-02-18 16:30"), guess.nextExpectedDate, 2 * ONE_HOUR);
    }

    @Test
    public void testMonthly() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-01 16:30"));
        releaseDates.add(makeDate("2024-02-01 16:30"));
        releaseDates.add(makeDate("2024-03-01 16:30"));
        releaseDates.add(makeDate("2024-04-01 16:30"));

        // Next month
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.MONTHLY, guess.schedule);
        assertClose(makeDate("2024-05-01 16:30"), guess.nextExpectedDate, 10 * ONE_HOUR);

        // One-off early release
        releaseDates.add(makeDate("2024-04-30 16:30"));
        guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.MONTHLY, guess.schedule);
        assertClose(makeDate("2024-06-01 16:30"), guess.nextExpectedDate, 10 * ONE_HOUR);

        // One-off late release
        releaseDates.remove(releaseDates.size() - 1);
        releaseDates.add(makeDate("2024-05-13 16:30"));
        guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.MONTHLY, guess.schedule);
        assertClose(makeDate("2024-06-01 16:30"), guess.nextExpectedDate, 10 * ONE_HOUR);
    }

    @Test
    public void testFourweekly() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-01 16:30"));
        releaseDates.add(makeDate("2024-01-29 16:30"));
        releaseDates.add(makeDate("2024-02-26 16:30"));
        releaseDates.add(makeDate("2024-03-25 16:30"));

        // 4 weeks later
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.FOURWEEKLY, guess.schedule);
        assertClose(makeDate("2024-04-22 16:30"), guess.nextExpectedDate, 10 * ONE_HOUR);
    }

    @Test
    public void testUnknown() {
        ArrayList<Date> releaseDates = new ArrayList<>();
        releaseDates.add(makeDate("2024-01-01 16:30"));
        releaseDates.add(makeDate("2024-01-04 16:30"));
        releaseDates.add(makeDate("2024-01-10 16:31"));
        releaseDates.add(makeDate("2024-01-11 16:30"));
        releaseDates.add(makeDate("2024-01-20 16:31"));
        releaseDates.add(makeDate("2024-01-22 16:30"));
        releaseDates.add(makeDate("2024-01-25 16:31"));
        releaseDates.add(makeDate("2024-01-25 16:30"));
        ReleaseScheduleGuesser.Guess guess = performGuess(releaseDates);
        assertEquals(ReleaseScheduleGuesser.Schedule.UNKNOWN, guess.schedule);
        assertClose(makeDate("2024-01-27 16:30"), guess.nextExpectedDate, 2 * ONE_DAY);
    }
}