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

add_setup(async () => {
  const account = MailServices.accounts.createAccount();
  const identity = MailServices.accounts.createIdentity();
  identity.email = "mochitest@localhost";
  account.addIdentity(identity);
  account.incomingServer = MailServices.accounts.createIncomingServer(
    "user",
    "test",
    "pop3"
  );
  MailServices.accounts.defaultAccount = account;
  registerCleanupFunction(() => {
    MailServices.accounts.removeAccount(account, true);
  });
});

/**
 * Tests that additions and removals are accurately displayed, or not
 * displayed if they happen outside the current address book.
 */
add_task(async function test_additions_and_removals() {
  async function deleteRowWithPrompt(index) {
    const promptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
    EventUtils.synthesizeMouseAtCenter(
      cardsList.getRowAtIndex(index),
      {},
      abWindow
    );
    EventUtils.synthesizeKey("VK_DELETE", {}, abWindow);
    await promptPromise;
    await new Promise(r => abWindow.setTimeout(r));
    await new Promise(r => abWindow.setTimeout(r));
  }

  const bookA = createAddressBook("book A");
  const contactA1 = bookA.addCard(createContact("contact", "A1"));
  const bookB = createAddressBook("book B");
  const contactB1 = bookB.addCard(createContact("contact", "B1"));

  const abWindow = await openAddressBookWindow();
  const cardsList = abWindow.cardsPane.cardsList;

  await openAllAddressBooks();
  info("Performing check #1");
  await checkCardsListed(contactA1, contactB1);

  // While in bookA, add a contact and list. Check that they show up.
  await openDirectory(bookA);
  await checkCardsListed(contactA1);
  const contactA2 = bookA.addCard(createContact("contact", "A2")); // Add A2.
  await checkCardsListed(contactA1, contactA2);
  const listC = bookA.addMailList(createMailingList("list C")); // Add C.
  await checkDirectoryDisplayed(bookA);
  await checkCardsListed(contactA1, contactA2, listC);
  listC.addCard(contactA1);
  await checkCardsListed(contactA1, contactA2, listC);

  await openAllAddressBooks();
  info("Performing check #2");
  await checkCardsListed(contactA1, contactA2, contactB1, listC);

  // While in listC, add a member and remove a member. Check that they show up
  // or disappear as appropriate.
  await openDirectory(listC);
  await checkCardsListed(contactA1);
  listC.addCard(contactA2);
  await checkCardsListed(contactA1, contactA2);
  await deleteRowWithPrompt(0);
  await checkCardsListed(contactA2);
  Assert.equal(cardsList.currentIndex, 0);

  await openAllAddressBooks();
  info("Performing check #3");
  await checkCardsListed(contactA1, contactA2, contactB1, listC);

  // While in bookA, delete a contact. Check it disappears.
  await openDirectory(bookA);
  await checkCardsListed(contactA1, contactA2, listC);
  await deleteRowWithPrompt(0); // Delete A1.
  await checkCardsListed(contactA2, listC);
  Assert.equal(cardsList.currentIndex, 0);
  // Now do some things in an unrelated book. Check nothing changes here.
  const contactB2 = bookB.addCard(createContact("contact", "B2")); // Add B2.
  await checkCardsListed(contactA2, listC);
  const listD = bookB.addMailList(createMailingList("list D")); // Add D.
  await checkDirectoryDisplayed(bookA);
  await checkCardsListed(contactA2, listC);
  listD.addCard(contactB1);
  await checkCardsListed(contactA2, listC);

  await openAllAddressBooks();
  info("Performing check #4");
  await checkCardsListed(contactA2, contactB1, contactB2, listC, listD);

  // While in listC, do some things in an unrelated list. Check nothing
  // changes here.
  await openDirectory(listC);
  await checkCardsListed(contactA2);
  listD.addCard(contactB2);
  await checkCardsListed(contactA2);
  listD.deleteCards([contactB1]);
  await checkCardsListed(contactA2);
  bookB.deleteCards([contactB1]);
  await checkCardsListed(contactA2);

  await openAllAddressBooks();
  info("Performing check #5");
  await checkCardsListed(contactA2, contactB2, listC, listD);

  // While in bookA, do some things in an unrelated book. Check nothing
  // changes here.
  await openDirectory(bookA);
  await checkCardsListed(contactA2, listC);
  bookB.deleteDirectory(listD); // Delete D.
  await checkDirectoryDisplayed(bookA);
  await checkCardsListed(contactA2, listC);
  await deleteRowWithPrompt(1); // Delete C.
  await checkCardsListed(contactA2);

  // While in "All Address Books", make some changes and check that things
  // appear or disappear as appropriate.
  await openAllAddressBooks();
  info("Performing check #6");
  await checkCardsListed(contactA2, contactB2);
  const listE = bookB.addMailList(createMailingList("list E")); // Add E.
  await checkDirectoryDisplayed(null);
  await checkCardsListed(contactA2, contactB2, listE);
  listE.addCard(contactB2);
  await checkCardsListed(contactA2, contactB2, listE);
  listE.deleteCards([contactB2]);
  await checkCardsListed(contactA2, contactB2, listE);
  bookB.deleteDirectory(listE); // Delete E.
  await checkDirectoryDisplayed(null);
  await checkCardsListed(contactA2, contactB2);
  await deleteRowWithPrompt(1);
  await checkCardsListed(contactA2);
  Assert.equal(cardsList.currentIndex, 0);
  bookA.deleteCards([contactA2]);
  await checkCardsListed();
  Assert.equal(cardsList.currentIndex, -1);

  // While in "All Address Books", delete a directory that has contacts and
  // mailing lists. They should disappear.
  const contactA3 = bookA.addCard(createContact("contact", "A3")); // Add A3.
  await checkCardsListed(contactA3);
  const listF = bookA.addMailList(createMailingList("list F")); // Add F.
  await checkCardsListed(contactA3, listF);
  await promiseDirectoryRemoved(bookA.URI);
  await checkCardsListed();

  abWindow.close();

  await promiseDirectoryRemoved(bookB.URI);
});

