/**
 * Copyright (c) 2012 Todoroo Inc
 *
 * See the file "LICENSE" for the full license governing this code.
 */
package com.todoroo.astrid.sync.repeats;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;

import android.content.Intent;

import com.google.ical.values.Frequency;
import com.google.ical.values.RRule;
import com.google.ical.values.Weekday;
import com.google.ical.values.WeekdayNum;
import com.timsu.astrid.R;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.SyncFlags;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.repeats.RepeatTaskCompleteListener;
import com.todoroo.astrid.test.DatabaseTestCase;
import com.todoroo.astrid.utility.Flags;

abstract public class AbstractSyncRepeatTests<REMOTE_MODEL> extends DatabaseTestCase {

    @Autowired
    protected TaskDao taskDao;

    @Autowired
    protected MetadataDao metadataDao;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        Preferences.setStringFromInteger(R.string.p_default_urgency_key, 0);
    }

    private void saveAndTriggerRepeatListener(Task task) {
        task.putTransitory(SyncFlags.ACTFM_SUPPRESS_SYNC, true);
        task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true);
        if(task.isSaved())
            taskDao.saveExisting(task);
        else
            taskDao.createNew(task);

        Intent intent = new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_COMPLETED);
        intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
        new RepeatTaskCompleteListener().onReceive(getContext(), intent);
    }

    protected void waitAndSync() {
        // Subclasses can override this to insert sync functionality
    }

    /**
     * @param t
     * @param expectedDueDate
     */
    protected REMOTE_MODEL assertTaskExistsRemotely(Task t, long expectedDueDate) {
        // Subclasses can override this to check the existence of remote objects
        return null;
    }

    /**
     * @param t task
     */
    protected void assertTaskCompletedRemotely(Task t) {
        // Subclasses can override this to check the status of the corresponding remote task
    }


    /**
     * @param remoteModel
     */
    protected long setCompletionDate(boolean completeBefore, Task t,
            REMOTE_MODEL remoteModel, long dueDate) {
        long completionDate = DateUtilities.now();
        t.setValue(Task.COMPLETION_DATE, completionDate);
        saveAndTriggerRepeatListener(t);
        return completionDate;
    }

    protected void assertTimesMatch(long expectedTime, long newDueDate) {
        assertTrue(String.format("Expected %s, was %s", new Date(expectedTime), new Date(newDueDate)),
                Math.abs(expectedTime - newDueDate) <= 600000); 
        // Allow a few minutes of variance to account for timing issues in tests
    }

    /*
     * Tests for no sync
     */

    public void testNoRepeat() {
        Task t = new Task();
        t.setValue(Task.TITLE, "no repeat");
        taskDao.save(t);

        t.setValue(Task.COMPLETION_DATE, DateUtilities.now());
        saveAndTriggerRepeatListener(t);

        TodorooCursor<Task> cursor = taskDao.query(Query.select(Task.ID));
        try {
            assertEquals(1, cursor.getCount());
        } finally {
            cursor.close();
        }
    }

    protected void testRepeating(boolean completeBefore, boolean fromCompletion,
            RRule rrule, Frequency frequency, String title) {
        Task t = new Task();
        t.setValue(Task.TITLE, title);
        long dueDate = DateUtilities.now() + ((completeBefore ? -1 : 1) * DateUtilities.ONE_DAY * 3);
        dueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, (dueDate / 1000L) * 1000L); // Strip milliseconds
        if (fromCompletion)
            t.setFlag(Task.FLAGS, Task.FLAG_REPEAT_AFTER_COMPLETION, true);

        t.setValue(Task.DUE_DATE, dueDate);

        if (rrule == null) {
            rrule = new RRule();
            rrule.setFreq(frequency);
            int interval = frequency.equals(Frequency.MINUTELY) ? 100: 2;
            rrule.setInterval(interval);
        }
        t.setValue(Task.RECURRENCE, rrule.toIcal());
        taskDao.save(t);

        waitAndSync();
        t = taskDao.fetch(t.getId(), Task.PROPERTIES); // Refetch
        REMOTE_MODEL remoteModel = assertTaskExistsRemotely(t, dueDate);

        long completionDate = setCompletionDate(completeBefore, t, remoteModel, dueDate);

        waitAndSync();
        assertTaskCompletedRemotely(t);

        TodorooCursor<Task> cursor = taskDao.query(Query.select(Task.PROPERTIES).where(TaskCriteria.notDeleted()));
        try {
            for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
                Task task = new Task(cursor);
                System.err.println("Task: " + task.getValue(Task.TITLE) + ", due: " + task.getValue(Task.DUE_DATE));
            }
            assertEquals(1, cursor.getCount());
            cursor.moveToFirst();
            t.readFromCursor(cursor);

            assertEquals(title, t.getValue(Task.TITLE));
            assertFalse(t.isCompleted());
            long newDueDate = t.getValue(Task.DUE_DATE);
            assertTrue(t.hasDueTime());

            long fromDate = (fromCompletion? completionDate : dueDate);
            long expectedTime = computeNextDueDateFromDate(fromDate, rrule, fromCompletion);

            assertTaskExistsRemotely(t, expectedTime);
            assertTimesMatch(expectedTime, newDueDate);
        } finally {
            cursor.close();
        }
    }

    private long computeWeeklyCaseDueDate(long fromDate, RRule rrule, boolean fromCompletion) {
        long result = fromDate;
        Frequency frequency = rrule.getFreq();
        assertTrue(frequency.equals(Frequency.WEEKLY));
        List<WeekdayNum> weekdayNums = rrule.getByDay();

        if (weekdayNums.size() == 0) {
            result += DateUtilities.ONE_WEEK * rrule.getInterval();
            return result;
        }
        HashSet<Weekday> weekdays = new HashSet<Weekday>();
        for (WeekdayNum curr : weekdayNums) {
            weekdays.add(curr.wday);
        }

        Weekday[] allWeekdays = Weekday.values();
//        result -= DateUtilities.ONE_DAY;
        Date date = new Date(result);
        Weekday start = allWeekdays[date.getDay()];
        int i;
        for (i = 0; i < allWeekdays.length; i++) {
            if (start == allWeekdays[i]) break;
        }
        int index = i;
        int daysToAdd = 0;
        Weekday next = null;
        for (i = index + 1; i < allWeekdays.length; i++) {
            Weekday curr = allWeekdays[i];
            daysToAdd++;
            if (weekdays.contains(curr)) {
                next = curr;
                break;
            }
        }

        if (next == null) {
            for (i = 0; i < index + 1; i++) {
                Weekday curr = allWeekdays[i];
                daysToAdd++;
                if (weekdays.contains(curr)) {
                    next = curr;
                    break;
                }
            }
        }

        if (fromCompletion) {
            result += DateUtilities.ONE_WEEK * (rrule.getInterval() - 1);
        }
        result += DateUtilities.ONE_DAY * daysToAdd;
        return result;
    }


    /** Advanced weekly repeating tests */
    protected long computeNextDueDateFromDate(long fromDate, RRule rrule, boolean fromCompletion) {
        long expectedTime = fromDate;
        Frequency frequency = rrule.getFreq();
        int interval = rrule.getInterval();
        if (frequency.equals(Frequency.MINUTELY)) {
            expectedTime += DateUtilities.ONE_MINUTE * interval;
        } else if (frequency.equals(Frequency.HOURLY)) {
            expectedTime += DateUtilities.ONE_HOUR * interval;
        } else if (frequency.equals(Frequency.DAILY)) {
            expectedTime += DateUtilities.ONE_DAY * interval;
        } else if (frequency.equals(Frequency.WEEKLY)) {
            expectedTime = computeWeeklyCaseDueDate(fromDate, rrule, fromCompletion);
        } else if (frequency.equals(Frequency.MONTHLY)) {
            Date originalDate = new Date(expectedTime);
            for (int i = 0; i < interval; i++) {
                int month = originalDate.getMonth();
                if (month == 11) { // Roll over the year and set the month to January
                    originalDate.setYear(originalDate.getYear() + 1);
                    originalDate.setMonth(0);
                } else {
                    originalDate.setMonth(originalDate.getMonth() + 1);
                }
            }
            expectedTime = originalDate.getTime();
        } else if (frequency.equals(Frequency.YEARLY)) {
            Date originalCompleteDate = new Date(expectedTime);
            originalCompleteDate.setYear(originalCompleteDate.getYear() + interval);
            expectedTime = originalCompleteDate.getTime();
        }
        
        return Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, expectedTime);
    }

    private void testFromDueDate(boolean completeBefore, Frequency frequency, String title) {
        testRepeating(completeBefore, false, null, frequency, title);
    }

    private void testFromCompletionDate(boolean completeBefore, Frequency frequency, String title) {
        testRepeating(completeBefore, true, null, frequency, title);
    }


    /** Tests for repeating from due date */

    public void testRepeatMinutelyFromDueDateCompleteBefore() {
        testFromDueDate(true, Frequency.MINUTELY, "minutely-before");
    }

    public void testRepeatMinutelyFromDueDateCompleteAfter() {
        testFromDueDate(false, Frequency.MINUTELY, "minutely-after");
    }

    public void testRepeatHourlyFromDueDateCompleteBefore() {
        testFromDueDate(true, Frequency.HOURLY, "hourly-before");
    }

    public void testRepeatHourlyFromDueDateCompleteAfter() {
        testFromDueDate(false, Frequency.HOURLY, "hourly-after");
    }

    public void testRepeatDailyFromDueDateCompleteBefore() {
        testFromDueDate(true, Frequency.DAILY, "daily-before");
    }

    public void testRepeatDailyFromDueDateCompleteAfter() {
        testFromDueDate(false, Frequency.DAILY, "daily-after");
    }

    public void testRepeatWeeklyFromDueDateCompleteBefore() {
        testFromDueDate(true, Frequency.WEEKLY, "weekly-before");
    }

    public void testRepeatWeeklyFromDueDateCompleteAfter() {
        testFromDueDate(false, Frequency.WEEKLY, "weekly-after");
    }

    public void testRepeatMonthlyFromDueDateCompleteBefore() {
        testFromDueDate(true, Frequency.MONTHLY, "monthly-before");
    }

    public void testRepeatMonthlyFromDueDateCompleteAfter() {
        testFromDueDate(false, Frequency.MONTHLY, "monthly-after");
    }

    public void testRepeatYearlyFromDueDateCompleteBefore() {
        testFromDueDate(true, Frequency.YEARLY, "yearly-before");
    }

    public void testRepeatYearlyFromDueDateCompleteAfter() {
        testFromDueDate(false, Frequency.YEARLY, "yearly-after");
    }


    /** Tests for repeating from completionDate */

    public void testRepeatMinutelyFromCompleteDateCompleteBefore() {
        testFromCompletionDate(true, Frequency.MINUTELY, "minutely-before");
    }

    public void testRepeatMinutelyFromCompleteDateCompleteAfter() {
        testFromCompletionDate(false, Frequency.MINUTELY, "minutely-after");
    }

    public void testRepeatHourlyFromCompleteDateCompleteBefore() {
        testFromCompletionDate(true, Frequency.HOURLY, "hourly-before");
    }

    public void testRepeatHourlyFromCompleteDateCompleteAfter() {
        testFromCompletionDate(false, Frequency.HOURLY, "hourly-after");
    }

    public void testRepeatDailyFromCompleteDateCompleteBefore() {
        testFromCompletionDate(true, Frequency.DAILY, "daily-before");
    }

    public void testRepeatDailyFromCompleteDateCompleteAfter() {
        testFromCompletionDate(false, Frequency.DAILY, "daily-after");
    }

    public void testRepeatWeeklyFromCompleteDateCompleteBefore() {
        testFromCompletionDate(true, Frequency.WEEKLY, "weekly-before");
    }

    public void testRepeatWeeklyFromCompleteDateCompleteAfter() {
        testFromCompletionDate(false, Frequency.WEEKLY, "weekly-after");
    }

    public void testRepeatMonthlyFromCompleteDateCompleteBefore() {
        testFromCompletionDate(true, Frequency.MONTHLY, "monthly-before");
    }

    public void testRepeatMonthlyFromCompleteDateCompleteAfter() {
        testFromCompletionDate(false, Frequency.MONTHLY, "monthly-after");
    }

    public void testRepeatYearlyFromCompleteDateCompleteBefore() {
        testFromCompletionDate(true, Frequency.YEARLY, "yearly-before");
    }

    public void testRepeatYearlyFromCompleteDateCompleteAfter() {
        testFromCompletionDate(false, Frequency.YEARLY, "yearly-after");
    }

    private void testAdvancedWeekly(boolean fromCompletion, boolean completeBefore, String title) {
        RRule rrule = new RRule();
        rrule.setFreq(Frequency.WEEKLY);

        int interval = 1;
        rrule.setInterval(interval);
        List<WeekdayNum> weekdays = new ArrayList<WeekdayNum>();
        weekdays.add(new WeekdayNum(0, Weekday.MO));
        weekdays.add(new WeekdayNum(0, Weekday.WE));
        rrule.setByDay(weekdays);
        testRepeating(completeBefore, fromCompletion, rrule, Frequency.WEEKLY, title);
    }



    // disabled until test can be fixed
    public void testAdvancedRepeatWeeklyFromDueDateCompleteBefore() {
        testAdvancedWeekly(false, true, "advanced-weekly-before");
    }

    public void testAdvancedRepeatWeeklyFromDueDateCompleteAfter() {
        testAdvancedWeekly(false, false, "advanced-weekly-after");
    }

    public void testAdvancedRepeatWeeklyFromCompleteDateCompleteBefore() {
        testAdvancedWeekly(true, true, "advanced-weekly-before");
    }

    public void testAdvancedRepeatWeeklyFromCompleteDateCompleteAfter() {
        testAdvancedWeekly(true, false, "advanced-weekly-after");
    }
}
