/**
 * Tests FormAutofillStorage object with addresses records.
 */

"use strict";

const TEST_STORE_FILE_NAME = "test-profile.json";
const COLLECTION_NAME = "addresses";

const TEST_ADDRESS_1 = {
  "given-name": "Timothy",
  "additional-name": "John",
  "family-name": "Berners-Lee",
  organization: "World Wide Web Consortium",
  "street-address": "32 Vassar Street\nMIT Room 32-G524",
  "address-level2": "Cambridge",
  "address-level1": "MA",
  "postal-code": "02139",
  country: "US",
  tel: "+16172535702",
  email: "timbl@w3.org",
  "unknown-1": "an unknown field from another client",
};

const TEST_ADDRESS_2 = {
  "street-address": "Some Address",
  country: "US",
};

const TEST_ADDRESS_3 = {
  "given-name": "Timothy",
  "family-name": "Berners-Lee",
  "street-address": "Other Address",
  "postal-code": "12345",
};

const TEST_ADDRESS_4 = {
  "given-name": "Timothy",
  "additional-name": "John",
  "family-name": "Berners-Lee",
  organization: "World Wide Web Consortium",
};

const TEST_ADDRESS_WITH_EMPTY_FIELD = {
  name: "Tim Berners",
  "street-address": "",
};

const TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD = {
  name: "",
  "address-line1": "",
  "address-line2": "",
  "address-line3": "",
  "country-name": "",
  "tel-country-code": "",
  "tel-national": "",
  "tel-area-code": "",
  "tel-local": "",
  "tel-local-prefix": "",
  "tel-local-suffix": "",
  email: "timbl@w3.org",
};

const TEST_ADDRESS_WITH_INVALID_FIELD = {
  "street-address": "Another Address",
  email: { email: "invalidemail" },
};

const TEST_ADDRESS_EMPTY_AFTER_NORMALIZE = {
  country: "XXXXXX",
};

const TEST_ADDRESS_EMPTY_AFTER_UPDATE_ADDRESS_2 = {
  "street-address": "",
  country: "XXXXXX",
};

const MERGE_TESTCASES = [
  {
    description: "Merge a superset",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      "unknown-1": "an unknown field from another client",
    },
    addressToMerge: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
      "unknown-1": "an unknown field from another client",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
      "unknown-1": "an unknown field from another client",
    },
  },
  {
    description: "Loose merge a subset",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
    },
    addressToMerge: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
    },
    noNeedToUpdate: true,
  },
  {
    description: "Strict merge a subset without empty string",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
    },
    addressToMerge: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
    },
    strict: true,
    noNeedToUpdate: true,
  },
  {
    description: "Merge an address with partial overlaps",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description:
      "Merge an address with multi-line street-address in storage and single-line incoming one",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "331 E. Evelyn Avenue Line2",
      tel: "+16509030800",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description:
      "Merge an address with 3-line street-address in storage and 2-line incoming one",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2\nLine3",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "331 E. Evelyn Avenue\nLine2 Line3",
      tel: "+16509030800",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2\nLine3",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description:
      "Merge an address with single-line street-address in storage and multi-line incoming one",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue Line2",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "331 E. Evelyn Avenue\nLine2",
      tel: "+16509030800",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description:
      "Merge an address with 2-line street-address in storage and 3-line incoming one",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2 Line3",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "331 E. Evelyn Avenue\nLine2\nLine3",
      tel: "+16509030800",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2\nLine3",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description: "Merge an address with the same amount of lines",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2\nLine3",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "331 E. Evelyn\nAvenue Line2\nLine3",
      tel: "+16509030800",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2\nLine3",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description:
      "Merge an address with superfluous external and internal whitespace in the street-address",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2\nLine3",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "  331 E. Evelyn\n  Avenue Line2\n  Line3  ",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue\nLine2\nLine3",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description: "Merge an address with collapsed whitespace",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "331 E.Evelyn Avenue",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description: "Merge an address with punctuation and mIxEd-cAsE",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "331.e.EVELYN AVENUE",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Avenue",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description: "Merge an address with accent characters",
    addressInStorage: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Straße",
      tel: "+16509030800",
    },
    addressToMerge: {
      "street-address": "331.e.EVELYN Strasse",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      "street-address": "331 E. Evelyn Straße",
      tel: "+16509030800",
      country: "US",
    },
  },
  {
    description: "Merge an address with a mIxEd-cAsE name",
    addressInStorage: {
      "given-name": "Timothy",
      tel: "+16509030800",
    },
    addressToMerge: {
      "given-name": "TIMOTHY",
      tel: "+16509030800",
      country: "US",
    },
    expectedAddress: {
      "given-name": "Timothy",
      tel: "+16509030800",
      country: "US",
    },
  },
];

