/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

Components.utils.import("resource://calendar/modules/calAsyncUtils.jsm");

const cIC = Components.interfaces.calICalendar;

var icalStringArray = [
    // Comments refer to the range defined in testGetItems().
    // 1: one-hour event
    "BEGIN:VEVENT\n" +
    "DTSTART:20020402T114500Z\n" +
    "DTEND:20020402T124500Z\n" +
    "END:VEVENT\n",
    // 2: Test a zero-length event with DTSTART and DTEND
    "BEGIN:VEVENT\n" +
    "DTSTART:20020402T000000Z\n" +
    "DTEND:20020402T000000Z\n" +
    "END:VEVENT\n",
    // 3: Test a zero-length event with DTSTART and no DTEND
    "BEGIN:VEVENT\n" +
    "DTSTART:20020402T000000Z\n" +
    "END:VEVENT\n",
    // 4: Test a zero-length event with DTEND set and no  DTSTART. Invalid!
    "BEGIN:VEVENT\n" +
    "DTEND:20020402T000000Z\n" +
    "END:VEVENT\n",
    // 5: one-hour event that is outside the range
    "BEGIN:VEVENT\n" +
    "DTSTART:20020401T114500Z\n" +
    "DTEND:20020401T124500Z\n" +
    "END:VEVENT\n",
    // 6: one-hour event that starts outside the range and ends inside.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020401T114500Z\n" +
    "DTEND:20020402T124500Z\n" +
    "END:VEVENT\n",
    // 7:  one-hour event that starts inside the range and ends outside.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020402T114500Z\n" +
    "DTEND:20020403T124500Z\n" +
    "END:VEVENT\n",
    // 8: one-hour event that starts at the end of the range.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020403T000000Z\n" +
    "DTEND:20020403T124500Z\n" +
    "END:VEVENT\n",
    // 9: allday event that starts at start of range and ends at end of range.
    "BEGIN:VEVENT\n" +
    "DTSTART;VALUE=DATE:20020402\n" +
    "DTEND;VALUE=DATE:20020403\n" +
    "END:VEVENT\n",
    // 10: allday event that starts at end of range.
    "BEGIN:VEVENT\n" +
    "DTSTART;VALUE=DATE:20020403\n" +
    "DTEND;VALUE=DATE:20020404\n" +
    "END:VEVENT\n",
    // 11: allday event that ends at start of range. See bug 333363.
    "BEGIN:VEVENT\n" +
    "DTSTART;VALUE=DATE:20020401\n" +
    "DTEND;VALUE=DATE:20020402\n" +
    "END:VEVENT\n",
    // 12: daily recurring allday event. parent item in the range.
    "BEGIN:VEVENT\n" +
    "DTSTART;VALUE=DATE:20020402\n" +
    "DTEND;VALUE=DATE:20020403\n" +
    "RRULE:FREQ=DAILY;INTERVAL=1;COUNT=10\n" +
    "END:VEVENT\n",
    // 13: daily recurring allday event. First occurence in the range.
    "BEGIN:VEVENT\n" +
    "DTSTART;VALUE=DATE:20020401\n" +
    "DTEND;VALUE=DATE:20020402\n" +
    "RRULE:FREQ=DAILY;COUNT=10\n" +
    "END:VEVENT\n",
    // 14: two-daily recurring allday event. Not in the range.
    "BEGIN:VEVENT\n" +
    "DTSTART;VALUE=DATE:20020401\n" +
    "DTEND;VALUE=DATE:20020402\n" +
    "RRULE:FREQ=DAILY;INTERVAL=2;COUNT=10\n" +
    "END:VEVENT\n",
    // 15: daily recurring one-hour event. Parent in the range.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020402T100000Z\n" +
    "DTEND:20020402T110000Z\n" +
    "RRULE:FREQ=DAILY;COUNT=10\n" +
    "END:VEVENT\n",
    // 16: daily recurring one-hour event. Occurrence in the range.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020401T100000Z\n" +
    "DTEND:20020401T110000Z\n" +
    "RRULE:FREQ=DAILY;COUNT=10\n" +
    "END:VEVENT\n",
    // 17: zero-length task with DTSTART and DUE set at start of range.
    "BEGIN:VTODO\n" +
    "DTSTART:20020402T000000Z\n" +
    "DUE:20020402T000000Z\n" +
    "END:VTODO\n",
    // 18: zero-length event with only DTSTART set at start of range.
    "BEGIN:VTODO\n" +
    "DTSTART:20020402T000000Z\n" +
    "END:VTODO\n",
    // 19: zero-length event with only DUE set at start of range.
    "BEGIN:VTODO\n" +
    "DUE:20020402T000000Z\n" +
    "END:VTODO\n",
    // 20: one-hour todo within the range.
    "BEGIN:VTODO\n" +
    "DTSTART:20020402T110000Z\n" +
    "DUE:20020402T120000Z\n" +
    "END:VTODO\n",
    // 21: zero-length todo that starts at end of range.
    "BEGIN:VTODO\n" +
    "DTSTART:20020403T000000Z\n" +
    "DUE:20020403T010000Z\n" +
    "END:VTODO\n",
    // 22: one-hour todo that ends at start of range.
    "BEGIN:VTODO\n" +
    "DTSTART:20020401T230000Z\n" +
    "DUE:20020402T000000Z\n" +
    "END:VTODO\n",
    // 23: daily recurring one-hour event. Parent in the range.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020402T000000\n" +
    "DTEND:20020402T010000\n" +
    "RRULE:FREQ=DAILY;COUNT=10\n" +
    "END:VEVENT\n",
    // 24: daily recurring 24-hour event. Parent in the range.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020402T000000\n" +
    "DTEND:20020403T000000\n" +
    "RRULE:FREQ=DAILY;COUNT=10\n" +
    "END:VEVENT\n",
    // 25: todo that has neither start nor due date set.
    // Should be returned on every getItems() call. See bug 405459.
    "BEGIN:VTODO\n" +
    "SUMMARY:Todo\n" +
    "END:VTODO\n",
    // 26: todo that has neither start nor due date but
    // a completion time set after range. See bug 405459.
    "BEGIN:VTODO\n" +
    "SUMMARY:Todo\n" +
    "COMPLETED:20030404T000001\n" +
    "END:VTODO\n",
    // 27: todo that has neither start nor due date but a
    // completion time set in the range. See bug 405459.
    "BEGIN:VTODO\n" +
    "SUMMARY:Todo\n" +
    "COMPLETED:20020402T120001\n" +
    "END:VTODO\n",
    // 28: todo that has neither start nor due date but a
    // completion time set before the range. See bug 405459.
    "BEGIN:VTODO\n" +
    "SUMMARY:Todo\n" +
    "COMPLETED:20020402T000000\n" +
    "END:VTODO\n",
    // 29: todo that has neither start nor due date set,
    // has the status "COMPLETED" but no completion time. See bug 405459.
    "BEGIN:VTODO\n" +
    "SUMMARY:Todo\n" +
    "STATUS:COMPLETED\n" +
    "END:VTODO\n",
    // 30: one-hour event with duration (in the range). See bug 390492.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020402T114500Z\n" +
    "DURATION:PT1H\n" +
    "END:VEVENT\n",
    // 31: one-hour event with duration (after the range). See bug 390492.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020403T000000Z\n" +
    "DURATION:PT1H\n" +
    "END:VEVENT\n",
    // 32: one-hour event with duration (before the range). See bug 390492.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020401T230000Z\n" +
    "DURATION:PT1H\n" +
    "END:VEVENT\n",
    // 33: one-day event with duration. Starts in the range, Ends outside. See bug 390492.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020402T120000Z\n" +
    "DURATION:P1D\n" +
    "END:VEVENT\n",
    // 34: one-day event with duration. Starts before the range. Ends inside. See bug 390492.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020401T120000Z\n" +
    "DURATION:P1D\n" +
    "END:VEVENT\n",
    // 35: one-day event with duration (before the range). See bug 390492.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020401T000000Z\n" +
    "DURATION:P1D\n" +
    "END:VEVENT\n",
    // 36: one-day event with duration (after the range). See bug 390492.
    "BEGIN:VEVENT\n" +
    "DTSTART:20020403T000000Z\n" +
    "DURATION:P1D\n" +
    "END:VEVENT\n"
];

