/* 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 Filelink URL insertion behaviours in compose windows.
 */

"use strict";

var {
  gMockFilePicker,
  gMockFilePickReg,
  select_attachments,
} = ChromeUtils.import(
  "resource://testing-common/mozmill/AttachmentHelpers.jsm"
);
var { gMockCloudfileManager, MockCloudfileAccount } = ChromeUtils.import(
  "resource://testing-common/mozmill/CloudfileHelpers.jsm"
);
var {
  add_cloud_attachments,
  assert_previous_text,
  close_compose_window,
  get_compose_body,
  open_compose_new_mail,
  open_compose_with_forward,
  open_compose_with_reply,
  type_in_composer,
} = ChromeUtils.import("resource://testing-common/mozmill/ComposeHelpers.jsm");
var {
  assert_next_nodes,
  assert_previous_nodes,
  wait_for_element,
} = ChromeUtils.import("resource://testing-common/mozmill/DOMHelpers.jsm");
var {
  add_message_to_folder,
  assert_selected_and_displayed,
  be_in_folder,
  create_message,
  FAKE_SERVER_HOSTNAME,
  get_special_folder,
  mc,
  select_click_row,
} = ChromeUtils.import(
  "resource://testing-common/mozmill/FolderDisplayHelpers.jsm"
);

var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { MailServices } = ChromeUtils.import(
  "resource:///modules/MailServices.jsm"
);

var kUploadedFile = "attachment-uploaded";
var kHtmlPrefKey = "mail.identity.default.compose_html";
var kReplyOnTopKey = "mail.identity.default.reply_on_top";
var kReplyOnTop = 1;
var kReplyOnBottom = 0;
var kTextNodeType = 3;
var kSigPrefKey = "mail.identity.id1.htmlSigText";
var kSigOnReplyKey = "mail.identity.default.sig_on_reply";
var kSigOnForwardKey = "mail.identity.default.sig_on_fwd";
var kDefaultSigKey = "mail.identity.id1.htmlSigText";
var kDefaultSig = "This is my signature.\n\nCheck out my website sometime!";
var kFiles = ["./data/testFile1", "./data/testFile2"];
var kLines = ["This is a line of text", "and here's another!"];

var gInbox;

add_task(function setupModule(module) {
  requestLongerTimeout(3);

  // These prefs can't be set in the manifest as they contain white-space.
  Services.prefs.setStringPref(
    "mail.identity.id1.htmlSigText",
    "Tinderbox is soo 90ies"
  );
  Services.prefs.setStringPref(
    "mail.identity.id2.htmlSigText",
    "Tinderboxpushlog is the new <b>hotness!</b>"
  );

  // For replies and forwards, we'll work off a message in the Inbox folder
  // of the fake "tinderbox" account.
  let server = MailServices.accounts.FindServer(
    "tinderbox",
    FAKE_SERVER_HOSTNAME,
    "pop3"
  );
  gInbox = get_special_folder(Ci.nsMsgFolderFlags.Inbox, false, server);
  add_message_to_folder(gInbox, create_message());

  gMockFilePickReg.register();
  gMockCloudfileManager.register();

  Services.prefs.setBoolPref(kHtmlPrefKey, true);

  // Don't create paragraphs in the test.
  // The test fails if it encounters paragraphs <p> instead of breaks <br>.
  Services.prefs.setBoolPref("mail.compose.default_to_paragraph", false);
});

registerCleanupFunction(function teardownModule(module) {
  gMockCloudfileManager.unregister();
  gMockFilePickReg.unregister();
  Services.prefs.clearUserPref(kDefaultSigKey);
  Services.prefs.clearUserPref(kHtmlPrefKey);
  Services.prefs.clearUserPref("mail.compose.default_to_paragraph");
});

function setupTest() {
  // If our signature got accidentally wiped out, let's just put it back.
  Services.prefs.setCharPref(kDefaultSigKey, kDefaultSig);
}

/**
 * Given some compose window controller, wait for some Filelink URLs to be
 * inserted.
 *
 * @param aController the controller for a compose window.
 * @param aNumUrls the number of Filelink URLs that are expected.
 * @returns an array containing the root containment node, the list node, and
 *          an array of the link URL nodes.
 */