ChromeUtils.defineESModuleGetters(this, {
  Preferences: "resource://gre/modules/Preferences.sys.mjs",
});

let do_check_record_matches = (recordWithMeta, record) => {
  for (let key in record) {
    Assert.equal(recordWithMeta[key], record[key]);
  }
};

add_task(async function test_initialize() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);

  Assert.equal(profileStorage._store.data.version, 1);
  Assert.equal(profileStorage._store.data.addresses.length, 0);

  let data = profileStorage._store.data;
  Assert.deepEqual(data.addresses, []);

  await profileStorage._saveImmediately();

  profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);

  Assert.deepEqual(profileStorage._store.data, data);
  for (let { _sync } of profileStorage._store.data.addresses) {
    Assert.ok(_sync);
    Assert.equal(_sync.changeCounter, 1);
  }
});

add_task(async function test_getAll() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
    TEST_ADDRESS_2,
  ]);

  let addresses = await profileStorage.addresses.getAll();

  Assert.equal(addresses.length, 2);
  do_check_record_matches(addresses[0], TEST_ADDRESS_1);
  do_check_record_matches(addresses[1], TEST_ADDRESS_2);

  // Check computed fields.
  Assert.equal(addresses[0].name, "Timothy John Berners-Lee");
  Assert.equal(addresses[0]["address-line1"], "32 Vassar Street");
  Assert.equal(addresses[0]["address-line2"], "MIT Room 32-G524");

  // Test with rawData set.
  addresses = await profileStorage.addresses.getAll({ rawData: true });
  Assert.equal(addresses[0].name, undefined);
  Assert.equal(addresses[0]["address-line1"], undefined);
  Assert.equal(addresses[0]["address-line2"], undefined);

  // Modifying output shouldn't affect the storage.
  addresses[0].organization = "test";
  do_check_record_matches(
    (await profileStorage.addresses.getAll())[0],
    TEST_ADDRESS_1
  );
});

add_task(async function test_get() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
    TEST_ADDRESS_2,
  ]);

  let addresses = await profileStorage.addresses.getAll();
  let guid = addresses[0].guid;

  let address = await profileStorage.addresses.get(guid);
  do_check_record_matches(address, TEST_ADDRESS_1);

  // Test with rawData set.
  address = await profileStorage.addresses.get(guid, { rawData: true });
  Assert.equal(address.name, undefined);
  Assert.equal(address["address-line1"], undefined);
  Assert.equal(address["address-line2"], undefined);

  // Modifying output shouldn't affect the storage.
  address.organization = "test";
  do_check_record_matches(
    await profileStorage.addresses.get(guid),
    TEST_ADDRESS_1
  );

  Assert.equal(await profileStorage.addresses.get("INVALID_GUID"), null);
});

