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

const { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);

async function subtest() {
  // Put some cards on the server.
  CardDAVServer.putCardInternal(
    "keep-me.vcf",
    "BEGIN:VCARD\r\nUID:keep-me\r\nFN:I'm going to stay.\r\nEND:VCARD\r\n"
  );
  CardDAVServer.putCardInternal(
    "change-me.vcf",
    // This one includes a character encoded with UTF-8.
    "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm going to be changed. \xCF\x9E\r\nEND:VCARD\r\n"
  );
  CardDAVServer.putCardInternal(
    "delete-me.vcf",
    "BEGIN:VCARD\r\nUID:delete-me\r\nFN:I'm going to be deleted.\r\nEND:VCARD\r\n"
  );

  const directory = await initDirectory();

  // We'll only use this for the initial sync, so I think it's okay to use
  // bulkAddCards and not get a notification for every contact.
  info("Initial sync with server.");
  await directory.fetchAllFromServer();

  info("Cards:");
  const cardMap = new Map();
  const oldETags = new Map();
  for (const card of directory.childCards) {
    info(card.displayName);
    info(card.getProperty("_href", ""));
    info(card.getProperty("_etag", ""));

    cardMap.set(card.UID, card);
    oldETags.set(card.UID, card.getProperty("_etag", ""));
  }

  Assert.equal(cardMap.size, 3);
  Assert.deepEqual([...cardMap.keys()].sort(), [
    "change-me",
    "delete-me",
    "keep-me",
  ]);
  Assert.equal(
    cardMap.get("change-me").displayName,
    "I'm going to be changed. Ϟ"
  );

  // Make some changes on the server.

  CardDAVServer.putCardInternal(
    "change-me.vcf",
    "BEGIN:VCARD\r\nUID:change-me\r\nFN:I've been changed.\r\nEND:VCARD\r\n"
  );
  CardDAVServer.deleteCardInternal("delete-me.vcf");
  CardDAVServer.putCardInternal(
    "new.vcf",
    "BEGIN:VCARD\r\nUID:new\r\nFN:I'm new!\r\nEND:VCARD\r\n"
  );

  // Sync with the server.

  info("Second sync with server.");

  observer.init();
  await directory.updateAllFromServerV1();
  observer.checkAndClearNotifications({
    "addrbook-contact-created": ["new"],
    "addrbook-contact-updated": ["change-me"],
    "addrbook-contact-deleted": ["delete-me"],
  });

  info("Cards:");
  cardMap.clear();
  for (const card of directory.childCards) {
    info(card.displayName);
    info(card.getProperty("_href", ""));
    info(card.getProperty("_etag", ""));

    cardMap.set(card.UID, card);
  }

  Assert.equal(cardMap.size, 3);
  Assert.deepEqual([...cardMap.keys()].sort(), ["change-me", "keep-me", "new"]);

  Assert.equal(
    cardMap.get("keep-me").getProperty("_etag", ""),
    oldETags.get("keep-me")
  );

  Assert.equal(cardMap.get("change-me").displayName, "I've been changed.");
  Assert.notEqual(
    cardMap.get("change-me").getProperty("_etag", ""),
    oldETags.get("change-me")
  );
  oldETags.set("change-me", cardMap.get("change-me").getProperty("_etag", ""));

  Assert.equal(cardMap.get("new").displayName, "I'm new!");
  oldETags.set("new", cardMap.get("new").getProperty("_etag", ""));

  oldETags.delete("delete-me");

  // Double-check that what we have matches what's on the server.

  await checkCardsOnServer({
    "change-me": {
      etag: cardMap.get("change-me").getProperty("_etag", ""),
      href: cardMap.get("change-me").getProperty("_href", ""),
      vCard: cardMap.get("change-me").getProperty("_vCard", ""),
    },
    "keep-me": {
      etag: cardMap.get("keep-me").getProperty("_etag", ""),
      href: cardMap.get("keep-me").getProperty("_href", ""),
      vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
    },
    new: {
      etag: cardMap.get("new").getProperty("_etag", ""),
      href: cardMap.get("new").getProperty("_href", ""),
      vCard: cardMap.get("new").getProperty("_vCard", ""),
    },
  });

  info("Third sync with server. No changes expected.");

  await directory.updateAllFromServerV1();

  observer.checkAndClearNotifications({
    "addrbook-contact-created": [],
    "addrbook-contact-updated": [],
    "addrbook-contact-deleted": [],
  });

  // Delete a card on the client.

  info("Deleting a card on the client.");

  try {
    directory.deleteCards([cardMap.get("new")]);
    Assert.ok(!directory.readOnly, "read-only directory should throw");
    observer.checkAndClearNotifications({
      "addrbook-contact-created": [],
      "addrbook-contact-updated": [],
      "addrbook-contact-deleted": ["new"],
    });

    await checkCardsOnServer({
      "change-me": {
        etag: cardMap.get("change-me").getProperty("_etag", ""),
        href: cardMap.get("change-me").getProperty("_href", ""),
        vCard: cardMap.get("change-me").getProperty("_vCard", ""),
      },
      "keep-me": {
        etag: cardMap.get("keep-me").getProperty("_etag", ""),
        href: cardMap.get("keep-me").getProperty("_href", ""),
        vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
      },
    });
  } catch (ex) {
    Assert.ok(directory.readOnly, "read-write directory should not throw");
  }

  // Change a card on the client.

  info("Changing a card on the client.");

  try {
    let changeMeCard = cardMap.get("change-me");
    Assert.equal(
      changeMeCard.getProperty("PopularityIndex", ""),
      0,
      "sanity check, initial PopularityIndex"
    );
    changeMeCard.displayName = "I've been changed again!";
    changeMeCard.setProperty("PopularityIndex", 10);

    // First entry into AddrBookDirectory.modifyCard.
    const firstNotification = observer.waitFor("addrbook-contact-updated");
    directory.modifyCard(changeMeCard);
    Assert.ok(!directory.readOnly, "read-only directory should throw");
    await firstNotification;

    // Second entry into AddrBookDirectory.modifyCard. Triggered by syncing.
    Assert.equal(
      await observer.waitFor("addrbook-contact-updated"),
      "change-me"
    );

    changeMeCard = directory.childCards.find(c => c.UID == "change-me");
    Assert.equal(
      changeMeCard.getProperty("PopularityIndex", ""),
      10,
      "PopularityIndex survived sync"
    );
    cardMap.set("change-me", changeMeCard);

    await checkCardsOnServer({
      "change-me": {
        etag: changeMeCard.getProperty("_etag", ""),
        href: changeMeCard.getProperty("_href", ""),
        vCard: changeMeCard.getProperty("_vCard", ""),
      },
      "keep-me": {
        etag: cardMap.get("keep-me").getProperty("_etag", ""),
        href: cardMap.get("keep-me").getProperty("_href", ""),
        vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
      },
    });
  } catch (ex) {
    Assert.ok(directory.readOnly, "read-write directory should not throw");
  }

  // Change a card on the client, but this time only non-vCard properties.

  info("Changing a card on the client, non-vCard properties.");

  try {
    let changeMeCard = cardMap.get("change-me");
    Assert.equal(
      changeMeCard.getProperty("PopularityIndex", ""),
      10,
      "sanity check, initial PopularityIndex"
    );
    changeMeCard.setProperty("PopularityIndex", 20);

    // First entry into AddrBookDirectory.modifyCard.
    const firstNotification = observer.waitFor("addrbook-contact-updated");
    directory.modifyCard(changeMeCard);
    Assert.ok(!directory.readOnly, "read-only directory should throw");
    await firstNotification;

    // Check there's no second entry into AddrBookDirectory.modifyCard.
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(resolve => setTimeout(resolve, 500));
    observer.checkAndClearNotifications({
      "addrbook-contact-created": [],
      "addrbook-contact-updated": [],
      "addrbook-contact-deleted": [],
    });

    changeMeCard = directory.childCards.find(c => c.UID == "change-me");
    Assert.equal(
      changeMeCard.getProperty("PopularityIndex", ""),
      20,
      "PopularityIndex did get modified"
    );
    cardMap.set("change-me", changeMeCard);
  } catch (ex) {
    Assert.ok(directory.readOnly, "read-write directory should not throw");
  }

  // Add a new card on the client.

  info("Adding a new card on the client.");

  try {
    let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
      Ci.nsIAbCard
    );
    newCard.displayName = "I'm another new contact. ϔ";
    newCard.setProperty("PopularityIndex", 10);
    newCard.UID = "another-new";
    newCard = directory.addCard(newCard);
    Assert.ok(!directory.readOnly, "read-only directory should throw");
    observer.checkAndClearNotifications({
      "addrbook-contact-created": ["another-new"],
      "addrbook-contact-updated": [],
      "addrbook-contact-deleted": [],
    });

    Assert.equal(
      await observer.waitFor("addrbook-contact-updated"),
      "another-new"
    );

    newCard = directory.childCards.find(c => c.UID == "another-new");
    Assert.equal(
      newCard.displayName,
      "I'm another new contact. ϔ",
      "non-ascii character survived the trip to the server"
    );
    Assert.equal(
      newCard.getProperty("PopularityIndex", ""),
      10,
      "PopularityIndex survived sync"
    );

    await checkCardsOnServer({
      "another-new": {
        etag: newCard.getProperty("_etag", ""),
        href: newCard.getProperty("_href", ""),
        vCard: newCard.getProperty("_vCard", ""),
      },
      "change-me": {
        etag: cardMap.get("change-me").getProperty("_etag", ""),
        href: cardMap.get("change-me").getProperty("_href", ""),
        vCard: cardMap.get("change-me").getProperty("_vCard", ""),
      },
      "keep-me": {
        etag: cardMap.get("keep-me").getProperty("_etag", ""),
        href: cardMap.get("keep-me").getProperty("_href", ""),
        vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
      },
    });
  } catch (ex) {
    Assert.ok(directory.readOnly, "read-write directory should not throw");
  }

  info("Fourth sync with server. No changes expected.");

  await directory.updateAllFromServerV1();

  observer.checkAndClearNotifications({
    "addrbook-contact-created": [],
    "addrbook-contact-updated": [],
    "addrbook-contact-deleted": [],
  });

  await clearDirectory(directory);
  CardDAVServer.reset();
}

add_task(async function testNormal() {
  await subtest();
});

add_task(async function testYahoo() {
  CardDAVServer.mimicYahoo = true;
  await subtest();
  CardDAVServer.mimicYahoo = false;
});

add_task(async function testReadOnly() {
  Services.prefs.setBoolPref("ldap_2.servers.carddav.readOnly", true);
  await subtest();
  Services.prefs.clearUserPref("ldap_2.servers.carddav.readOnly");
});