function wait_for_attachment_urls(aController, aNumUrls) {
  let mailBody = get_compose_body(aController);

  // Wait until we can find the root attachment URL node...
  let root = wait_for_element(
    mailBody.parentNode,
    "body > #cloudAttachmentListRoot"
  );

  let list = wait_for_element(
    mailBody,
    "#cloudAttachmentListRoot > #cloudAttachmentList"
  );

  let urls = null;
  aController.waitFor(function() {
    urls = mailBody.querySelectorAll(
      "#cloudAttachmentList > .cloudAttachmentItem"
    );
    return urls != null && urls.length == aNumUrls;
  });

  return [root, list, urls];
}

/**
 * Helper function that sets up the mock file picker for a series of files,
 * spawns a reply window for the first message in the gInbox, optionally
 * types some strings into the compose window, and then attaches some
 * Filelinks.
 *
 * @param aText an array of strings to type into the compose window. Each
 *              string is followed by pressing the RETURN key, except for
 *              the final string.  Pass an empty array if you don't want
 *              anything typed.
 * @param aFiles an array of filename strings for files located beneath
 *               the test directory.
 */
function prepare_some_attachments_and_reply(aText, aFiles) {
  gMockFilePicker.returnFiles = collectFiles(aFiles);

  let provider = new MockCloudfileAccount();
  provider.init("someKey");

  be_in_folder(gInbox);
  let msg = select_click_row(0);
  assert_selected_and_displayed(mc, msg);

  let cw = open_compose_with_reply();

  // If we have any typing to do, let's do it.
  type_in_composer(cw, aText);
  add_cloud_attachments(cw, provider);
  return cw;
}

/**
 * Helper function that sets up the mock file picker for a series of files,
 * spawns an inline forward compose window for the first message in the gInbox,
 * optionally types some strings into the compose window, and then attaches
 * some Filelinks.
 *
 * @param aText an array of strings to type into the compose window. Each
 *              string is followed by pressing the RETURN key, except for
 *              the final string.  Pass an empty array if you don't want
 *              anything typed.
 * @param aFiles an array of filename strings for files located beneath
 *               the test directory.
 */
function prepare_some_attachments_and_forward(aText, aFiles) {
  gMockFilePicker.returnFiles = collectFiles(aFiles);

  let provider = new MockCloudfileAccount();
  provider.init("someKey");

  be_in_folder(gInbox);
  let msg = select_click_row(0);
  assert_selected_and_displayed(mc, msg);

  let cw = open_compose_with_forward();

  // Put the selection at the beginning of the document...
  let editor = cw.window.GetCurrentEditor();
  editor.beginningOfDocument();

  // Do any necessary typing...
  type_in_composer(cw, aText);
  add_cloud_attachments(cw, provider);
  return cw;
}

/**
 * Helper function that runs a test function with signature-in-reply and
 * signature-in-forward enabled, and then runs the test again with those
 * prefs disabled.
 *
 * @param aSpecialTest a test that takes two arguments - the first argument
 *                     is the aText array of any text that should be typed,
 *                     and the second is a boolean for whether or not the
 *                     special test should expect a signature or not.
 * @param aText any text to be typed into the compose window, passed to
 *              aSpecialTest.
 */
function try_with_and_without_signature_in_reply_or_fwd(aSpecialTest, aText) {
  // By default, we have a signature included in replies, so we'll start
  // with that.
  Services.prefs.setBoolPref(kSigOnReplyKey, true);
  Services.prefs.setBoolPref(kSigOnForwardKey, true);
  aSpecialTest(aText, true);

  Services.prefs.setBoolPref(kSigOnReplyKey, false);
  Services.prefs.setBoolPref(kSigOnForwardKey, false);
  aSpecialTest(aText, false);
}

/**
 * Helper function that runs a test function without a signature, once
 * in HTML mode, and again in plaintext mode.
 *
 * @param aTest a test that takes no arguments.
 */
function try_without_signature(aTest) {
  let oldSig = Services.prefs.getCharPref(kSigPrefKey);
  Services.prefs.setCharPref(kSigPrefKey, "");

  try_with_plaintext_and_html_mail(aTest);
  Services.prefs.setCharPref(kSigPrefKey, oldSig);
}