/**
 * Tests that added contacts are inserted in the right place in the list.
 */
add_task(async function test_insertion_order() {
  await openAddressBookWindow();

  const bookA = createAddressBook("book A");
  await openDirectory(bookA);
  await checkCardsListed();
  const contactA2 = bookA.addCard(createContact("contact", "A2"));
  await checkCardsListed(contactA2);
  const contactA1 = bookA.addCard(createContact("contact", "A1")); // Add first.
  await checkCardsListed(contactA1, contactA2);
  const contactA5 = bookA.addCard(createContact("contact", "A5")); // Add last.
  await checkCardsListed(contactA1, contactA2, contactA5);
  const contactA3 = bookA.addCard(createContact("contact", "A3")); // Add in the middle.
  await checkCardsListed(contactA1, contactA2, contactA3, contactA5);

  // Flip sort direction.
  await showSortMenu("sort", "GeneratedName descending");

  await checkCardsListed(contactA5, contactA3, contactA2, contactA1);
  const contactA4 = bookA.addCard(createContact("contact", "A4")); // Add in the middle.
  await checkCardsListed(contactA5, contactA4, contactA3, contactA2, contactA1);
  const contactA7 = bookA.addCard(createContact("contact", "A7")); // Add first.
  await checkCardsListed(
    contactA7,
    contactA5,
    contactA4,
    contactA3,
    contactA2,
    contactA1
  );
  const contactA0 = bookA.addCard(createContact("contact", "A0")); // Add last.
  await checkCardsListed(
    contactA7,
    contactA5,
    contactA4,
    contactA3,
    contactA2,
    contactA1,
    contactA0
  );

  contactA3.displayName = "contact A6";
  contactA3.lastName = "contact A3";
  contactA3.primaryEmail = "contact.A6@invalid";
  bookA.modifyCard(contactA3); // Rename, should change position.
  await checkCardsListed(
    contactA7,
    contactA3, // Actually A6.
    contactA5,
    contactA4,
    contactA2,
    contactA1,
    contactA0
  );

  // Restore original sort direction.
  await showSortMenu("sort", "GeneratedName ascending");

  await checkCardsListed(
    contactA0,
    contactA1,
    contactA2,
    contactA4,
    contactA5,
    contactA3, // Actually A6.
    contactA7
  );
  await closeAddressBookWindow();

  await promiseDirectoryRemoved(bookA.URI);
});

/**
 * Tests the name column is updated when the format changes.
 */
