"use strict";

const TEST_STORE_FILE_NAME = "test-profile.json";

// NOTE: a guide to reading these test-cases:
// parent: What the local record looked like the last time we wrote the
//         record to the Sync server.
// local:  What the local record looks like now. IOW, the differences between
//         'parent' and 'local' are changes recently made which we wish to sync.
// remote: An incoming record we need to apply (ie, a record that was possibly
//         changed on a remote device)
//
// To further help understanding this, a few of the testcases are annotated.
const RECONCILE_TESTCASES = [
  {
    description: "Local change",
    parent: {
      // So when we last wrote the record to the server, it had these values.
      "guid": "2bbd2d8fbc6b",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    local: [{
      // The current local record - by comparing against parent we can see that
      // only the given-name has changed locally.
      "given-name": "Skip",
      "family-name": "Hammond",
    }],
    remote: {
      // This is the incoming record. It has the same values as "parent", so
      // we can deduce the record hasn't actually been changed remotely so we
      // can safely ignore the incoming record and write our local changes.
      "guid": "2bbd2d8fbc6b",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    reconciled: {
      "guid": "2bbd2d8fbc6b",
      "given-name": "Skip",
      "family-name": "Hammond",
    },
  },
  {
    description: "Remote change",
    parent: {
      "guid": "e3680e9f890d",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    local: [{
      "given-name": "Mark",
      "family-name": "Hammond",
    }],
    remote: {
      "guid": "e3680e9f890d",
      "version": 1,
      "given-name": "Skip",
      "family-name": "Hammond",
    },
    reconciled: {
      "guid": "e3680e9f890d",
      "given-name": "Skip",
      "family-name": "Hammond",
    },
  },
  {
    description: "New local field",
    parent: {
      "guid": "0cba738b1be0",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    local: [{
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
    }],
    remote: {
      "guid": "0cba738b1be0",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    reconciled: {
      "guid": "0cba738b1be0",
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
    },
  },
  {
    description: "New remote field",
    parent: {
      "guid": "be3ef97f8285",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    local: [{
      "given-name": "Mark",
      "family-name": "Hammond",
    }],
    remote: {
      "guid": "be3ef97f8285",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
    },
    reconciled: {
      "guid": "be3ef97f8285",
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
    },
  },
  {
    description: "Deleted field locally",
    parent: {
      "guid": "9627322248ec",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
    },
    local: [{
      "given-name": "Mark",
      "family-name": "Hammond",
    }],
    remote: {
      "guid": "9627322248ec",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
    },
    reconciled: {
      "guid": "9627322248ec",
      "given-name": "Mark",
      "family-name": "Hammond",
    },
  },
  {
    description: "Deleted field remotely",
    parent: {
      "guid": "7d7509f3eeb2",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
    },
    local: [{
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
    }],
    remote: {
      "guid": "7d7509f3eeb2",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    reconciled: {
      "guid": "7d7509f3eeb2",
      "given-name": "Mark",
      "family-name": "Hammond",
    },
  },
  {
    description: "Local and remote changes to unrelated fields",
    parent: {
      // The last time we wrote this to the server, country was NZ.
      "guid": "e087a06dfc57",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "country": "NZ",
    },
    local: [{
      // The current local record - so locally we've changed given-name to Skip.
      "given-name": "Skip",
      "family-name": "Hammond",
      "country": "NZ",
    }],
    remote: {
      // Remotely, we've changed the country to AU.
      "guid": "e087a06dfc57",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "country": "AU",
    },
    reconciled: {
      "guid": "e087a06dfc57",
      "given-name": "Skip",
      "family-name": "Hammond",
      "country": "AU",
    },
  },
  {
    description: "Multiple local changes",
    parent: {
      "guid": "340a078c596f",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
    },
    local: [{
      "given-name": "Skip",
      "family-name": "Hammond",
    }, {
      "given-name": "Skip",
      "family-name": "Hammond",
      "organization": "Mozilla",
    }],
    remote: {
      "guid": "340a078c596f",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "tel": "123456",
      "country": "AU",
    },
    reconciled: {
      "guid": "340a078c596f",
      "given-name": "Skip",
      "family-name": "Hammond",
      "organization": "Mozilla",
      "country": "AU",
    },
  },
  {
    // Local and remote diverged from the shared parent, but the values are the
    // same, so we shouldn't fork.
    description: "Same change to local and remote",
    parent: {
      "guid": "0b3a72a1bea2",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    local: [{
      "given-name": "Skip",
      "family-name": "Hammond",
    }],
    remote: {
      "guid": "0b3a72a1bea2",
      "version": 1,
      "given-name": "Skip",
      "family-name": "Hammond",
    },
    reconciled: {
      "guid": "0b3a72a1bea2",
      "given-name": "Skip",
      "family-name": "Hammond",
    },
  },
  {
    description: "Conflicting changes to single field",
    parent: {
      // This is what we last wrote to the sync server.
      "guid": "62068784d089",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    local: [{
      // The current version of the local record - the given-name has changed locally.
      "given-name": "Skip",
      "family-name": "Hammond",
    }],
    remote: {
      // An incoming record has a different given-name than any of the above!
      "guid": "62068784d089",
      "version": 1,
      "given-name": "Kip",
      "family-name": "Hammond",
    },
    forked: {
      // So we've forked the local record to a new GUID (and the next sync is
      // going to write this as a new record)
      "given-name": "Skip",
      "family-name": "Hammond",
    },
    reconciled: {
      // And we've updated the local version of the record to be the remote version.
      guid: "62068784d089",
      "given-name": "Kip",
      "family-name": "Hammond",
    },
  },
  {
    description: "Conflicting changes to multiple fields",
    parent: {
      "guid": "244dbb692e94",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "country": "NZ",
    },
    local: [{
      "given-name": "Skip",
      "family-name": "Hammond",
      "country": "AU",
    }],
    remote: {
      "guid": "244dbb692e94",
      "version": 1,
      "given-name": "Kip",
      "family-name": "Hammond",
      "country": "CA",
    },
    forked: {
      "given-name": "Skip",
      "family-name": "Hammond",
      "country": "AU",
    },
    reconciled: {
      "guid": "244dbb692e94",
      "given-name": "Kip",
      "family-name": "Hammond",
      "country": "CA",
    },
  },
  {
    description: "Field deleted locally, changed remotely",
    parent: {
      "guid": "6fc45e03d19a",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "country": "AU",
    },
    local: [{
      "given-name": "Mark",
      "family-name": "Hammond",
    }],
    remote: {
      "guid": "6fc45e03d19a",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "country": "NZ",
    },
    forked: {
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    reconciled: {
      "guid": "6fc45e03d19a",
      "given-name": "Mark",
      "family-name": "Hammond",
      "country": "NZ",
    },
  },
  {
    description: "Field changed locally, deleted remotely",
    parent: {
      "guid": "fff9fa27fa18",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "country": "AU",
    },
    local: [{
      "given-name": "Mark",
      "family-name": "Hammond",
      "country": "NZ",
    }],
    remote: {
      "guid": "fff9fa27fa18",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
    },
    forked: {
      "given-name": "Mark",
      "family-name": "Hammond",
      "country": "NZ",
    },
    reconciled: {
      "guid": "fff9fa27fa18",
      "given-name": "Mark",
      "family-name": "Hammond",
    },
  },
  {
    // Created, last modified should be synced; last used and times used should
    // be local. Remote created time older than local, remote modified time
    // newer than local.
    description: "Created, last modified time reconciliation without local changes",
    parent: {
      "guid": "5113f329c42f",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "timeCreated": 1234,
      "timeLastModified": 5678,
      "timeLastUsed": 5678,
      "timesUsed": 6,
    },
    local: [],
    remote: {
      "guid": "5113f329c42f",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "timeCreated": 1200,
      "timeLastModified": 5700,
      "timeLastUsed": 5700,
      "timesUsed": 3,
    },
    reconciled: {
      "guid": "5113f329c42f",
      "given-name": "Mark",
      "family-name": "Hammond",
      "timeCreated": 1200,
      "timeLastModified": 5700,
      "timeLastUsed": 5678,
      "timesUsed": 6,
    },
  },
  {
    // Local changes, remote created time newer than local, remote modified time
    // older than local.
    description: "Created, last modified time reconciliation with local changes",
    parent: {
      "guid": "791e5608b80a",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "timeCreated": 1234,
      "timeLastModified": 5678,
      "timeLastUsed": 5678,
      "timesUsed": 6,
    },
    local: [{
      "given-name": "Skip",
      "family-name": "Hammond",
    }],
    remote: {
      "guid": "791e5608b80a",
      "version": 1,
      "given-name": "Mark",
      "family-name": "Hammond",
      "timeCreated": 1300,
      "timeLastModified": 5000,
      "timeLastUsed": 5000,
      "timesUsed": 3,
    },
    reconciled: {
      "guid": "791e5608b80a",
      "given-name": "Skip",
      "family-name": "Hammond",
      "timeCreated": 1234,
      "timeLastUsed": 5678,
      "timesUsed": 6,
    },
  },
];

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

  // Cross-version reconciliation isn't supported yet. See bug 1377204.
  throws(() => {
    profileStorage.addresses.reconcile({
      "guid": "31d83d2725ec",
      "version": 2,
      "given-name": "Mark",
      "family-name": "Hammond",
    });
  }, /Got unknown record version/);
});

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

  let guid = "de1ba7b094fe";
  profileStorage.addresses.add({
    guid,
    version: 1,
    "given-name": "Mark",
    "family-name": "Hammond",
  }, {sourceSync: true});
  profileStorage.addresses.update(guid, {
    "given-name": "Skip",
    "family-name": "Hammond",
    "organization": "Mozilla",
  });

  let remote = {
    guid,
    "version": 1,
    "given-name": "Mark",
    "family-name": "Hammond",
    "tel": "123456",
  };

  {
    let {forkedGUID} = profileStorage.addresses.reconcile(remote);
    let updatedRecord = profileStorage.addresses.get(guid, {
      rawData: true,
    });

    ok(!forkedGUID, "First merge should not fork record");
    ok(objectMatches(updatedRecord, {
      "guid": "de1ba7b094fe",
      "given-name": "Skip",
      "family-name": "Hammond",
      "organization": "Mozilla",
      "tel": "123456",
    }), "First merge should merge local and remote changes");
  }

  {
    let {forkedGUID} = profileStorage.addresses.reconcile(remote);
    let updatedRecord = profileStorage.addresses.get(guid, {
      rawData: true,
    });

    ok(!forkedGUID, "Second merge should not fork record");
    ok(objectMatches(updatedRecord, {
      "guid": "de1ba7b094fe",
      "given-name": "Skip",
      "family-name": "Hammond",
      "organization": "Mozilla",
      "tel": "123456",
    }), "Second merge should not change record");
  }
});

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

  for (let test of RECONCILE_TESTCASES) {
    do_print(test.description);

    profileStorage.addresses.add(test.parent, {sourceSync: true});

    for (let updatedRecord of test.local) {
      profileStorage.addresses.update(test.parent.guid, updatedRecord);
    }

    let localRecord = profileStorage.addresses.get(test.parent.guid, {
      rawData: true,
    });

    let {forkedGUID} = profileStorage.addresses.reconcile(test.remote);
    let reconciledRecord = profileStorage.addresses.get(test.parent.guid, {
      rawData: true,
    });
    if (forkedGUID) {
      let forkedRecord = profileStorage.addresses.get(forkedGUID, {
        rawData: true,
      });

      notEqual(forkedRecord.guid, reconciledRecord.guid);
      equal(forkedRecord.timeLastModified, localRecord.timeLastModified);
      ok(objectMatches(forkedRecord, test.forked),
        `${test.description} should fork record`);
    } else {
      ok(!test.forked, `${test.description} should not fork record`);
    }

    ok(objectMatches(reconciledRecord, test.reconciled));
  }
});
