/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 *
 * Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/licenses/publicdomain/
 *
 * ***** END LICENSE BLOCK ***** */

 /*
  * Test suite for folder compaction
  *
  * Currently tested:
  * - Compacting local folders
  * TODO
  * - Compacting imap offline stores.
  */

var {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
const {PromiseTestUtils} = ChromeUtils.import("resource://testing-common/mailnews/PromiseTestUtils.jsm");

Services.prefs.setCharPref("mail.serverDefaultStoreContractID",
                           "@mozilla.org/msgstore/berkeleystore;1");

// Globals
var gMsgFile1, gMsgFile2, gMsgFile3;
var gMsg2ID = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org";
var gMsg3ID = "4849BF7B.2030800@example.com";
var gLocalFolder2;
var gLocalFolder3;
// After a compact (or other operation), this is what we expect the
// folder size to be.
var gExpectedFolderSize;
var gMsgHdrs = [];
var gExpectedInboxSize;
var gExpectedFolder2Size;
var gExpectedFolder3Size;
var gMsgLinebreak = "";

// Transfer message keys between function calls.
var gMsgKeys = [];

// nsIMsgCopyServiceListener implementation
var copyListenerWrap = {
  SetMessageKey(aKey) {
    let hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
    gMsgHdrs.push({hdr, ID: hdr.messageId});
  },
  OnStopCopy(aStatus) {
    // Check: message successfully copied.
    Assert.equal(aStatus, 0);
  },
};

var urlListenerWrap = {
  OnStopRunningUrl(aUrl, aExitCode) {
    // Check: message successfully copied.
    Assert.equal(aExitCode, 0);

    if (gMsgKeys.length > 0) {
      // Bug 854798: Check if the new message keys are the same as before compaction.
      let folderMsgs = gMsgKeys.folder.messages;
      // First message was deleted so skip it in the old array.
      for (let i = 1; i < gMsgKeys.length; i++) {
        Assert.ok(folderMsgs.hasMoreElements());
        let header = folderMsgs.getNext().QueryInterface(Ci.nsIMsgDBHdr);
        Assert.equal(header.messageKey, gMsgKeys[i]);
      }
      Assert.ok(!folderMsgs.hasMoreElements());
      gMsgKeys.length = 0;
    }
  },
};

function copyFileMessage(file, destFolder, isDraftOrTemplate) {
  let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
  MailServices.copy.CopyFileMessage(file, destFolder, null, isDraftOrTemplate, 0, "", listener, null);
  return listener.promise;
}

function copyMessages(items, isMove, srcFolder, destFolder) {
  let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
  var array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
  items.forEach(function(item) {
    array.appendElement(item);
  });
  MailServices.copy.CopyMessages(srcFolder, array, destFolder, isMove, listener, null, true);
  return listener.promise;
}

function deleteMessages(srcFolder, items) {
  var array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
  items.forEach(function(item) {
    array.appendElement(item);
  });

  let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
  srcFolder.deleteMessages(array, null, false, true, listener, true);
  return listener.promise;
}

function calculateFolderSize(folder) {
  let msgDB = folder.msgDatabase;
  let enumerator = msgDB.EnumerateMessages();
  let totalSize = 0;
  if (enumerator) {
    if (gMsgLinebreak == "") {
      // Figure out what the linebreak sequence is on the platform running the tests.
      gMsgLinebreak = "@mozilla.org/windows-registry-key;1" in Cc ? "\r\n" : "\n";
    }
    while (enumerator.hasMoreElements()) {
      var header = enumerator.getNext();
      if (header instanceof Ci.nsIMsgDBHdr)
        totalSize += header.messageSize + gMsgLinebreak.length;
    }
  }
  return totalSize;
}

function verifyMsgOffsets(folder) {
  let msgDB = folder.msgDatabase;
  let enumerator = msgDB.EnumerateMessages();
  if (enumerator) {
    while (enumerator.hasMoreElements()) {
      let header = enumerator.getNext();
      if (header instanceof Ci.nsIMsgDBHdr) {
        let storeToken = header.getStringProperty("storeToken");
        Assert.equal(storeToken, header.messageOffset);
      }
    }
  }
}

/*
 * TESTS
 */

// Beware before commenting out a test -- later tests might just depend on earlier ones
var gTestArray = [
  // Copying messages from files
  async function testCopyFileMessage1() {
    await copyFileMessage(gMsgFile1, localAccountUtils.inboxFolder, false);
  },
  async function testCopyFileMessage2() {
    await copyFileMessage(gMsgFile2, localAccountUtils.inboxFolder, false);
  },
  async function testCopyFileMessage3() {
    await copyFileMessage(gMsgFile3, localAccountUtils.inboxFolder, true);
    showMessages(localAccountUtils.inboxFolder, "after initial 3 messages copy to inbox");
  },

  // Moving/copying messages
  async function testCopyMessages1() {
    await copyMessages([gMsgHdrs[0].hdr], false, localAccountUtils.inboxFolder, gLocalFolder2);
  },
  async function testCopyMessages2() {
    await copyMessages([gMsgHdrs[1].hdr, gMsgHdrs[2].hdr], false, localAccountUtils.inboxFolder, gLocalFolder2);
    showMessages(gLocalFolder2, "after copying 3 messages");
  },
  async function testMoveMessages1() {
    await copyMessages([gMsgHdrs[0].hdr, gMsgHdrs[1].hdr], true, localAccountUtils.inboxFolder, gLocalFolder3);

    showMessages(localAccountUtils.inboxFolder, "after moving 2 messages");
    showMessages(gLocalFolder3, "after moving 2 messages");
  },

  // Deleting messages
  async function testDeleteMessages1() { // delete to trash
    // Let's take a moment to re-initialize stuff that got moved
    var folder3DB = gLocalFolder3.msgDatabase;
    gMsgHdrs[0].hdr = folder3DB.getMsgHdrForMessageID(gMsgHdrs[0].ID);

    // Store message keys before deletion and compaction.
    gMsgKeys.folder = gLocalFolder3;
    let folderMsgs = gLocalFolder3.messages;
    while (folderMsgs.hasMoreElements()) {
      let header = folderMsgs.getNext().QueryInterface(Ci.nsIMsgDBHdr);
      gMsgKeys.push(header.messageKey);
    }

    // Now delete the message
    await deleteMessages(gLocalFolder3, [gMsgHdrs[0].hdr], false, false);

    showMessages(gLocalFolder3, "after deleting 1 message to trash");
  },
  async function compactFolder() {
    gExpectedFolderSize = calculateFolderSize(gLocalFolder3);
    Assert.notEqual(gLocalFolder3.expungedBytes, 0);
    let listener = new PromiseTestUtils.PromiseUrlListener(urlListenerWrap);
    gLocalFolder3.compact(listener, null);
    await listener.promise;

    showMessages(gLocalFolder3, "after compact");
  },
  async function testDeleteMessages2() {
    Assert.equal(gExpectedFolderSize, gLocalFolder3.filePath.fileSize);
    verifyMsgOffsets(gLocalFolder3);
    var folder2DB = gLocalFolder2.msgDatabase;
    gMsgHdrs[0].hdr = folder2DB.getMsgHdrForMessageID(gMsgHdrs[0].ID);

    // Store message keys before deletion and compaction.
    gMsgKeys.folder = gLocalFolder2;
    let folderMsgs = gLocalFolder2.messages;
    while (folderMsgs.hasMoreElements()) {
      let header = folderMsgs.getNext().QueryInterface(Ci.nsIMsgDBHdr);
      gMsgKeys.push(header.messageKey);
    }

    // Now delete the message
    await deleteMessages(gLocalFolder2, [gMsgHdrs[0].hdr], false, false);

    showMessages(gLocalFolder2, "after deleting 1 message");
  },
  async function compactAllFolders() {
    gExpectedInboxSize = calculateFolderSize(localAccountUtils.inboxFolder);
    gExpectedFolder2Size = calculateFolderSize(gLocalFolder2);
    gExpectedFolder3Size = calculateFolderSize(gLocalFolder3);

    // Save the first message key, which will change after compact with
    // rebuild.
    let f2m2Key =
      gLocalFolder2.msgDatabase.getMsgHdrForMessageID(gMsg2ID).messageKey;

    // force expunged bytes count to get cached.
    gLocalFolder2.expungedBytes;
    // mark localFolder2 as having an invalid db, and remove it
    // for good measure.
    gLocalFolder2.msgDatabase.summaryValid = false;
    gLocalFolder2.msgDatabase = null;
    gLocalFolder2.ForceDBClosed();
    let dbPath = gLocalFolder2.filePath;
    dbPath.leafName = dbPath.leafName + ".msf";
    dbPath.remove(false);

    showMessages(localAccountUtils.inboxFolder, "before compactAll");
    // Save the key for the inbox message, we'll check after compact that it
    // did not change.
    let preInboxMsg3Key = localAccountUtils.inboxFolder.msgDatabase
                                           .getMsgHdrForMessageID(gMsg3ID)
                                           .messageKey;

    // We used to check here that the keys did not change during rebuild.
    // But that is no true in general, it was only conicidental since the
    // checked folder had never been compacted, so the key equaled the offset.
    // We do not in guarantee that, indeed after rebuild we expect the keys
    // to change.
    let checkResult = {
      OnStopRunningUrl(aUrl, aExitCode) {
        // Check: message successfully compacted.
        Assert.equal(aExitCode, 0);
      },
    };
    let listener = new PromiseTestUtils.PromiseUrlListener(checkResult);
    localAccountUtils.inboxFolder.compactAll(listener, null, true);
    await listener.promise;

    showMessages(localAccountUtils.inboxFolder, "after compactAll");
    showMessages(gLocalFolder2, "after compactAll");

    // For the inbox, which was compacted but not rebuild, key is unchanged.
    let postInboxMsg3Key = localAccountUtils.inboxFolder.msgDatabase
                                            .getMsgHdrForMessageID(gMsg3ID)
                                            .messageKey;
    Assert.equal(preInboxMsg3Key, postInboxMsg3Key);

    // For folder2, which was rebuilt, keys change but all messages should exist.
    let message2 = gLocalFolder2.msgDatabase.getMsgHdrForMessageID(gMsg2ID);
    Assert.ok(message2);
    Assert.ok(gLocalFolder2.msgDatabase.getMsgHdrForMessageID(gMsg3ID));

    // In folder2, gMsg2ID is the first message. After compact with database
    // rebuild, that key has now changed.
    Assert.notEqual(message2.messageKey, f2m2Key);
  },
  function lastTestCheck() {
    Assert.equal(gExpectedInboxSize, localAccountUtils.inboxFolder.filePath.fileSize);
    Assert.equal(gExpectedFolder2Size, gLocalFolder2.filePath.fileSize);
    Assert.equal(gExpectedFolder3Size, gLocalFolder3.filePath.fileSize);
    verifyMsgOffsets(gLocalFolder2);
    verifyMsgOffsets(gLocalFolder3);
    verifyMsgOffsets(localAccountUtils.inboxFolder);
  },
];

function run_test() {
  localAccountUtils.loadLocalMailAccount();
  // Load up some messages so that we can copy them in later.
  gMsgFile1 = do_get_file("../../../data/bugmail10");
  gMsgFile2 = do_get_file("../../../data/bugmail11");
  gMsgFile3 = do_get_file("../../../data/draft1");

  // Create another folder to move and copy messages around, and force initialization.
  gLocalFolder2 = localAccountUtils.rootFolder.createLocalSubfolder("folder2");

  // Create a third folder for more testing.
  gLocalFolder3 = localAccountUtils.rootFolder.createLocalSubfolder("folder3");

  gTestArray.forEach(x => add_task(x));
  run_next_test();
}

// debug utility to show the key/offset/ID relationship of messages in a folder
function showMessages(folder, text) {
  dump("Show messages for folder <" + folder.name + "> " + text + "\n");
  let folderMsgs = folder.messages;
  while (folderMsgs.hasMoreElements()) {
    let header = folderMsgs.getNext().QueryInterface(Ci.nsIMsgDBHdr);
    dump("key: " + header.messageKey +
          " offset: " + header.messageOffset +
          " ID: " + header.messageId +
          "\n");
  }
}