/**
 * Helper function that runs a test function for HTML mail composition, and
 * then again in plaintext mail composition.
 *
 * @param aTest a test that takes no arguments.
 */
function try_with_plaintext_and_html_mail(aTest) {
  aTest();
  Services.prefs.setBoolPref(kHtmlPrefKey, false);
  aTest();
  Services.prefs.setBoolPref(kHtmlPrefKey, true);
}

/**
 * Test that if we open up a composer and immediately attach a Filelink,
 * a linebreak is inserted before the containment node in order to allow
 * the user to write before the attachment URLs.  This assumes the user
 * does not have a signature already inserted into the message body.
 */
add_task(function test_inserts_linebreak_on_empty_compose() {
  try_without_signature(subtest_inserts_linebreak_on_empty_compose);
});

/**
 * Subtest for test_inserts_linebreak_on_empty_compose - can be executed
 * on both plaintext and HTML compose windows.
 */
function subtest_inserts_linebreak_on_empty_compose() {
  gMockFilePicker.returnFiles = collectFiles(kFiles);
  let provider = new MockCloudfileAccount();
  provider.init("someKey");
  let cw = open_compose_new_mail();
  add_cloud_attachments(cw, provider);

  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  let br = root.previousSibling;
  Assert.equal(
    br.localName,
    "br",
    "The attachment URL containment node should be preceded by a linebreak"
  );

  let mailBody = get_compose_body(cw);

  Assert.equal(
    mailBody.firstChild,
    br,
    "The linebreak should be the first child of the compose body"
  );

  close_compose_window(cw);
}

/**
 * Test that if we open up a composer and immediately attach a Filelink,
 * a linebreak is inserted before the containment node. This test also
 * ensures that, with a signature already in the compose window, we don't
 * accidentally insert the attachment URL containment within the signature
 * node.
 */
add_task(function test_inserts_linebreak_on_empty_compose_with_signature() {
  gMockFilePicker.returnFiles = collectFiles(kFiles);
  let provider = new MockCloudfileAccount();
  provider.init("someKey");
  let cw = open_compose_new_mail();
  add_cloud_attachments(cw, provider);
  // wait_for_attachment_urls ensures that the attachment URL containment
  // node is an immediate child of the body of the message, so if this
  // succeeds, then we were not in the signature node.
  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  let br = assert_previous_nodes("br", root, 1);

  let mailBody = get_compose_body(cw);
  Assert.equal(
    mailBody.firstChild,
    br,
    "The linebreak should be the first child of the compose body"
  );

  // Now ensure that the node after the attachments is a br, and following
  // that is the signature.
  br = assert_next_nodes("br", root, 1);

  let pre = br.nextSibling;
  Assert.equal(
    pre.localName,
    "pre",
    "The linebreak should be followed by the signature pre"
  );
  Assert.ok(
    pre.classList.contains("moz-signature"),
    "The pre should have the moz-signature class"
  );

  close_compose_window(cw);

  Services.prefs.setBoolPref(kHtmlPrefKey, false);

  // Now let's try with plaintext mail.
  cw = open_compose_new_mail();
  add_cloud_attachments(cw, provider);
  [root] = wait_for_attachment_urls(cw, kFiles.length);

  br = assert_previous_nodes("br", root, 1);

  mailBody = get_compose_body(cw);
  Assert.equal(
    mailBody.firstChild,
    br,
    "The linebreak should be the first child of the compose body"
  );

  // Now ensure that the node after the attachments is a br, and following
  // that is the signature.
  br = assert_next_nodes("br", root, 1);

  let div = br.nextSibling;
  Assert.equal(
    div.localName,
    "div",
    "The linebreak should be followed by the signature div"
  );
  Assert.ok(
    div.classList.contains("moz-signature"),
    "The div should have the moz-signature class"
  );

  close_compose_window(cw);

  Services.prefs.setBoolPref(kHtmlPrefKey, true);
});

/**
 * Tests that removing all Filelinks causes the root node to be removed.
 */
add_task(function test_removing_filelinks_removes_root_node() {
  try_with_plaintext_and_html_mail(
    subtest_removing_filelinks_removes_root_node
  );
});

/**
 * Test for test_removing_filelinks_removes_root_node - can be executed
 * on both plaintext and HTML compose windows.
 */