add_task(async function test_add() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
    TEST_ADDRESS_2,
  ]);

  let addresses = await profileStorage.addresses.getAll();

  Assert.equal(addresses.length, 2);

  do_check_record_matches(addresses[0], TEST_ADDRESS_1);
  do_check_record_matches(addresses[1], TEST_ADDRESS_2);

  Assert.notEqual(addresses[0].guid, undefined);
  Assert.equal(addresses[0].version, 1);
  Assert.notEqual(addresses[0].timeCreated, undefined);
  Assert.equal(addresses[0].timeLastModified, addresses[0].timeCreated);
  Assert.equal(addresses[0].timeLastUsed, 0);
  Assert.equal(addresses[0].timesUsed, 0);

  // Empty string should be deleted before saving.
  await profileStorage.addresses.add(TEST_ADDRESS_WITH_EMPTY_FIELD);
  let address = profileStorage.addresses._data[2];
  Assert.equal(address.name, TEST_ADDRESS_WITH_EMPTY_FIELD.name);
  Assert.equal(address["street-address"], undefined);

  // Empty computed fields shouldn't cause any problem.
  await profileStorage.addresses.add(TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD);
  address = profileStorage.addresses._data[3];
  Assert.equal(address.email, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD.email);

  await Assert.rejects(
    profileStorage.addresses.add(TEST_ADDRESS_WITH_INVALID_FIELD),
    /"email" contains invalid data type: object/
  );

  await Assert.rejects(
    profileStorage.addresses.add({}),
    /Record contains no valid field\./
  );

  await Assert.rejects(
    profileStorage.addresses.add(TEST_ADDRESS_EMPTY_AFTER_NORMALIZE),
    /Record contains no valid field\./
  );
});

add_task(async function test_update() {
  // Test assumes that when an entry is saved a second time, it's last modified date will
  // be different from the first. With high values of precision reduction, we execute too
  // fast for that to be true.
  let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
  Preferences.set("privacy.reduceTimerPrecision", false);

  registerCleanupFunction(function () {
    Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
  });

  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
    TEST_ADDRESS_2,
  ]);

  let addresses = await profileStorage.addresses.getAll();
  let guid = addresses[1].guid;
  // We need to cheat a little due to race conditions of Date.now() when
  // we're running these tests, so we subtract one and test accordingly
  // in the times Date.now() returns the same timestamp
  let timeLastModified = addresses[1].timeLastModified - 1;

  let onChanged = TestUtils.topicObserved(
    "formautofill-storage-changed",
    (subject, data) =>
      data == "update" &&
      subject.wrappedJSObject.guid == guid &&
      subject.wrappedJSObject.collectionName == COLLECTION_NAME
  );

  Assert.notEqual(addresses[1].country, undefined);

  await profileStorage.addresses.update(guid, TEST_ADDRESS_3);
  await onChanged;
  await profileStorage._saveImmediately();

  profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below.

  let address = await profileStorage.addresses.get(guid, { rawData: true });

  Assert.equal(address.country, undefined);
  Assert.ok(address.timeLastModified > timeLastModified);
  do_check_record_matches(address, TEST_ADDRESS_3);
  Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1);

  // Test preserveOldProperties parameter and field with empty string.
  await profileStorage.addresses.update(
    guid,
    TEST_ADDRESS_WITH_EMPTY_FIELD,
    true
  );
  await onChanged;
  await profileStorage._saveImmediately();

  profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below.

  address = await profileStorage.addresses.get(guid, { rawData: true });

  Assert.equal(address["given-name"], "Tim");
  Assert.equal(address["family-name"], "Berners");
  Assert.equal(address["street-address"], undefined);
  Assert.equal(address["postal-code"], "12345");
  Assert.notEqual(address.timeLastModified, timeLastModified);
  Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 2);

  // Empty string should be deleted while updating.
  await profileStorage.addresses.update(
    profileStorage.addresses._data[0].guid,
    TEST_ADDRESS_WITH_EMPTY_FIELD
  );
  address = profileStorage.addresses._data[0];
  Assert.equal(address.name, TEST_ADDRESS_WITH_EMPTY_FIELD.name);
  Assert.equal(address["street-address"], undefined);
  Assert.equal(address[("unknown-1", "an unknown field from another client")]);

  // Empty computed fields shouldn't cause any problem.
  await profileStorage.addresses.update(
    profileStorage.addresses._data[0].guid,
    TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD,
    false
  );
  address = profileStorage.addresses._data[0];
  Assert.equal(address.email, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD.email);
  await profileStorage.addresses.update(
    profileStorage.addresses._data[1].guid,
    TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD,
    true
  );
  address = profileStorage.addresses._data[1];
  Assert.equal(address.email, TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD.email);

  await Assert.rejects(
    profileStorage.addresses.update("INVALID_GUID", TEST_ADDRESS_3),
    /No matching record\./
  );

  await Assert.rejects(
    profileStorage.addresses.update(guid, TEST_ADDRESS_WITH_INVALID_FIELD),
    /"email" contains invalid data type: object/
  );

  await Assert.rejects(
    profileStorage.addresses.update(guid, {}),
    /Record contains no valid field\./
  );

  await Assert.rejects(
    profileStorage.addresses.update(guid, TEST_ADDRESS_EMPTY_AFTER_NORMALIZE),
    /Record contains no valid field\./
  );

  profileStorage.addresses.update(guid, TEST_ADDRESS_2);
  await Assert.rejects(
    profileStorage.addresses.update(
      guid,
      TEST_ADDRESS_EMPTY_AFTER_UPDATE_ADDRESS_2
    ),
    /Record contains no valid field\./
  );
});