function run_test() {
    testIcalData();
    testMetaData();

    run_next_test();
}

function testIcalData() {
    // First entry is test number, second item is expected result for testGetItems().
    let wantedArray = [[1, 1],
                       [2, 1],
                       [3, 1],
                       [5, 0],
                       [6, 1],
                       [7, 1],
                       [8, 0],
                       [9, 1],
                       [10, 0],
                       [11, 0],
                       [12, 1],
                       [13, 1],
                       [14, 0],
                       [15, 1],
                       [16, 1],
                       [17, 1],
                       [18, 1],
                       [19, 1],
                       [20, 1],
                       [21, 0],
                       [22, 0],
                       [23, 1],
                       [24, 1],
                       [25, 1],
                       [26, 1],
                       [27, 1],
                       [28, 0],
                       [29, 1],
                       [30, 1],
                       [31, 0],
                       [32, 0],
                       [33, 1],
                       [34, 1],
                       [35, 0],
                       [36, 0]];

    for (let i = 0; i < wantedArray.length; i++) {
        let itemArray = wantedArray[i];
        // Correct for 1 to stay in synch with test numbers.
        let calItem = icalStringArray[itemArray[0] - 1];

        let item;
        if (calItem.search(/VEVENT/) != -1) {
            item = createEventFromIcalString(calItem);
        } else if (calItem.search(/VTODO/) != -1) {
            item = createTodoFromIcalString(calItem);
        }

        print("Test " + wantedArray[i][0]);
        testGetItems(item, itemArray[1]);
        testGetItem(item);
    }

    /**
     * Adds aItem to a calendar and performs a getItems() call using the
     * following range:
     *   2002/04/02 0:00 - 2002/04/03 0:00
     * The amount of returned items is compared with expected amount (aResult).
     * Additionally, the properties of the returned item are compared with aItem.
     */
    function testGetItems(aItem, aResult) {
        for (let calendar of [getStorageCal(), getMemoryCal()]) {
            checkCalendar(calendar, aItem, aResult);
        }
    }

    function checkCalendar(calendar, aItem, aResult) {
        // construct range
        let rangeStart = createDate(2002, 3, 2); // 3 = April
        let rangeEnd = rangeStart.clone();
        rangeEnd.day += 1;

        // filter options
        let filter = Ci.calICalendar.ITEM_FILTER_TYPE_ALL |
                     Ci.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES |
                     Ci.calICalendar.ITEM_FILTER_COMPLETED_ALL;

        // implement listener
        let count = 0;
        let listener = {
            onOperationComplete: function(aCalendar,
                                          aStatus,
                                          aOperationType,
                                          aId,
                                          aDetail) {
                equal(aStatus, 0);
                if (aOperationType == Ci.calIOperationListener.ADD) {
                    // perform getItems() on calendar
                    aCalendar.getItems(filter, 0, rangeStart, rangeEnd, listener);
                } else if (aOperationType == Ci.calIOperationListener.GET) {
                    equal(count, aResult);
                    do_test_finished();
                }
            },
            onGetResult: function(aCalendar,
                                  aStatus,
                                  aItemType,
                                  aDetail,
                                  aCount,
                                  aItems) {
                if (aCount) {
                    count += aCount;
                    for (let i = 0; i < aCount; i++) {
                        compareItemsSpecific(aItems[i].parentItem, aItem);
                    }
                }
            }
        };

        // add item to calendar
        do_test_pending();
        calendar.addItem(aItem, listener);
    }

    /**
     * (1) Add aItem to a calendar.
     * The properties of the added item are compared with the passed item.
     * (2) Perform a getItem() call.
     * The properties of the returned item are compared with the passed item.
     */
    function testGetItem(aItem) {
        // get calendars
        let calArray = [];
        calArray.push(getStorageCal());
        calArray.push(getMemoryCal());
        for (let calendar of calArray) {
            // implement listener
            let count = 0;
            let returnedItem = null;
            let listener = {
                onOperationComplete: function(aCalendar,
                                              aStatus,
                                              aOperationType,
                                              aId,
                                              aDetail) {
                    equal(aStatus, 0);
                    if (aOperationType == Ci.calIOperationListener.ADD) {
                        compareItemsSpecific(aDetail, aItem);
                        // perform getItem() on calendar
                        aCalendar.getItem(aId, listener);
                    } else if (aOperationType == Ci.calIOperationListener.GET) {
                        equal(count, 1);
                        compareItemsSpecific(returnedItem, aItem);
                    }
                },
                onGetResult: function(aCalendar,
                                      aStatus,
                                      aItemType,
                                      aDetail,
                                      aCount,
                                      aItems) {
                    if (aCount) {
                        count += aCount;
                        returnedItem = aItems[0];
                    }
                }
            };
            // add item to calendar
            calendar.addItem(aItem, listener);
        }
    }
}