add_task(async function test_name_column() {
  const {
    GENERATE_DISPLAY_NAME,
    GENERATE_LAST_FIRST_ORDER,
    GENERATE_FIRST_LAST_ORDER,
  } = Ci.nsIAbCard;

  const book = createAddressBook("book");
  book.addCard(createContact("alpha", "tango", "kilo"));
  book.addCard(createContact("bravo", "zulu", "quebec"));
  book.addCard(createContact("charlie", "mike", "whiskey"));
  book.addCard(createContact("delta", "foxtrot", "sierra"));
  book.addCard(createContact("echo", "november", "uniform"));

  const abWindow = await openAddressBookWindow();
  const cardsList = abWindow.cardsPane.cardsList;

  // Check the format is display name, ascending.
  Assert.equal(
    Services.prefs.getIntPref("mail.addr_book.lastnamefirst"),
    GENERATE_DISPLAY_NAME
  );

  await checkNamesListed("kilo", "quebec", "sierra", "uniform", "whiskey");

  // Select the "delta foxtrot" contact. This should remain selected throughout.
  cardsList.selectedIndex = 2;
  Assert.equal(cardsList.selectedIndex, 2);

  // Change the format to last, first.
  await showSortMenu("format", GENERATE_LAST_FIRST_ORDER);
  await checkNamesListed(
    "foxtrot, delta",
    "mike, charlie",
    "november, echo",
    "tango, alpha",
    "zulu, bravo"
  );
  Assert.equal(cardsList.selectedIndex, 0);
  Assert.deepEqual(cardsList.selectedIndices, [0]);

  // Change the format to first last.
  await showSortMenu("format", GENERATE_FIRST_LAST_ORDER);
  await checkNamesListed(
    "alpha tango",
    "bravo zulu",
    "charlie mike",
    "delta foxtrot",
    "echo november"
  );
  Assert.equal(cardsList.selectedIndex, 3);

  // Flip the order to descending.
  await showSortMenu("sort", "GeneratedName descending");

  await checkNamesListed(
    "echo november",
    "delta foxtrot",
    "charlie mike",
    "bravo zulu",
    "alpha tango"
  );
  Assert.equal(cardsList.selectedIndex, 1);

  // Change the format to last, first.
  await showSortMenu("format", GENERATE_LAST_FIRST_ORDER);
  await checkNamesListed(
    "zulu, bravo",
    "tango, alpha",
    "november, echo",
    "mike, charlie",
    "foxtrot, delta"
  );
  Assert.equal(cardsList.selectedIndex, 4);

  // Change the format to display name.
  await showSortMenu("format", GENERATE_DISPLAY_NAME);
  await checkNamesListed("whiskey", "uniform", "sierra", "quebec", "kilo");
  Assert.equal(cardsList.selectedIndex, 2);

  // Sort by email address, ascending.
  await showSortMenu("sort", "EmailAddresses ascending");

  await checkNamesListed("kilo", "quebec", "whiskey", "sierra", "uniform");
  Assert.equal(cardsList.selectedIndex, 3);

  // Change the format to last, first.
  await showSortMenu("format", GENERATE_LAST_FIRST_ORDER);
  await checkNamesListed(
    "tango, alpha",
    "zulu, bravo",
    "mike, charlie",
    "foxtrot, delta",
    "november, echo"
  );
  Assert.equal(cardsList.selectedIndex, 3);

  // Change the format to first last.
  await showSortMenu("format", GENERATE_FIRST_LAST_ORDER);
  await checkNamesListed(
    "alpha tango",
    "bravo zulu",
    "charlie mike",
    "delta foxtrot",
    "echo november"
  );
  Assert.equal(cardsList.selectedIndex, 3);

  // Change the format to display name.
  await showSortMenu("format", GENERATE_DISPLAY_NAME);
  await checkNamesListed("kilo", "quebec", "whiskey", "sierra", "uniform");
  Assert.equal(cardsList.selectedIndex, 3);

  // Restore original sort column and direction.
  await showSortMenu("sort", "GeneratedName ascending");

  await checkNamesListed("kilo", "quebec", "sierra", "uniform", "whiskey");
  Assert.equal(cardsList.selectedIndex, 2);

  await closeAddressBookWindow();

  await promiseDirectoryRemoved(book.URI);
});

/**
 * Tests that sort order and name format survive closing and reopening.
 */
add_task(async function test_persistence() {
  const book = createAddressBook("book");
  book.addCard(createContact("alpha", "tango", "kilo"));
  book.addCard(createContact("bravo", "zulu", "quebec"));
  book.addCard(createContact("charlie", "mike", "whiskey"));
  book.addCard(createContact("delta", "foxtrot", "sierra"));
  book.addCard(createContact("echo", "november", "uniform"));

  Services.xulStore.removeDocument("about:addressbook");
  Services.prefs.clearUserPref("mail.addr_book.lastnamefirst");

  await openAddressBookWindow();
  await checkNamesListed("kilo", "quebec", "sierra", "uniform", "whiskey");

  info("sorting by GeneratedName, descending");
  await showSortMenu("sort", "GeneratedName descending");
  await checkNamesListed("whiskey", "uniform", "sierra", "quebec", "kilo");

  await closeAddressBookWindow();
  info("address book closed, reopening");
  await openAddressBookWindow();
  await checkNamesListed("whiskey", "uniform", "sierra", "quebec", "kilo");

  info("sorting by EmailAddresses, ascending");
  await showSortMenu("sort", "EmailAddresses ascending");
  await checkNamesListed("kilo", "quebec", "whiskey", "sierra", "uniform");

  await closeAddressBookWindow();
  info("address book closed, reopening");
  await openAddressBookWindow();
  await checkNamesListed("kilo", "quebec", "whiskey", "sierra", "uniform");

  info("setting name format to first last");
  await showSortMenu("format", Ci.nsIAbCard.GENERATE_FIRST_LAST_ORDER);
  await checkNamesListed(
    "alpha tango",
    "bravo zulu",
    "charlie mike",
    "delta foxtrot",
    "echo november"
  );

  await closeAddressBookWindow();
  info("address book closed, reopening");
  await openAddressBookWindow();
  await checkNamesListed(
    "alpha tango",
    "bravo zulu",
    "charlie mike",
    "delta foxtrot",
    "echo november"
  );

  await closeAddressBookWindow();

  Services.xulStore.removeDocument("about:addressbook");
  Services.prefs.clearUserPref("mail.addr_book.lastnamefirst");

  await promiseDirectoryRemoved(book.URI);
});

/**
 * Tests the context menu compose items.
 */