function subtest_removing_filelinks_removes_root_node() {
  let cw = prepare_some_attachments_and_reply([], kFiles);
  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  // Now select the attachments in the attachment bucket, and remove them.
  select_attachments(cw, 0, 1);
  cw.window.goDoCommand("cmd_delete");

  // Wait for the root to be removed.
  let mailBody = get_compose_body(cw);
  cw.waitFor(function() {
    let result = mailBody.querySelector(root.id);
    return result == null;
  }, "Timed out waiting for attachment container to be removed");

  close_compose_window(cw);
}

/**
 * Test that if we write some text in an empty message (no signature),
 * and the selection is at the end of a line of text, attaching some Filelinks
 * causes the attachment URL container to be separated from the text by
 * two br tags.
 */
add_task(function test_adding_filelinks_to_written_message() {
  try_without_signature(subtest_adding_filelinks_to_written_message);
});

/**
 * Subtest for test_adding_filelinks_to_written_message - generalized for both
 * HTML and plaintext mail.
 */
function subtest_adding_filelinks_to_written_message() {
  gMockFilePicker.returnFiles = collectFiles(kFiles);
  let provider = new MockCloudfileAccount();
  provider.init("someKey");
  let cw = open_compose_new_mail();

  type_in_composer(cw, kLines);
  add_cloud_attachments(cw, provider);

  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  let br = root.previousSibling;
  Assert.equal(
    br.localName,
    "br",
    "The attachment URL containment node should be preceded by a linebreak"
  );
  br = br.previousSibling;
  Assert.equal(
    br.localName,
    "br",
    "The attachment URL containment node should be preceded by " +
      "two linebreaks"
  );
  close_compose_window(cw);
}

/**
 * Tests for inserting Filelinks into a reply, when we're configured to
 * reply above the quote.
 */
add_task(function test_adding_filelinks_to_empty_reply_above() {
  let oldReplyOnTop = Services.prefs.getIntPref(kReplyOnTopKey);
  Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnTop);

  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_reply_above,
    []
  );
  // Now with HTML mail...
  Services.prefs.setBoolPref(kHtmlPrefKey, false);
  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_reply_above_plaintext,
    []
  );

  Services.prefs.setBoolPref(kHtmlPrefKey, true);
  Services.prefs.setIntPref(kReplyOnTopKey, oldReplyOnTop);
});

/**
 * Tests for inserting Filelinks into a reply, when we're configured to
 * reply above the quote, after entering some text.
 */
add_task(function test_adding_filelinks_to_nonempty_reply_above() {
  let oldReplyOnTop = Services.prefs.getIntPref(kReplyOnTopKey);
  Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnTop);

  subtest_adding_filelinks_to_reply_above(kLines);

  Services.prefs.setBoolPref(kHtmlPrefKey, false);
  subtest_adding_filelinks_to_reply_above_plaintext(kLines);
  Services.prefs.setBoolPref(kHtmlPrefKey, true);

  Services.prefs.setIntPref(kReplyOnTopKey, oldReplyOnTop);
});

/**
 * Subtest for test_adding_filelinks_to_reply_above for the plaintext composer.
 * Does some special casing for the weird br insertions that happens in
 * various cases.
 */
function subtest_adding_filelinks_to_reply_above_plaintext(aText, aWithSig) {
  let cw = prepare_some_attachments_and_reply(aText, kFiles);
  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  let br;
  if (aText.length) {
    br = assert_next_nodes("br", root, 2);
  } else {
    br = assert_next_nodes("br", root, 1);
  }

  let div = br.nextSibling;
  Assert.equal(
    div.localName,
    "div",
    "The linebreak should be followed by a div"
  );

  Assert.ok(div.classList.contains("moz-cite-prefix"));

  if (aText.length) {
    br = assert_previous_nodes("br", root, 2);
  } else {
    br = assert_previous_nodes("br", root, 1);
  }

  if (aText.length == 0) {
    // If we didn't type anything, that br should be the first element of the
    // message body.
    let msgBody = get_compose_body(cw);
    Assert.equal(
      msgBody.firstChild,
      br,
      "The linebreak should have been the first element in the " +
        "message body"
    );
  } else {
    let targetText = aText[aText.length - 1];
    let textNode = br.previousSibling;
    Assert.equal(textNode.nodeType, kTextNodeType);
    Assert.equal(textNode.nodeValue, targetText);
  }

  close_compose_window(cw);
}