function testMetaData() {
    function testMetaData_(aCalendar) {
        dump("testMetaData_() calendar type: " + aCalendar.type + "\n");
        let event1 = createEventFromIcalString("BEGIN:VEVENT\n" +
                                               "DTSTART;VALUE=DATE:20020402\n" +
                                               "END:VEVENT\n");

        event1.id = "item1";
        aCalendar.addItem(event1, null);
        aCalendar.setMetaData("item1", "meta1");
        equal(aCalendar.getMetaData("item1"), "meta1");
        equal(aCalendar.getMetaData("unknown"), null);

        let event2 = event1.clone();
        event2.id = "item2";
        aCalendar.addItem(event2, null);
        aCalendar.setMetaData("item2", "meta2-");
        equal(aCalendar.getMetaData("item2"), "meta2-");

        aCalendar.setMetaData("item2", "meta2");
        equal(aCalendar.getMetaData("item2"), "meta2");

        let count = {};
        let ids = {};
        let values = {};
        aCalendar.getAllMetaData(count, ids, values);
        equal(count.value, 2);
        ok(ids.value[0] == "item1" || ids.value[1] == "item1");
        ok(ids.value[0] == "item2" || ids.value[1] == "item2");
        ok(values.value[0] == "meta1" || values.value[1] == "meta1");
        ok(values.value[0] == "meta2" || values.value[1] == "meta2");

        aCalendar.deleteItem(event1, null);
        equal(aCalendar.getMetaData("item1"), null);
        aCalendar.getAllMetaData(count, ids, values);
        equal(count.value, 1);
        ok(ids.value[0] == "item2");
        ok(values.value[0] == "meta2");

        aCalendar.deleteMetaData("item2");
        equal(aCalendar.getMetaData("item2"), null);
        aCalendar.getAllMetaData(count, ids, values);
        equal(count.value, 0);

        aCalendar.setMetaData("item2", "meta2");
        equal(aCalendar.getMetaData("item2"), "meta2");
        aCalendar.QueryInterface(Ci.calICalendarProvider).deleteCalendar(aCalendar, null);
        equal(aCalendar.getMetaData("item2"), null);
        aCalendar.getAllMetaData(count, ids, values);
        equal(count.value, 0);

        aCalendar.deleteMetaData("unknown"); // check graceful return
    }

    testMetaData_(getMemoryCal());
    testMetaData_(getStorageCal());
}