add_task(async function test_notifyUsed() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
    TEST_ADDRESS_2,
  ]);

  let addresses = await profileStorage.addresses.getAll();
  let guid = addresses[1].guid;
  let timeLastUsed = addresses[1].timeLastUsed;
  let timesUsed = addresses[1].timesUsed;

  profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below.
  let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);

  let onChanged = TestUtils.topicObserved(
    "formautofill-storage-changed",
    (subject, data) =>
      data == "notifyUsed" &&
      subject.wrappedJSObject.guid == guid &&
      subject.wrappedJSObject.collectionName == COLLECTION_NAME
  );

  profileStorage.addresses.notifyUsed(guid);
  await onChanged;

  let address = await profileStorage.addresses.get(guid);

  Assert.equal(address.timesUsed, timesUsed + 1);
  Assert.notEqual(address.timeLastUsed, timeLastUsed);

  // Using a record should not bump its change counter.
  Assert.equal(
    getSyncChangeCounter(profileStorage.addresses, guid),
    changeCounter
  );

  Assert.throws(
    () => profileStorage.addresses.notifyUsed("INVALID_GUID"),
    /No matching record\./
  );
});

add_task(async function test_remove() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
    TEST_ADDRESS_2,
  ]);

  let addresses = await profileStorage.addresses.getAll();
  let guid = addresses[1].guid;

  let onChanged = TestUtils.topicObserved(
    "formautofill-storage-changed",
    (subject, data) =>
      data == "remove" &&
      subject.wrappedJSObject.guid == guid &&
      subject.wrappedJSObject.collectionName == COLLECTION_NAME
  );

  Assert.equal(addresses.length, 2);

  profileStorage.addresses.remove(guid);
  await onChanged;

  addresses = await profileStorage.addresses.getAll();

  Assert.equal(addresses.length, 1);

  Assert.equal(await profileStorage.addresses.get(guid), null);
});

MERGE_TESTCASES.forEach(testcase => {
  add_task(async function test_merge() {
    info("Starting testcase: " + testcase.description);
    let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
      testcase.addressInStorage,
    ]);
    let addresses = await profileStorage.addresses.getAll();
    let guid = addresses[0].guid;
    // We need to cheat a little due to race conditions of Date.now() when
    // we're running these tests, so we subtract one and test accordingly
    // in the times Date.now() returns the same timestamp
    let timeLastModified = addresses[0].timeLastModified - 1;

    // Merge address and verify the guid in notifyObservers subject
    let onMerged = TestUtils.topicObserved(
      "formautofill-storage-changed",
      (subject, data) =>
        data == "update" &&
        subject.wrappedJSObject.guid == guid &&
        subject.wrappedJSObject.collectionName == COLLECTION_NAME
    );

    // Force to create sync metadata.
    profileStorage.addresses.pullSyncChanges();
    Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1);

    Assert.ok(
      profileStorage.addresses.mergeIfPossible(
        guid,
        testcase.addressToMerge,
        testcase.strict
      )
    );
    if (!testcase.noNeedToUpdate) {
      await onMerged;
    }

    addresses = await profileStorage.addresses.getAll();
    Assert.equal(addresses.length, 1);
    do_check_record_matches(addresses[0], testcase.expectedAddress);
    if (testcase.noNeedToUpdate) {
      // see timeLastModified for why we check -1
      Assert.equal(addresses[0].timeLastModified - 1, timeLastModified);

      // No need to bump the change counter if the data is unchanged.
      Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1);
    } else {
      Assert.ok(addresses[0].timeLastModified > timeLastModified);

      // Record merging should bump the change counter.
      Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 2);
    }
  });
});