/**
 * Subtest for test_adding_filelinks_to_reply_above for the HTML composer.
 */
function subtest_adding_filelinks_to_reply_above(aText) {
  let cw = prepare_some_attachments_and_reply(aText, kFiles);
  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  // If there's any text written, then there's only a single break between the
  // end of the text and the reply. Otherwise, there are two breaks.
  let br =
    aText.length > 1
      ? assert_next_nodes("br", root, 2)
      : assert_next_nodes("br", root, 1);

  // ... which is followed by a div with a class of "moz-cite-prefix".
  let div = br.nextSibling;
  Assert.equal(
    div.localName,
    "div",
    "The linebreak should be followed by a div"
  );

  Assert.ok(div.classList.contains("moz-cite-prefix"));

  close_compose_window(cw);
}

/**
 * Tests for inserting Filelinks into a reply, when we're configured to
 * reply below the quote.
 */
add_task(function test_adding_filelinks_to_empty_reply_below() {
  let oldReplyOnTop = Services.prefs.getIntPref(kReplyOnTopKey);
  Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnBottom);

  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_reply_below,
    []
  );
  Services.prefs.setBoolPref(kHtmlPrefKey, false);
  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_plaintext_reply_below,
    []
  );
  Services.prefs.setBoolPref(kHtmlPrefKey, true);

  Services.prefs.setIntPref(kReplyOnTopKey, oldReplyOnTop);
});

/**
 * Tests for inserting Filelinks into a reply, when we're configured to
 * reply below the quote, after entering some text.
 */
add_task(function test_adding_filelinks_to_nonempty_reply_below() {
  let oldReplyOnTop = Services.prefs.getIntPref(kReplyOnTopKey);
  Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnBottom);

  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_reply_below,
    kLines
  );

  Services.prefs.setBoolPref(kHtmlPrefKey, false);
  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_plaintext_reply_below,
    kLines
  );
  Services.prefs.setBoolPref(kHtmlPrefKey, true);

  Services.prefs.setIntPref(kReplyOnTopKey, oldReplyOnTop);
});

/**
 * Subtest for test_adding_filelinks_to_reply_below for the HTML composer.
 */
function subtest_adding_filelinks_to_reply_below(aText, aWithSig) {
  let cw = prepare_some_attachments_and_reply(aText, kFiles);
  let [root] = wait_for_attachment_urls(cw, kFiles.length);
  // So, we should have the root, followed by a br
  let br = root.nextSibling;
  Assert.equal(
    br.localName,
    "br",
    "The attachment URL containment node should be followed by a br"
  );

  let blockquote;
  if (aText.length) {
    // If there was any text inserted, check for 2 previous br nodes, and then
    // the inserted text, and then the blockquote.
    br = assert_previous_nodes("br", root, 2);
    let textNode = assert_previous_text(br.previousSibling, aText);
    blockquote = textNode.previousSibling;
  } else {
    // If no text was inserted, check for 1 previous br node, and then the
    // blockquote.
    br = assert_previous_nodes("br", root, 1);
    blockquote = br.previousSibling;
  }

  Assert.equal(
    blockquote.localName,
    "blockquote",
    "The linebreak should be preceded by a blockquote."
  );

  let prefix = blockquote.previousSibling;
  Assert.equal(
    prefix.localName,
    "div",
    "The blockquote should be preceded by the prefix div"
  );
  Assert.ok(
    prefix.classList.contains("moz-cite-prefix"),
    "The prefix should have the moz-cite-prefix class"
  );

  close_compose_window(cw);
}

/**
 * Subtest for test_adding_filelinks_to_reply_below for the plaintext composer.
 */
