/* 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/. */

// ChromeUtils.import should be used for this, but it breaks mozmill.
// Assume whatever test loaded this file already has mailTestUtils.
/* globals mailTestUtils */

var { MailServices } = ChromeUtils.importESModule(
  "resource:///modules/MailServices.sys.mjs"
);

var allTestedEvents =
  MailServices.mfn.msgAdded |
  MailServices.mfn.msgsClassified |
  MailServices.mfn.msgsJunkStatusChanged |
  MailServices.mfn.msgsDeleted |
  MailServices.mfn.msgsMoveCopyCompleted |
  MailServices.mfn.msgKeyChanged |
  MailServices.mfn.msgUnincorporatedMoved |
  MailServices.mfn.folderAdded |
  MailServices.mfn.folderDeleted |
  MailServices.mfn.folderMoveCopyCompleted |
  MailServices.mfn.folderRenamed |
  MailServices.mfn.folderCompactStart |
  MailServices.mfn.folderCompactFinish |
  MailServices.mfn.folderReindexTriggered;

// Current test being executed
var gTest = 1;

// Which events are expected
var gExpectedEvents = [];

// The current status (what all has been done)
var gCurrStatus = 0;
var kStatus = {
  notificationsDone: 0x1,
  onStopCopyDone: 0x2,
  functionCallDone: 0x4,
  everythingDone: 0,
};
kStatus.everythingDone =
  kStatus.notificationsDone | kStatus.onStopCopyDone | kStatus.functionCallDone;

// For copyFileMessage: this stores the header that was received
var gHdrsReceived = [];

var gMsgHdrs = [];