async function testOfflineStorage(storageGetter, isRecurring) {
    let storage = storageGetter();
    print(`Running offline storage test for ${storage.type} calendar for ${isRecurring ? "recurring" : "normal"} item`);
    let pcal = cal.async.promisifyCalendar(storage);

    let event1 = createEventFromIcalString("BEGIN:VEVENT\n" +
                                           "DTSTART;VALUE=DATE:20020402\n" +
                                           "DTEND;VALUE=DATE:20020403\n" +
                                           "SUMMARY:event1\n" +
                                           (isRecurring ? "RRULE:FREQ=DAILY;INTERVAL=1;COUNT=10\n" : "") +
                                           "END:VEVENT\n");

    event1 = await pcal.addItem(event1);

    // Make sure the event is really in the calendar
    let result = await pcal.getAllItems();
    equal(result.length, 1);

    // When searching for offline added items, there are none
    let filter = cIC.ITEM_FILTER_ALL_ITEMS | cIC.ITEM_FILTER_OFFLINE_CREATED;
    result = await pcal.getItems(filter, 0, null, null);
    equal(result.length, 0);

    // Mark the item as offline added
    await pcal.addOfflineItem(event1);

    // Now there should be an offline item
    result = await pcal.getItems(filter, 0, null, null);
    equal(result.length, 1);

    let event2 = event1.clone();
    event2.title = "event2";

    event2 = await pcal.modifyItem(event2, event1);

    await pcal.modifyOfflineItem(event2);

    // The flag should still be offline added, as it was already marked as such
    filter = cIC.ITEM_FILTER_ALL_ITEMS | cIC.ITEM_FILTER_OFFLINE_CREATED;
    result = await pcal.getItems(filter, 0, null, null);
    equal(result.length, 1);

    // Reset the flag
    await pcal.resetItemOfflineFlag(event2);

    // No more offline items after resetting the flag
    filter = cIC.ITEM_FILTER_ALL_ITEMS | cIC.ITEM_FILTER_OFFLINE_CREATED;
    result = await pcal.getItems(filter, 0, null, null);
    equal(result.length, 0);

    // Setting modify flag without one set should actually set that flag
    await pcal.modifyOfflineItem(event2);
    filter = cIC.ITEM_FILTER_ALL_ITEMS | cIC.ITEM_FILTER_OFFLINE_CREATED;
    result = await pcal.getItems(filter, 0, null, null);
    equal(result.length, 0);

    filter = cIC.ITEM_FILTER_ALL_ITEMS | cIC.ITEM_FILTER_OFFLINE_MODIFIED;
    result = await pcal.getItems(filter, 0, null, null);
    equal(result.length, 1);

    // Setting the delete flag should modify the flag accordingly
    await pcal.deleteOfflineItem(event2);
    filter = cIC.ITEM_FILTER_ALL_ITEMS | cIC.ITEM_FILTER_OFFLINE_MODIFIED;
    result = await pcal.getItems(filter, 0, null, null);
    equal(result.length, 0);

    filter = cIC.ITEM_FILTER_ALL_ITEMS | cIC.ITEM_FILTER_OFFLINE_DELETED;
    result = await pcal.getItems(filter, 0, null, null);
    equal(result.length, 1);

    // Setting the delete flag on an offline added item should remove it
    await pcal.resetItemOfflineFlag(event2);
    await pcal.addOfflineItem(event2);
    await pcal.deleteOfflineItem(event2);
    result = await pcal.getAllItems();
    equal(result.length, 0);
}

add_task(testOfflineStorage.bind(null, () => getMemoryCal(), false));
add_task(testOfflineStorage.bind(null, () => getStorageCal(), false));
add_task(testOfflineStorage.bind(null, () => getMemoryCal(), true));
add_task(testOfflineStorage.bind(null, () => getStorageCal(), true));