add_task(async function test_context_menu_compose() {
  const account = MailServices.accounts.createLocalMailAccount();
  account.addIdentity(MailServices.accounts.createIdentity());

  registerCleanupFunction(async () => {
    MailServices.accounts.removeAccount(account, true);
  });

  const book = createAddressBook("Book");
  const contactA = book.addCard(createContact("Contact", "A"));
  let contactB = createContact("Contact", "B");
  contactB.setProperty("SecondEmail", "b.contact@invalid");
  contactB = book.addCard(contactB);
  let contactC = createContact("Contact", "C");
  contactC.primaryEmail = null;
  contactC.setProperty("SecondEmail", "c.contact@invalid");
  contactC = book.addCard(contactC);
  let contactD = createContact("Contact", "D");
  contactD.primaryEmail = null;
  contactD = book.addCard(contactD);
  const list = book.addMailList(createMailingList("List"));
  list.addCard(contactA);
  list.addCard(contactB);

  const abWindow = await openAddressBookWindow();
  const abDocument = abWindow.document;
  const cardsList = abWindow.cardsPane.cardsList;

  const menu = abDocument.getElementById("cardContext");
  const writeMenuItem = abDocument.getElementById("cardContextWrite");
  const writeMenu = abDocument.getElementById("cardContextWriteMenu");
  const writeMenuSeparator = abDocument.getElementById(
    "cardContextWriteSeparator"
  );

  await openDirectory(book);

  // Contact A, first and only email address.

  let composeWindowPromise = BrowserTestUtils.domWindowOpened();

  await showCardsContext(0);
  Assert.ok(!writeMenuItem.hidden, "write menu item shown");
  Assert.ok(writeMenu.hidden, "write menu hidden");
  Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
  menu.activateItem(writeMenuItem);

  await checkComposeWindow(await composeWindowPromise, [
    "Contact A <contact.a@invalid>",
  ]);

  // Contact B, first email address.

  composeWindowPromise = BrowserTestUtils.domWindowOpened();

  await showCardsContext(1);
  Assert.ok(writeMenuItem.hidden, "write menu item hidden");
  Assert.ok(!writeMenu.hidden, "write menu shown");
  Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
  let shownPromise = BrowserTestUtils.waitForEvent(writeMenu, "popupshown");
  writeMenu.openMenu(true);
  await shownPromise;
  let subMenuItems = writeMenu.querySelectorAll("menuitem");
  Assert.equal(subMenuItems.length, 2);
  Assert.equal(subMenuItems[0].label, "Contact B <contact.b@invalid>");
  Assert.equal(subMenuItems[1].label, "Contact B <b.contact@invalid>");

  writeMenu.menupopup.activateItem(subMenuItems[0]);

  await checkComposeWindow(await composeWindowPromise, [
    "Contact B <contact.b@invalid>",
  ]);

  // Contact B, second email address.

  composeWindowPromise = BrowserTestUtils.domWindowOpened();

  await showCardsContext(1);
  Assert.ok(writeMenuItem.hidden, "write menu item hidden");
  Assert.ok(!writeMenu.hidden, "write menu shown");
  Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
  shownPromise = BrowserTestUtils.waitForEvent(writeMenu, "popupshown");
  writeMenu.openMenu(true);
  await shownPromise;
  subMenuItems = writeMenu.querySelectorAll("menuitem");
  Assert.equal(subMenuItems.length, 2);
  Assert.equal(subMenuItems[0].label, "Contact B <contact.b@invalid>");
  Assert.equal(subMenuItems[1].label, "Contact B <b.contact@invalid>");

  writeMenu.menupopup.activateItem(subMenuItems[1]);

  await checkComposeWindow(await composeWindowPromise, [
    "Contact B <b.contact@invalid>",
  ]);

  // Contact C, second and only email address.

  composeWindowPromise = BrowserTestUtils.domWindowOpened();

  await showCardsContext(2);
  Assert.ok(!writeMenuItem.hidden, "write menu item shown");
  Assert.ok(writeMenu.hidden, "write menu hidden");
  Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
  menu.activateItem(writeMenuItem);

  await checkComposeWindow(await composeWindowPromise, [
    "Contact C <c.contact@invalid>",
  ]);

  // Contact D, no email address.

  await showCardsContext(3);
  Assert.ok(writeMenuItem.hidden, "write menu item hidden");
  Assert.ok(writeMenu.hidden, "write menu hidden");
  Assert.ok(writeMenuSeparator.hidden, "write menu separator hidden");
  menu.hidePopup();

  // List.

  composeWindowPromise = BrowserTestUtils.domWindowOpened();

  await showCardsContext(4);
  Assert.ok(!writeMenuItem.hidden, "write menu item shown");
  Assert.ok(writeMenu.hidden, "write menu hidden");
  Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
  menu.activateItem(writeMenuItem);

  await checkComposeWindow(await composeWindowPromise, ["List <List>"]);

  // Contact A and Contact D.

  composeWindowPromise = BrowserTestUtils.domWindowOpened();

  cardsList.selectedIndices = [0, 3];
  await showCardsContext(3);
  Assert.ok(!writeMenuItem.hidden, "write menu item shown");
  Assert.ok(writeMenu.hidden, "write menu hidden");
  Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
  menu.activateItem(writeMenuItem);

  await checkComposeWindow(await composeWindowPromise, [
    "Contact A <contact.a@invalid>",
  ]);

  // Contact B and Contact C.

  composeWindowPromise = BrowserTestUtils.domWindowOpened();

  cardsList.selectedIndices = [1, 2];
  await showCardsContext(2);
  Assert.ok(!writeMenuItem.hidden, "write menu item shown");
  Assert.ok(writeMenu.hidden, "write menu hidden");
  Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
  menu.activateItem(writeMenuItem);

  await checkComposeWindow(await composeWindowPromise, [
    "Contact B <contact.b@invalid>",
    "Contact C <c.contact@invalid>",
  ]);

  // Contact B and List.

  composeWindowPromise = BrowserTestUtils.domWindowOpened();

  cardsList.selectedIndices = [1, 4];
  await showCardsContext(4);
  Assert.ok(!writeMenuItem.hidden, "write menu item shown");
  Assert.ok(writeMenu.hidden, "write menu hidden");
  Assert.ok(!writeMenuSeparator.hidden, "write menu separator shown");
  menu.activateItem(writeMenuItem);

  await checkComposeWindow(await composeWindowPromise, [
    "Contact B <contact.b@invalid>",
    "List <List>",
  ]);

  await closeAddressBookWindow();

  await promiseDirectoryRemoved(book.URI);
});