function subtest_adding_filelinks_to_plaintext_reply_below(aText, aWithSig) {
  let cw = prepare_some_attachments_and_reply(aText, kFiles);
  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  let br, span;

  assert_next_nodes("br", root, 1);

  if (aText.length) {
    br = assert_previous_nodes("br", root, 2);
    // If text was entered, make sure it matches what we expect...
    let textNode = assert_previous_text(br.previousSibling, aText);
    // And then grab the span, which should be before the final text node.
    span = textNode.previousSibling;
  } else {
    br = assert_previous_nodes("br", root, 1);
    // If no text was entered, just grab the last br's previous sibling - that
    // will be the span.
    span = br.previousSibling;
    // Sometimes we need to skip one more linebreak.
    if (span.localName != "span") {
      span = span.previousSibling;
    }
  }

  Assert.equal(
    span.localName,
    "span",
    "The linebreak should be preceded by a span."
  );

  let prefix = span.previousSibling;
  Assert.equal(
    prefix.localName,
    "div",
    "The blockquote should be preceded by the prefix div"
  );
  Assert.ok(
    prefix.classList.contains("moz-cite-prefix"),
    "The prefix should have the moz-cite-prefix class"
  );

  close_compose_window(cw);
}

/**
 * Tests Filelink insertion on an inline-forward compose window with nothing
 * typed into it.
 */
add_task(function test_adding_filelinks_to_empty_forward() {
  Services.prefs.setIntPref(kReplyOnTopKey, kReplyOnTop);
  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_forward,
    []
  );
  Services.prefs.setBoolPref(kHtmlPrefKey, false);
  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_forward,
    []
  );
  Services.prefs.setBoolPref(kHtmlPrefKey, true);
});

/**
 * Tests Filelink insertion on an inline-forward compose window with some
 * text typed into it.
 */
add_task(function test_adding_filelinks_to_forward() {
  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_forward,
    kLines
  );
  Services.prefs.setBoolPref(kHtmlPrefKey, false);
  try_with_and_without_signature_in_reply_or_fwd(
    subtest_adding_filelinks_to_forward,
    kLines
  );
  Services.prefs.setBoolPref(kHtmlPrefKey, true);
});

/**
 * Subtest for both test_adding_filelinks_to_empty_forward and
 * test_adding_filelinks_to_forward - ensures that the inserted Filelinks
 * are positioned correctly.
 */
function subtest_adding_filelinks_to_forward(aText, aWithSig) {
  let cw = prepare_some_attachments_and_forward(aText, kFiles);
  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  let br = assert_next_nodes("br", root, 1);
  let forwardDiv = br.nextSibling;
  Assert.equal(forwardDiv.localName, "div");
  Assert.ok(forwardDiv.classList.contains("moz-forward-container"));

  if (aText.length) {
    // If there was text typed in, it should be separated from the root by two
    // br's
    let br = assert_previous_nodes("br", root, 2);
    assert_previous_text(br.previousSibling, aText);
  } else {
    // Otherwise, there's only 1 br, and that br should be the first element
    // of the message body.
    let br = assert_previous_nodes("br", root, 1);
    let mailBody = get_compose_body(cw);
    Assert.equal(br, mailBody.firstChild);
  }

  close_compose_window(cw);
}

/**
 * Test that if we convert a Filelink from one provider to another, that the
 * old Filelink is removed, and a new Filelink is added for the new provider.
 * We test this on both HTML and plaintext mail.
 */
add_task(function test_converting_filelink_updates_urls() {
  try_with_plaintext_and_html_mail(subtest_converting_filelink_updates_urls);
});

/**
 * Subtest for test_converting_filelink_updates_urls that creates two
 * storage provider accounts, uploads files to one, converts them to the
 * other, and ensures that the attachment links in the message body get
 * get updated.
 */
function subtest_converting_filelink_updates_urls() {
  gMockFilePicker.returnFiles = collectFiles(kFiles);
  let providerA = new MockCloudfileAccount();
  let providerB = new MockCloudfileAccount();
  providerA.init("providerA");
  providerB.init("providerB");

  let cw = open_compose_new_mail();
  add_cloud_attachments(cw, providerA);

  let [, , urls] = wait_for_attachment_urls(cw, kFiles.length);

  // Convert each Filelink to providerB, ensuring that the URLs are replaced.
  for (let i = 0; i < kFiles.length; ++i) {
    let url = urls[i];
    select_attachments(cw, i);
    cw.window.convertSelectedToCloudAttachment(providerB);
    gMockCloudfileManager.resolveUploads();
    [, , urls] = wait_for_attachment_urls(cw, kFiles.length);

    let newUrl = urls[i];

    Assert.notEqual(url, newUrl, "The original URL should have been replaced");
  }

  close_compose_window(cw);
}

