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

/*
 * Tests imap msg header download chunking
 */

var { MailServices } = ChromeUtils.import(
  "resource:///modules/MailServices.jsm"
);
var { MessageGenerator, MessageScenarioFactory } = ChromeUtils.import(
  "resource://testing-common/mailnews/MessageGenerator.jsm"
);
var { PromiseTestUtils } = ChromeUtils.import(
  "resource://testing-common/mailnews/PromiseTestUtils.jsm"
);
var { TestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TestUtils.sys.mjs"
);

/**
 * Keep it so that OVERALL_MESSAGES % CHUNKING_SIZE !== 0.
 * With a modulo operator for CHUNKING_SIZE and a prime number for
 *  OVERALL_MESSAGES this should prove that there have been a
 *  chunking process without being depended on the first chunk.
 */
const CHUNKING_SIZE = 3;
const OVERALL_MESSAGES = 137;

// Dummy message window so we can say the inbox is open in a window.
var dummyMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
  Ci.nsIMsgWindow
);

function FolderIntPropertyChangedListener() {
  this._promise = new Promise(resolve => {
    this._resolve = resolve;
  });
  this._gotNewMailBiff = false;
}

FolderIntPropertyChangedListener.prototype = {
  onFolderIntPropertyChanged(aItem, aProperty, aOldValue, aNewValue) {
    if (
      aProperty == "BiffState" &&
      aNewValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail
    ) {
      this._gotNewMailBiff = true;
      this._resolve();
    }
  },
  get promise() {
    return this._promise;
  },
  get gotNewMailBiff() {
    return this._gotNewMailBiff;
  },
};

var gFolderListener = new FolderIntPropertyChangedListener();
/** Used to store a listener between tasks for inspecting chunking behaviour. */
var gListener = new PromiseTestUtils.PromiseUrlListener();

add_setup(async function () {
  Assert.equal(
    OVERALL_MESSAGES % CHUNKING_SIZE !== 0,
    true,
    "const sanity check"
  );
  setupIMAPPump();
  // We need to register the dummyMsgWindow so that we'll think the
  //  Inbox is open in a folder and fetch headers in chunks.
  dummyMsgWindow.openFolder = IMAPPump.inbox;
  MailServices.mailSession.AddMsgWindow(dummyMsgWindow);
  MailServices.mailSession.AddFolderListener(
    gFolderListener,
    Ci.nsIFolderListener.intPropertyChanged
  );

  // Set chunk size to CHUNKING_SIZE, so we'll have to chain several requests to get
  //  OVERALL_MESSAGES headers.
  Services.prefs.setIntPref("mail.imap.hdr_chunk_size", CHUNKING_SIZE);
  // Turn off offline sync to avoid complications in verifying that we can
  //  run a url after the first header chunk.
  Services.prefs.setBoolPref(
    "mail.server.server1.autosync_offline_stores",
    false
  );
});

// Upload messages to the imap fake server Inbox.
add_task(async function uploadImapMessages() {
  // make OVERALL_MESSAGES messages
  let messageGenerator = new MessageGenerator();
  let scenarioFactory = new MessageScenarioFactory(messageGenerator);

  // build up a list of messages
  let messages = [];
  messages = messages.concat(scenarioFactory.directReply(OVERALL_MESSAGES));

  // Add OVERALL_MESSAGES messages with uids 1,2,3...,OVERALL_MESSAGES.
  let imapInbox = IMAPPump.daemon.getMailbox("INBOX");
  // Create the ImapMessages and store them on the mailbox.
  messages.forEach(function (message) {
    let dataUri = Services.io.newURI(
      "data:text/plain;base64," + btoa(message.toMessageString())
    );
    imapInbox.addMessage(
      new ImapMessage(dataUri.spec, imapInbox.uidnext++, [])
    );
  });
  // Do not wait for the listener to finish.
  // We want to observe the message batches in the update process.
  // updateFolderWithListener with null for nsIMsgWindow makes biff notify.
  IMAPPump.inbox.updateFolderWithListener(null, gListener);
});

add_task(async function testMessageFetched() {
  // If we're really chunking, then the message fetch should have started before
  // we finished the updateFolder URL.
  await TestUtils.waitForCondition(() => {
    return gFolderListener.gotNewMailBiff === true;
  });
  Assert.ok(gFolderListener.gotNewMailBiff);

  // We do not check for the first chunk as this is unreliable without explicit
  //  listeners/events.
  // Instead we are checking if there's no rest of the division with
  //  CHUNKING_SIZE while the chunking process is ongoing.
  // It's important that the chunking is intact and as well not failing
  //  randomly in the test infrastructure.
  // See at the CHUNKING_SIZE and OVERALL_MESSAGES declarations.
  //
  // HINT:
  // If this causes future problems because stuff getting faster,
  //  try to increase the overall message count.
  await TestUtils.waitForCondition(() => {
    let messagesDBFolder = IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages;
    if (messagesDBFolder !== 0) {
      Assert.equal(
        messagesDBFolder % CHUNKING_SIZE,
        0,
        `${messagesDBFolder} messages in folder should be of chunk size ${CHUNKING_SIZE}`
      ); // This is the primary test.
      return true;
    } else if (messagesDBFolder === OVERALL_MESSAGES) {
      throw new Error(
        `Batching failed in sizes of ${CHUNKING_SIZE} found instead ${OVERALL_MESSAGES} immediately`
      );
    }
    return false; // Rerun waitForCondition.
  }, 50);
});

add_task(async function testHdrsDownloaded() {
  await gListener.promise; // Now we wait for the finished update of the Folder.
  // Make sure that we got all OVERALL_MESSAGES headers.
  Assert.equal(
    IMAPPump.inbox.msgDatabase.dBFolderInfo.numMessages,
    OVERALL_MESSAGES
  );
});

// Cleanup
add_task(async function endTest() {
  teardownIMAPPump();
});
