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

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"
  );

  let directory = 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();

  let lastSyncToken = directory._syncToken;
  info(`Token is: ${lastSyncToken}`);

  info("Cards:");
  let cardMap = new Map();
  let oldETags = new Map();
  for (let card of directory.childCards) {
    info(
      ` ${card.displayName} [${card.getProperty(
        "_href",
        ""
      )}, ${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.updateAllFromServerV2();
  Assert.notEqual(directory._syncToken, lastSyncToken);
  lastSyncToken = directory._syncToken;
  observer.checkAndClearNotifications({
    "addrbook-contact-created": ["new"],
    "addrbook-contact-updated": ["change-me"],
    "addrbook-contact-deleted": ["delete-me"],
  });

  info("Cards:");
  cardMap.clear();
  for (let card of directory.childCards) {
    info(
      ` ${card.displayName} [${card.getProperty(
        "_href",
        ""
      )}, ${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.updateAllFromServerV2();
  // This time the token should NOT change, there's been no contact with the
  // server since last time.
  Assert.equal(directory._syncToken, lastSyncToken);
  lastSyncToken = directory._syncToken;

  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");
    changeMeCard.displayName = "I've been changed again!";
    directory.modifyCard(changeMeCard);
    Assert.ok(!directory.readOnly, "read-only directory should throw.");

    Assert.equal(
      await observer.waitFor("addrbook-contact-updated"),
      "change-me"
    );
    observer.checkAndClearNotifications({
      "addrbook-contact-created": [],
      "addrbook-contact-updated": ["change-me"],
      "addrbook-contact-deleted": [],
    });

    changeMeCard = directory.childCards.find(c => c.UID == "change-me");
    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");
  }

  // 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.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"
    );

    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.updateAllFromServerV2();
  if (directory.readOnly) {
    Assert.equal(directory._syncToken, lastSyncToken);
  } else {
    Assert.notEqual(directory._syncToken, lastSyncToken);
  }

  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 testGoogle() {
  CardDAVServer.mimicGoogle = true;
  Services.prefs.setBoolPref("ldap_2.servers.carddav.carddav.vcard3", true);
  await subtest();
  Services.prefs.clearUserPref("ldap_2.servers.carddav.carddav.vcard3");
  CardDAVServer.mimicGoogle = 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");
});

add_task(async function testExpiredToken() {
  // Put some cards on the server.
  CardDAVServer.putCardInternal(
    "first.vcf",
    "BEGIN:VCARD\r\nUID:first\r\nFN:First Person\r\nEND:VCARD\r\n"
  );
  CardDAVServer.putCardInternal(
    "second.vcf",
    "BEGIN:VCARD\r\nUID:second\r\nFN:Second Person\r\nEND:VCARD\r\n"
  );
  CardDAVServer.putCardInternal(
    "third.vcf",
    "BEGIN:VCARD\r\nUID:third\r\nFN:Third Person\r\nEND:VCARD\r\n"
  );

  let directory = initDirectory();

  info("Initial sync with server.");
  await directory.fetchAllFromServer();

  info(`Token is: ${directory._syncToken}`);

  info("Cards:");
  for (let card of directory.childCards) {
    info(
      ` ${card.displayName} [${card.getProperty(
        "_href",
        ""
      )}, ${card.getProperty("_etag", "")}]`
    );
  }

  Assert.equal(directory.childCardCount, 3);
  Assert.deepEqual(Array.from(directory.childCards, c => c.UID).sort(), [
    "first",
    "second",
    "third",
  ]);

  // Corrupt the sync token. This will cause a 400 Bad Request response and a
  // complete resync should happen.

  directory._syncToken = "wrong token";

  // Make some changes on the server.

  CardDAVServer.putCardInternal(
    "fourth.vcf",
    "BEGIN:VCARD\r\nUID:fourth\r\nFN:Fourth\r\nEND:VCARD\r\n"
  );
  CardDAVServer.putCardInternal(
    "second.vcf",
    "BEGIN:VCARD\r\nUID:second\r\nFN:Second Person, but different\r\nEND:VCARD\r\n"
  );
  CardDAVServer.deleteCardInternal("first.vcf");

  // Sync with the server.

  info("Sync with server.");

  let notificationPromise = TestUtils.topicObserved(
    "addrbook-directory-invalidated"
  );
  observer.init();
  await directory.updateAllFromServerV2();
  // Check what notifications were fired. There should be an "invalidated"
  // notification, making the others redundant, but the "deleted"
  // notification is hard to avoid.
  observer.checkAndClearNotifications({
    "addrbook-contact-created": [],
    "addrbook-contact-updated": [],
    "addrbook-contact-deleted": ["first"],
  });
  await notificationPromise;

  info(`Token is now: ${directory._syncToken}`);

  info("Cards:");
  for (let card of directory.childCards) {
    info(
      ` ${card.displayName} [${card.getProperty(
        "_href",
        ""
      )}, ${card.getProperty("_etag", "")}]`
    );
  }

  // Check that the changes were synced.

  Assert.equal(directory.childCardCount, 3);
  Assert.deepEqual(Array.from(directory.childCards, c => c.UID).sort(), [
    "fourth",
    "second",
    "third",
  ]);
  Assert.equal(
    directory.childCards.find(c => c.UID == "second").displayName,
    "Second Person, but different"
  );

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