/**
 * Test that if we convert a Filelink to a normal attachment that the
 * Filelink is removed from the message body.
 */
add_task(function test_converting_filelink_to_normal_removes_url() {
  try_with_plaintext_and_html_mail(
    subtest_converting_filelink_to_normal_removes_url
  );
});

/**
 * Subtest for test_converting_filelink_to_normal_removes_url that adds
 * some Filelinks to an email, and then converts those Filelinks back into
 * normal attachments, checking to ensure that the links are removed from
 * the body of the email.
 */
function subtest_converting_filelink_to_normal_removes_url() {
  gMockFilePicker.returnFiles = collectFiles(kFiles);
  let provider = new MockCloudfileAccount();
  provider.init("someKey");

  let cw = open_compose_new_mail();
  add_cloud_attachments(cw, provider);

  let [root, list] = wait_for_attachment_urls(cw, kFiles.length);

  for (let i = 0; i < kFiles.length; ++i) {
    select_attachments(cw, i);
    cw.window.convertSelectedToRegularAttachment();

    let urls = list.querySelectorAll(".cloudAttachmentItem");
    Assert.equal(urls.length, kFiles.length - (i + 1));
  }

  // At this point, the root should also have been removed.
  let mailBody = get_compose_body(cw);
  root = mailBody.querySelector("#cloudAttachmentListRoot");
  if (root) {
    throw new Error("Should not have found the cloudAttachmentListRoot");
  }

  close_compose_window(cw);
}

/**
 * Tests that if the user manually removes the Filelinks from the message body
 * that it doesn't break future Filelink insertions. Tests both HTML and
 * plaintext composers.
 */
add_task(function test_filelinks_work_after_manual_removal() {
  try_with_plaintext_and_html_mail(subtest_filelinks_work_after_manual_removal);
});

/**
 * Subtest that first adds some Filelinks to the message body, removes them,
 * and then adds another Filelink ensuring that the new URL is successfully
 * inserted.
 */
function subtest_filelinks_work_after_manual_removal() {
  // Insert some Filelinks...
  gMockFilePicker.returnFiles = collectFiles(kFiles);
  let provider = new MockCloudfileAccount();
  provider.init("someKey");
  let cw = open_compose_new_mail();
  add_cloud_attachments(cw, provider);

  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  // Now remove the root node from the document body
  root.remove();

  gMockFilePicker.returnFiles = collectFiles(["./data/testFile3"]);
  add_cloud_attachments(cw, provider);
  [root] = wait_for_attachment_urls(cw, 1);

  close_compose_window(cw);
}

/**
 * Test that if the users selection caret is on a newline when the URL
 * insertion occurs, that the caret does not move when the insertion is
 * complete. Tests both HTML and plaintext composers.
 */
add_task(function test_insertion_restores_caret_point() {
  try_with_plaintext_and_html_mail(subtest_insertion_restores_caret_point);
});

/**
 * Subtest that types some things into the composer, finishes on two
 * linebreaks, inserts some Filelink URLs, and then types some more,
 * ensuring that the selection is where we expect it to be.
 */
function subtest_insertion_restores_caret_point() {
  // Insert some Filelinks...
  gMockFilePicker.returnFiles = collectFiles(kFiles);
  let provider = new MockCloudfileAccount();
  provider.init("someKey");

  let cw = open_compose_new_mail();

  // Put the selection at the beginning of the document...
  let editor = cw.window.GetCurrentEditor();
  editor.beginningOfDocument();

  // Do any necessary typing, ending with two linebreaks.
  type_in_composer(cw, ["Line 1", "Line 2", "", ""]);

  // Attach some Filelinks.
  add_cloud_attachments(cw, provider);
  let [root] = wait_for_attachment_urls(cw, kFiles.length);

  // Type some text.
  const kTypedIn = "Test";
  type_in_composer(cw, [kTypedIn]);

  // That text should be inserted just above the root attachment URL node.
  assert_previous_text(root.previousSibling, [kTypedIn]);

  close_compose_window(cw);
}