add_task(async function test_merge_same_address() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
  ]);
  let addresses = await profileStorage.addresses.getAll();
  let guid = addresses[0].guid;
  let timeLastModified = addresses[0].timeLastModified;

  // Force to create sync metadata.
  profileStorage.addresses.pullSyncChanges();
  Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1);

  // Merge same address will still return true but it won't update timeLastModified.
  Assert.ok(profileStorage.addresses.mergeIfPossible(guid, TEST_ADDRESS_1));
  Assert.equal(addresses[0].timeLastModified, timeLastModified);

  // ... and won't bump the change counter, either.
  Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1);
});

add_task(async function test_merge_unable_merge() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
    TEST_ADDRESS_2,
  ]);

  let addresses = await profileStorage.addresses.getAll();
  let guid = addresses[1].guid;

  // Force to create sync metadata.
  profileStorage.addresses.pullSyncChanges();
  Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1);

  // Unable to merge because of conflict
  Assert.equal(
    await profileStorage.addresses.mergeIfPossible(guid, TEST_ADDRESS_3),
    false
  );

  // Unable to merge because no overlap
  Assert.equal(
    await profileStorage.addresses.mergeIfPossible(guid, TEST_ADDRESS_4),
    false
  );

  // Unable to strict merge because subset with empty string
  let subset = Object.assign({}, TEST_ADDRESS_1);
  subset.organization = "";
  Assert.equal(
    await profileStorage.addresses.mergeIfPossible(guid, subset, true),
    false
  );

  // Shouldn't bump the change counter
  Assert.equal(getSyncChangeCounter(profileStorage.addresses, guid), 1);
});

add_task(async function test_mergeToStorage() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
    TEST_ADDRESS_2,
  ]);
  // Merge an address to storage
  let anotherAddress = profileStorage.addresses._clone(TEST_ADDRESS_2);
  await profileStorage.addresses.add(anotherAddress);
  anotherAddress.email = "timbl@w3.org";
  Assert.equal(
    (await profileStorage.addresses.mergeToStorage(anotherAddress)).length,
    2
  );

  Assert.equal(
    (await profileStorage.addresses.getAll())[1].email,
    anotherAddress.email
  );
  Assert.equal(
    (await profileStorage.addresses.getAll())[2].email,
    anotherAddress.email
  );

  // Empty computed fields shouldn't cause any problem.
  Assert.equal(
    (
      await profileStorage.addresses.mergeToStorage(
        TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD
      )
    ).length,
    3
  );
});

add_task(async function test_mergeToStorage_strict() {
  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [
    TEST_ADDRESS_1,
    TEST_ADDRESS_2,
  ]);
  // Try to merge a subset with empty string
  let anotherAddress = profileStorage.addresses._clone(TEST_ADDRESS_1);
  anotherAddress.email = "";
  Assert.equal(
    (await profileStorage.addresses.mergeToStorage(anotherAddress, true))
      .length,
    0
  );
  Assert.equal(
    (await profileStorage.addresses.getAll())[0].email,
    TEST_ADDRESS_1.email
  );

  // Empty computed fields shouldn't cause any problem.
  Assert.equal(
    (
      await profileStorage.addresses.mergeToStorage(
        TEST_ADDRESS_WITH_EMPTY_COMPUTED_FIELD,
        true
      )
    ).length,
    1
  );
});