/**
 * Tests the context menu edit items.
 */
add_task(async function test_context_menu_edit() {
  const normalBook = createAddressBook("Normal Book");
  const normalList = normalBook.addMailList(createMailingList("Normal List"));
  const normalContact = normalBook.addCard(createContact("Normal", "Contact"));
  normalList.addCard(normalContact);

  const readOnlyBook = createAddressBook("Read-Only Book");
  const readOnlyList = readOnlyBook.addMailList(
    createMailingList("Read-Only List")
  );
  const readOnlyContact = readOnlyBook.addCard(
    createContact("Read-Only", "Contact")
  );
  readOnlyList.addCard(readOnlyContact);
  readOnlyBook.setBoolValue("readOnly", true);

  const abWindow = await openAddressBookWindow();
  const abDocument = abWindow.document;
  const cardsList = abWindow.cardsPane.cardsList;

  const menu = abDocument.getElementById("cardContext");
  const editMenuItem = abDocument.getElementById("cardContextEdit");
  const exportMenuItem = abDocument.getElementById("cardContextExport");

  async function checkEditItems(index, hidden, isMailList = false) {
    await showCardsContext(index);

    Assert.equal(
      editMenuItem.hidden,
      hidden,
      `editMenuItem should be hidden=${hidden} on index ${index}`
    );
    Assert.equal(
      exportMenuItem.hidden,
      !isMailList,
      `exportMenuItem should be hidden=${!isMailList} on index ${index}`
    );

    Assert.deepEqual(document.l10n.getAttributes(editMenuItem), {
      id: isMailList
        ? "about-addressbook-books-context-edit-list"
        : "about-addressbook-books-context-edit",
      args: null,
    });

    const hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
    menu.hidePopup();
    await hiddenPromise;
  }

  info("Testing Normal Book");
  await openDirectory(normalBook);
  await checkEditItems(0, false); // normal contact
  await checkEditItems(1, false, true); // normal list

  cardsList.selectedIndices = [0, 1];
  await checkEditItems(0, true); // normal contact + normal list
  await checkEditItems(1, true); // normal contact + normal list

  info("Testing Normal List");
  await openDirectory(normalList);
  await checkEditItems(0, false); // normal contact

  info("Testing Read-Only Book");
  await openDirectory(readOnlyBook);
  await checkEditItems(0, true); // read-only contact
  await checkEditItems(1, true, true); // read-only list

  info("Testing Read-Only List");
  await openDirectory(readOnlyList);
  await checkEditItems(0, true); // read-only contact

  info("Testing All Address Books");
  await openAllAddressBooks();
  await checkEditItems(0, false); // normal contact
  await checkEditItems(1, false, true); // normal list
  await checkEditItems(2, true); // read-only contact
  await checkEditItems(3, true, true); // read-only list

  cardsList.selectedIndices = [0, 1];
  await checkEditItems(1, true); // normal contact + normal list

  cardsList.selectedIndices = [0, 2];
  await checkEditItems(2, true); // normal contact + read-only contact

  cardsList.selectedIndices = [1, 3];
  await checkEditItems(3, true); // normal list + read-only list

  cardsList.selectedIndices = [0, 1, 2, 3];
  await checkEditItems(3, true); // everything

  await closeAddressBookWindow();

  await promiseDirectoryRemoved(normalBook.URI);
  await promiseDirectoryRemoved(readOnlyBook.URI);
});

/**
 * Tests the context menu delete items.
 */