// Our listener, which captures events and verifies them as they are received.
var gMFListener = {
  msgAdded(aMsg) {
    verify([MailServices.mfn.msgAdded, aMsg]);
    // We might not actually have a header in gHdrsReceived in the IMAP case,
    // so use the aMsg we got instead
    gMsgHdrs.push({ hdr: aMsg, ID: aMsg.messageId });
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    } else if (gExpectedEvents[0][0] == MailServices.mfn.msgsClassified) {
      // XXX this is a hack to deal with limitations of the classification logic
      //  and the new list.  We want to issue a call to clear the list once all
      //  the messages have been added, which would be when the next expected
      //  event is msgsClassified.  (The limitation is that if we don't do this,
      //  we can end up getting told about this message again later.)
      aMsg.folder.clearNewMessages();
    }
  },

  msgsClassified(aMsgs, aJunkProcessed, aTraitProcessed) {
    dump("classified id: " + aMsgs[0].messageId + "\n");
    verify([
      MailServices.mfn.msgsClassified,
      aMsgs,
      aJunkProcessed,
      aTraitProcessed,
    ]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  msgsJunkStatusChanged(messages) {
    verify([MailServices.mfn.msgsJunkStatusChanged, messages]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  msgsDeleted(aMsgs) {
    verify([MailServices.mfn.msgsDeleted, aMsgs]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  msgsMoveCopyCompleted(aMove, aSrcMsgs, aDestFolder, aDestMsgs) {
    verify([
      MailServices.mfn.msgsMoveCopyCompleted,
      aMove,
      aSrcMsgs,
      aDestFolder,
      aDestMsgs,
    ]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  msgKeyChanged(aOldKey, aNewMsgHdr) {
    verify([MailServices.mfn.msgKeyChanged, aOldKey, aNewMsgHdr]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  msgUnincorporatedMoved(srcFolder, msg) {
    verify([MailServices.mfn.msgUnincorporatedMoved, srcFolder, msg]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  folderAdded(aFolder) {
    verify([MailServices.mfn.folderAdded, aFolder]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  folderDeleted(aFolder) {
    verify([MailServices.mfn.folderDeleted, aFolder]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
    verify([
      MailServices.mfn.folderMoveCopyCompleted,
      aMove,
      aSrcFolder,
      aDestFolder,
    ]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  folderRenamed(aOrigFolder, aNewFolder) {
    verify([MailServices.mfn.folderRenamed, aOrigFolder, aNewFolder]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  folderCompactStart(folder) {
    verify([MailServices.mfn.folderCompactStart, folder]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  folderCompactFinish(folder) {
    verify([MailServices.mfn.folderCompactFinish, folder]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },

  folderReindexTriggered(folder) {
    verify([MailServices.mfn.folderReindexTriggered, folder]);
    if (gExpectedEvents.length == 0) {
      gCurrStatus |= kStatus.notificationsDone;
      if (gCurrStatus == kStatus.everythingDone) {
        resetStatusAndProceed();
      }
    }
  },
};

/**
 * Copy listener, for proceeding after each operation.
 *
 * @implements {nsIMsgCopyServiceListener}
 */
var copyListener = {
  // For copyFileMessage: this should be the folder the message is being stored to
  mFolderStoredIn: null,
  mMessageId: "",
  onStartCopy() {},
  onProgress() {},
  setMessageKey(aKey) {
    gHdrsReceived.push(this.mFolderStoredIn.GetMessageHeader(aKey));
  },
  getMessageId() {
    return this.mMessageId;
  },
  onStopCopy(aStatus) {
    // Check: message successfully copied.
    Assert.equal(aStatus, 0);
    gCurrStatus |= kStatus.onStopCopyDone;
    if (gCurrStatus == kStatus.everythingDone) {
      resetStatusAndProceed();
    }
  },
};

function resetStatusAndProceed() {
  gHdrsReceived.length = 0;
  gCurrStatus = 0;
  // Ugly hack: make sure we don't get stuck in a JS->C++->JS->C++... call stack
  // This can happen with a bunch of synchronous functions grouped together, and
  // can even cause tests to fail because they're still waiting for the listener
  // to return
  do_timeout(0, () => {
    this.doTest(++gTest);
  });
}

// Checks whether the array returned from a function has exactly these elements.
function hasExactlyElements(array, elements) {
  // If an nsIArray (it could also be a single header or a folder)
  if (elements instanceof Ci.nsIArray) {
    var count = elements.length;

    // Check: array sizes should be equal.
    Assert.equal(count, array.length);

    for (let i = 0; i < count; i++) {
      // Check: query element, must be a header or folder and present in the array
      var currElement;
      try {
        currElement = elements.queryElementAt(i, Ci.nsIMsgDBHdr);
      } catch (e) {}
      if (!currElement) {
        try {
          currElement = elements.queryElementAt(i, Ci.nsIMsgFolder);
        } catch (e) {}
      }
      Assert.equal(typeof currElement, "object");
      Assert.notEqual(
        mailTestUtils.non_strict_index_of(array, currElement),
        -1
      );
    }
  } else if (Array.isArray(elements)) {
    Assert.equal(elements.length, array.length);
    for (const el of elements) {
      Assert.equal(typeof el, "object");
      Assert.equal(
        el instanceof Ci.nsIMsgDBHdr || el instanceof Ci.nsIMsgFolder,
        true
      );
      Assert.notEqual(mailTestUtils.non_strict_index_of(array, el), -1);
    }
  } else if (
    elements instanceof Ci.nsIMsgDBHdr ||
    elements instanceof Ci.nsIMsgFolder
  ) {
    // If a single header or a folder

    // Check: there should be only one element in the array.
    Assert.equal(array.length, 1);

    // Check: the element should be present
    Assert.notEqual(mailTestUtils.non_strict_index_of(array, elements), -1);
  } else {
    // This shouldn't happen
    do_throw("Unrecognized item returned from listener");
  }
}

// Verifies an event
function verify(event) {
  // Check: make sure we actually have an item to process
  Assert.ok(gExpectedEvents.length >= 1);
  var expected = gExpectedEvents.shift();

  // Check: events match.
  var eventType = expected[0];
  Assert.equal(event[0], eventType);

  dump("..... Verifying event type " + eventType + "\n");

  switch (eventType) {
    case MailServices.mfn.msgAdded:
      // So for IMAP right now, we aren't able to get the actual nsIMsgDBHdr.
      // Instead, we'll match up message ids as a (poor?) substitute.
      if (expected[1].expectedMessageId) {
        Assert.equal(expected[1].expectedMessageId, event[1].messageId);
        break;
      }
    // If we do have a header, fall through to the case below
    case MailServices.mfn.msgsDeleted:
    case MailServices.mfn.folderDeleted:
      // Check: headers match/folder matches.
      hasExactlyElements(expected[1], event[1]);
      break;
    case MailServices.mfn.msgsClassified:
      // In the IMAP case expected[1] is a list of mesage-id strings whereas in
      // the local case (where we are copying from files), we actually have
      // the headers.
      if (typeof expected[1][0] == "string") {
        // IMAP; message id strings
        // The IMAP case has additional complexity in that the 'new message'
        // list is not tailored to our needs and so may over-report about
        // new messagse.  So to deal with this we make sure the msgsClassified
        // event is telling us about at least the N expected events and that
        // the last N of these events match
        if (event[1].length < expected[1].length) {
          do_throw("Not enough reported classified messages.");
        }
        const ignoreCount = event[1].length - expected[1].length;
        for (let i = 0; i < expected[1].length; i++) {
          const eventHeader = event[1][i + ignoreCount];
          Assert.equal(expected[1][i], eventHeader.messageId);
        }
      } else {
        // actual headers
        hasExactlyElements(expected[1], event[1]);
      }
      // aJunkProcessed: was the message processed for junk?
      Assert.equal(expected[2], event[2]);
      // aTraitProcessed: was the message processed for traits?
      Assert.equal(expected[3], event[3]);
      break;
    case MailServices.mfn.msgsJunkStatusChanged:
      // Check: same messages?
      hasExactlyElements(expected[1], event[1]);
      break;
    case MailServices.mfn.msgKeyChanged:
      Assert.equal(expected[1].expectedMessageId, event[2].messageId);
      break;
    case MailServices.mfn.msgUnincorporatedMoved:
      // Check: Same folder?
      Assert.equal(expected[1].URI, event[1].URI);
      // Check: message matches?
      hasExactlyElements(expected[2], event[2]);
      break;
    case MailServices.mfn.msgsMoveCopyCompleted:
    case MailServices.mfn.folderMoveCopyCompleted:
      // Check: Move or copy as expected.
      Assert.equal(expected[1], event[1]);

      // Check: headers match/folder matches.
      hasExactlyElements(expected[2], event[2]);

      // Check: destination folder matches.
      Assert.equal(expected[3].URI, event[3].URI);

      if (eventType == MailServices.mfn.folderMoveCopyCompleted) {
        break;
      }

      // Check: destination headers.  We expect these for local and imap folders,
      //  but we will not have heard about the headers ahead of time,
      //  so the best we can do is make sure they match up.  To this end,
      //  we check that the message-id header values match up.
      for (let iMsg = 0; iMsg < event[2].length; iMsg++) {
        const srcHdr = event[2][iMsg];
        const destHdr = event[4][iMsg];
        Assert.equal(srcHdr.messageId, destHdr.messageId);
      }
      break;
    case MailServices.mfn.folderAdded:
      // Check: parent folder matches
      Assert.equal(expected[1].URI, event[1].parent.URI);

      // Check: folder name matches
      Assert.equal(expected[2], event[1].prettyName);
      Assert.equal(expected[2], event[1].name);

      // Not a check, but call the passed in callback with the new folder,
      // used e.g. to store this folder somewhere.
      if (expected[3]) {
        expected[3](event[1]);
      }
      break;
    case MailServices.mfn.folderRenamed:
      // Check: source folder matches
      hasExactlyElements(expected[1], event[1]);

      // Check: destination folder name matches
      Assert.equal(expected[2], event[2].prettyName);
      break;
    case MailServices.mfn.folderCompactStart:
    case MailServices.mfn.folderCompactFinish:
    case MailServices.mfn.folderReindexTriggered:
      // Check: same folder?
      Assert.equal(expected[1].URI, event[1].URI);
      break;
  }
}