add_task(async function test_context_menu_delete() {
  const normalBook = createAddressBook("Normal Book");
  const normalList = normalBook.addMailList(createMailingList("Normal List"));
  const normalContact = normalBook.addCard(createContact("Normal", "Contact"));
  normalList.addCard(normalContact);

  const readOnlyBook = createAddressBook("Read-Only Book");
  const readOnlyList = readOnlyBook.addMailList(
    createMailingList("Read-Only List")
  );
  const readOnlyContact = readOnlyBook.addCard(
    createContact("Read-Only", "Contact")
  );
  readOnlyList.addCard(readOnlyContact);
  readOnlyBook.setBoolValue("readOnly", true);

  const abWindow = await openAddressBookWindow();
  const abDocument = abWindow.document;
  const cardsList = abWindow.cardsPane.cardsList;

  const menu = abDocument.getElementById("cardContext");
  const deleteMenuItem = abDocument.getElementById("cardContextDelete");
  const removeMenuItem = abDocument.getElementById("cardContextRemove");

  async function checkDeleteItems(index, deleteHidden, removeHidden, disabled) {
    await showCardsContext(index);

    Assert.equal(
      deleteMenuItem.hidden,
      deleteHidden,
      `deleteMenuItem.hidden on index ${index}`
    );
    Assert.equal(
      deleteMenuItem.disabled,
      disabled,
      `deleteMenuItem.disabled on index ${index}`
    );
    Assert.equal(
      removeMenuItem.hidden,
      removeHidden,
      `removeMenuItem.hidden on index ${index}`
    );
    Assert.equal(
      removeMenuItem.disabled,
      disabled,
      `removeMenuItem.disabled on index ${index}`
    );

    const hiddenPromise = BrowserTestUtils.waitForEvent(menu, "popuphidden");
    menu.hidePopup();
    await hiddenPromise;
  }

  info("Testing Normal Book");
  await openDirectory(normalBook);
  await checkDeleteItems(0, false, true, false); // normal contact
  await checkDeleteItems(1, false, true, false); // normal list

  cardsList.selectedIndices = [0, 1];
  await checkDeleteItems(0, false, true, false); // normal contact + normal list
  await checkDeleteItems(1, false, true, false); // normal contact + normal list

  info("Testing Normal List");
  await openDirectory(normalList);
  await checkDeleteItems(0, true, false, false); // normal contact

  info("Testing Read-Only Book");
  await openDirectory(readOnlyBook);
  await checkDeleteItems(0, false, true, true); // read-only contact
  await checkDeleteItems(1, false, true, true); // read-only list

  info("Testing Read-Only List");
  await openDirectory(readOnlyList);
  await checkDeleteItems(0, true, false, true); // read-only contact

  info("Testing All Address Books");
  await openAllAddressBooks();
  await checkDeleteItems(0, false, true, false); // normal contact
  await checkDeleteItems(1, false, true, false); // normal list
  await checkDeleteItems(2, false, true, true); // read-only contact
  await checkDeleteItems(3, false, true, true); // read-only list

  cardsList.selectedIndices = [0, 1];
  await checkDeleteItems(1, false, true, false); // normal contact + normal list

  cardsList.selectedIndices = [0, 2];
  await checkDeleteItems(2, false, true, true); // normal contact + read-only contact

  cardsList.selectedIndices = [1, 3];
  await checkDeleteItems(3, false, true, true); // normal list + read-only list

  cardsList.selectedIndices = [0, 1, 2, 3];
  await checkDeleteItems(3, false, true, true); // everything

  await closeAddressBookWindow();

  await promiseDirectoryRemoved(normalBook.URI);
  await promiseDirectoryRemoved(readOnlyBook.URI);
});

add_task(async function test_layout() {
  function checkColumns(visibleColumns, sortColumn, sortDirection) {
    const visibleHeaders = cardsHeader.querySelectorAll(
      `th[is="tree-view-table-header-cell"]:not([hidden])`
    );
    Assert.deepEqual(
      Array.from(visibleHeaders, h => h.id),
      visibleColumns,
      "visible columns are correct"
    );

    for (const header of visibleHeaders) {
      const button = header.querySelector("button");
      Assert.equal(
        button.classList.contains("ascending"),
        header.id == sortColumn && sortDirection == "ascending",
        `${header.id} header is ascending`
      );
      Assert.equal(
        button.classList.contains("descending"),
        header.id == sortColumn && sortDirection == "descending",
        `${header.id} header is descending`
      );
    }
  }

  function checkRowHeight(height) {
    Assert.equal(cardsList.getRowAtIndex(0).clientHeight, height);
  }

  Services.prefs.setIntPref("mail.uidensity", 0);
  personalBook.addCard(
    createContact("contact", "one", undefined, "first@invalid")
  );
  personalBook.addCard(
    createContact("contact", "two", undefined, "second@invalid")
  );
  personalBook.addCard(
    createContact("contact", "three", undefined, "third@invalid")
  );
  personalBook.addCard(
    createContact("contact", "four", undefined, "fourth@invalid")
  );

  let abWindow = await openAddressBookWindow();
  let abDocument = abWindow.document;
  let cardsList = abWindow.cardsPane.cardsList;
  let cardsHeader = abWindow.cardsPane.table.header;
  let sharedSplitter = abDocument.getElementById("sharedSplitter");

  // Sanity check.

  Assert.ok(
    !abDocument.body.classList.contains("layout-table"),
    "not table layout on opening"
  );
  Assert.equal(
    sharedSplitter.resizeDirection,
    "horizontal",
    "splitter direction is horizontal"
  );
  Assert.equal(
    sharedSplitter.resizeElement.id,
    "cardsPane",
    "splitter is affecting the cards pane"
  );
  Assert.equal(
    cardsList.getAttribute("rows"),
    "ab-card-row",
    "list row implementation used"
  );

  // Switch layout to table.

  await toggleLayout();

  Assert.ok(
    abDocument.body.classList.contains("layout-table"),
    "layout changed"
  );
  Assert.equal(
    sharedSplitter.resizeDirection,
    "vertical",
    "splitter direction is vertical"
  );
  Assert.equal(
    sharedSplitter.resizeElement.id,
    "detailsPane",
    "splitter is affecting the details pane"
  );
  Assert.equal(
    cardsList.getAttribute("rows"),
    "ab-table-card-row",
    "table row implementation used"
  );

  checkColumns(
    ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Addresses"],
    "GeneratedName",
    "ascending"
  );
  await checkNamesListed(
    "contact four",
    "contact one",
    "contact three",
    "contact two"
  );
  checkRowHeight(18);

  // Click the email addresses header to sort.

  EventUtils.synthesizeMouseAtCenter(
    cardsHeader.querySelector(`[id="EmailAddressesButton"]`),
    {},
    abWindow
  );
  checkColumns(
    ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Addresses"],
    "EmailAddresses",
    "ascending"
  );
  await checkNamesListed(
    "contact one",
    "contact four",
    "contact two",
    "contact three"
  );

  // Click the email addresses header again to flip the sort.

  EventUtils.synthesizeMouseAtCenter(
    cardsHeader.querySelector(`[id="EmailAddressesButton"]`),
    {},
    abWindow
  );
  checkColumns(
    ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Addresses"],
    "EmailAddresses",
    "descending"
  );
  await checkNamesListed(
    "contact three",
    "contact two",
    "contact four",
    "contact one"
  );

  // Add a column.

  await showPickerMenu("toggle", "Title");
  await TestUtils.waitForCondition(
    () => !cardsHeader.querySelector(`[id="Title"]`).hidden
  );
  checkColumns(
    ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Addresses", "Title"],
    "EmailAddresses",
    "descending"
  );

  // Remove a column.

  await showPickerMenu("toggle", "Addresses");
  await TestUtils.waitForCondition(
    () => cardsHeader.querySelector(`[id="Addresses"]`).hidden
  );
  checkColumns(
    ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Title"],
    "EmailAddresses",
    "descending"
  );

  // Change the density.

  Services.prefs.setIntPref("mail.uidensity", 1);
  checkRowHeight(22);

  Services.prefs.setIntPref("mail.uidensity", 2);
  checkRowHeight(32);

  // Close and reopen the Address Book and check that settings were remembered.

  await closeAddressBookWindow();

  abWindow = await openAddressBookWindow();
  abDocument = abWindow.document;
  cardsList = abWindow.cardsPane.cardsList;
  cardsHeader = abWindow.cardsPane.table.header;
  sharedSplitter = abDocument.getElementById("sharedSplitter");

  Assert.ok(
    abDocument.body.classList.contains("layout-table"),
    "table layout preserved on reopening"
  );
  Assert.equal(
    sharedSplitter.resizeDirection,
    "vertical",
    "splitter direction preserved as vertical"
  );
  Assert.equal(
    sharedSplitter.resizeElement.id,
    "detailsPane",
    "splitter preserved affecting the details pane"
  );
  Assert.equal(
    cardsList.getAttribute("rows"),
    "ab-table-card-row",
    "table row implementation used"
  );

  checkColumns(
    ["GeneratedName", "EmailAddresses", "PhoneNumbers", "Title"],
    "EmailAddresses",
    "descending"
  );
  await checkNamesListed(
    "contact three",
    "contact two",
    "contact four",
    "contact one"
  );
  checkRowHeight(32);

  // Reset layout to list.

  await toggleLayout();

  Assert.ok(
    !abDocument.body.classList.contains("layout-table"),
    "layout changed"
  );
  Assert.equal(
    sharedSplitter.resizeDirection,
    "horizontal",
    "splitter direction is horizontal"
  );
  Assert.equal(
    sharedSplitter.resizeElement.id,
    "cardsPane",
    "splitter is affecting the cards pane"
  );
  Assert.equal(
    cardsList.getAttribute("rows"),
    "ab-card-row",
    "list row implementation used"
  );

  await closeAddressBookWindow();

  Services.xulStore.removeDocument("about:addressbook");
  Services.prefs.clearUserPref("mail.uidensity");
  personalBook.deleteCards(personalBook.childCards);
});

add_task(async function test_placeholders() {
  const writableBook = createAddressBook("Writable Book");
  const readOnlyBook = createAddressBook("Read-Only Book");
  readOnlyBook.setBoolValue("readOnly", true);

  const abWindow = await openAddressBookWindow();
  const placeholderCreateContact = abWindow.document.getElementById(
    "placeholderCreateContact"
  );

  info("checking all address books");
  await openAllAddressBooks();
  await checkPlaceholders(["placeholderEmptyBook", "placeholderCreateContact"]);

  info("checking writable book");
  await openDirectory(writableBook);
  await checkPlaceholders(["placeholderEmptyBook", "placeholderCreateContact"]);

  const writableList = writableBook.addMailList(
    createMailingList("Writable List")
  );
  await checkPlaceholders();

  info("checking writable list");
  await openDirectory(writableList);
  await checkPlaceholders(["placeholderEmptyBook"]);

  info("checking writable book");
  await openDirectory(writableBook);
  writableBook.deleteDirectory(writableList);
  await checkPlaceholders(["placeholderEmptyBook", "placeholderCreateContact"]);

  info("checking read-only book");
  await openDirectory(readOnlyBook);
  await checkPlaceholders(["placeholderEmptyBook"]);

  // This wouldn't happen but we need to check the state in a read-only list.
  readOnlyBook.setBoolValue("readOnly", false);
  const readOnlyList = readOnlyBook.addMailList(
    createMailingList("Read-Only List")
  );
  readOnlyBook.setBoolValue("readOnly", true);
  await checkPlaceholders();

  info("checking read-only list");
  await openDirectory(readOnlyList);
  await checkPlaceholders(["placeholderEmptyBook"]);

  info("checking read-only book");
  await openDirectory(readOnlyBook);
  readOnlyBook.setBoolValue("readOnly", false);
  readOnlyBook.deleteDirectory(readOnlyList);
  readOnlyBook.setBoolValue("readOnly", true);
  await checkPlaceholders(["placeholderEmptyBook"]);

  info("checking button opens a new contact to edit");
  await openAllAddressBooks();
  await checkPlaceholders(["placeholderEmptyBook", "placeholderCreateContact"]);
  EventUtils.synthesizeMouseAtCenter(placeholderCreateContact, {}, abWindow);

  await TestUtils.waitForCondition(
    () => abWindow.detailsPane.isEditing,
    "entering editing mode"
  );

  await closeAddressBookWindow();
  await promiseDirectoryRemoved(writableBook.URI);
  await promiseDirectoryRemoved(readOnlyBook.URI);
});

/**
 * Checks that mailling lists address books are shown in the table layout.
 */
add_task(async function test_list_table_layout() {
  const book = createAddressBook("Book");
  book.addCard(createContact("contact", "one"));
  const list = createMailingList("list one");
  book.addMailList(list);

  const abWindow = await openAddressBookWindow();
  const cardsList = abWindow.cardsPane.cardsList;
  const cardsHeader = abWindow.cardsPane.table.header;

  // Switch layout to table.

  await toggleLayout();

  await showPickerMenu("toggle", "addrbook");
  await TestUtils.waitForCondition(
    () => !cardsHeader.querySelector(`[id="addrbook"]`).hidden
  );
  await new Promise(resolve => abWindow.requestAnimationFrame(resolve));

  // Check for the contact that the column is shown.
  Assert.ok(
    !cardsList.getRowAtIndex(0).querySelector(".addrbook-column").hidden,
    "Address book column is shown."
  );

  Assert.ok(
    cardsList
      .getRowAtIndex(0)
      .querySelector(".addrbook-column")
      .textContent.includes("Book"),
    "Address book column has the correct name for a contact."
  );

  Assert.ok(
    cardsList
      .getRowAtIndex(0)
      .querySelector(".addrbook-column")
      .textContent.includes("Book"),
    "Address book column has the correct name for a list."
  );

  Services.xulStore.removeDocument("about:addressbook");
  await closeAddressBookWindow();
  await promiseDirectoryRemoved(book.URI);
});

/**
 * Tests the option of showing the address book for All Address Book for the
 * list view (vertical layout).
 */
add_task(async function test_list_all_address_book() {
  const firstBook = createAddressBook("First Book");
  const secondBook = createAddressBook("Second Book");
  firstBook.addCard(createContact("contact", "one"));
  secondBook.addCard(createContact("contact", "two"));
  const list = createMailingList("list two");
  secondBook.addMailList(list);

  const abWindow = await openAddressBookWindow();
  const cardsList = abWindow.cardsPane.cardsList;
  const cardsHeader = abWindow.cardsPane.table.header;

  info("Check that no address book suffix is present.");
  Assert.ok(
    !cardsList.getRowAtIndex(0).querySelector(".address-book-name"),
    "No address book suffix is present."
  );
  Assert.ok(
    !cardsList.getRowAtIndex(1).querySelector(".address-book-name"),
    "No address book suffix is present."
  );
  Assert.ok(
    !cardsList.getRowAtIndex(2).querySelector(".address-book-name"),
    "No address book suffix is present."
  );

  info("Toggle the option to show address books.");
  await showSortMenu("toggle", "addrbook");
  await TestUtils.waitForCondition(
    () => !cardsHeader.querySelector(`[id="addrbook"]`).hidden
  );
  await new Promise(resolve => abWindow.requestAnimationFrame(resolve));

  Assert.ok(
    cardsList
      .getRowAtIndex(0)
      .querySelector(".address-book-name")
      .textContent.includes("First Book"),
    "Address book suffix is present."
  );
  Assert.ok(
    cardsList
      .getRowAtIndex(1)
      .querySelector(".address-book-name")
      .textContent.includes("Second Book"),
    "Address book suffix is present."
  );
  Assert.ok(
    cardsList
      .getRowAtIndex(2)
      .querySelector(".address-book-name")
      .textContent.includes("Second Book"),
    "Address book suffix is present for a list."
  );

  info(`Select another address book and check that no address book suffix is
  present for another book besides All Address Book`);
  await openDirectory(secondBook);
  Assert.ok(
    !cardsList.getRowAtIndex(0).querySelector(".address-book-name"),
    "Address book suffix is only present in All Address Book."
  );

  Services.xulStore.removeDocument("about:addressbook");
  await closeAddressBookWindow();
  await promiseDirectoryRemoved(firstBook.URI);
  await promiseDirectoryRemoved(secondBook.URI);
});
