"use strict";

// Fix the environment to run Contacts tests
if (SpecialPowers.isMainProcess()) {
  SpecialPowers.Cu.import("resource://gre/modules/ContactService.jsm");
} else {
  SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('contacts_chromescript.js'));
}

// Some helpful global vars
var isAndroid = (navigator.userAgent.indexOf("Android") !== -1);

var defaultOptions = {
  sortBy: "givenName",
};

// Make sure we only touch |navigator.mozContacts| after we have the necessary
// permissions, or we'll race when checking the listen permission needed for the
// oncontactchange event. This is only needed for tests because at first we have
// the permission set to UNKNOWN_ACTION. That should never happen for real apps,
// see dom/apps/PermissionsTable.jsm.
var mozContacts;

// To test sorting
var c1 = {
  name: ["a a"],
  familyName: ["a"],
  givenName: ["a"],
};

var c2 = {
  name: ["b b"],
  familyName: ["b"],
  givenName: ["b"],
};

var c3 = {
  name: ["c c", "a a", "b b"],
  familyName: ["c","a","b"],
  givenName: ["c","a","b"],
};

var c4 = {
  name: ["c c", "a a", "c c"],
  familyName: ["c","a","c"],
  givenName: ["c","a","c"],
};

var c5 = {
  familyName: [],
  givenName: [],
};

var c6 = {
  name: ["e"],
  familyName: ["e","e","e"],
  givenName: ["e","e","e"],
};

var c7 = {
  name: ["e"],
  familyName: ["e","e","e"],
  givenName: ["e","e","e"],
};

var c8 = {
  name: ["e"],
  familyName: ["e","e","e"],
  givenName: ["e","e","e"],
};

var adr1 = {
  type: ["work"],
  streetAddress: "street 1",
  locality: "locality 1",
  region: "region 1",
  postalCode: "postal code 1",
  countryName: "country 1"
};

var adr2 = {
  type: ["home, fax"],
  streetAddress: "street2",
  locality: "locality2",
  region: "region2",
  postalCode: "postal code2",
  countryName: "country2"
};

var properties1 = {
  // please keep capital letters at the start of these names
  name: ["Test1 TestFamilyName", "Test2 Wagner"],
  familyName: ["TestFamilyName","Wagner"],
  givenName: ["Test1","Test2"],
  phoneticFamilyName: ["TestphoneticFamilyName1","TestphoneticFamilyName2"],
  phoneticGivenName: ["TestphoneticGivenName1","TestphoneticGivenName2"],
  nickname: ["nicktest"],
  tel: [{type: ["work"], value: "123456", carrier: "testCarrier"} , {type: ["home", "fax"], value: "+55 (31) 9876-3456"}, {type: ["home"], value: "+49 451 491934"}],
  adr: [adr1],
  email: [{type: ["work"], value: "x@y.com"}],
};

var properties2 = {
  name: ["dummyHonorificPrefix dummyGivenName dummyFamilyName dummyHonorificSuffix", "dummyHonorificPrefix2"],
  familyName: ["dummyFamilyName"],
  givenName: ["dummyGivenName"],
  phoneticFamilyName: ["dummyphoneticFamilyName"],
  phoneticGivenName: ["dummyphoneticGivenName"],
  honorificPrefix: ["dummyHonorificPrefix","dummyHonorificPrefix2"],
  honorificSuffix: ["dummyHonorificSuffix"],
  additionalName: ["dummyadditionalName"],
  nickname: ["dummyNickname"],
  tel: [{type: ["test"], value: "7932012345", carrier: "myCarrier", pref: 1},{type: ["home", "custom"], value: "7932012346", pref: 0}],
  email: [{type: ["test"], value: "a@b.c"}, {value: "b@c.d", pref: 1}],
  adr: [adr1, adr2],
  impp: [{type: ["aim"], value:"im1", pref: 1}, {value: "im2"}],
  org: ["org1", "org2"],
  jobTitle: ["boss", "superboss"],
  note: ["test note"],
  category: ["cat1", "cat2"],
  url: [{type: ["work", "work2"], value: "www.1.com", pref: 1}, {value:"www2.com"}],
  bday: new Date("1980, 12, 01"),
  anniversary: new Date("2000, 12, 01"),
  sex: "male",
  genderIdentity: "test",
  key: ["ERPJ394GJJWEVJ0349GJ09W3H4FG0WFW80VHW3408GH30WGH348G3H"]
};

// To test sorting(CJK)
var c9 = {
  phoneticFamilyName: ["a"],
  phoneticGivenName: ["a"],
};

var c10 = {
  phoneticFamilyName: ["b"],
  phoneticGivenName: ["b"],
};

var c11 = {
  phoneticFamilyName: ["c","a","b"],
  phoneticGivenName: ["c","a","b"],
};

var c12 = {
  phoneticFamilyName: ["c","a","c"],
  phoneticGivenName: ["c","a","c"],
};

var c13 = {
  phoneticFamilyName: [],
  phoneticGivenName: [],
};

var c14 = {
  phoneticFamilyName: ["e","e","e"],
  phoneticGivenName: ["e","e","e"],
};

var c15 = {
  phoneticFamilyName: ["e","e","e"],
  phoneticGivenName: ["e","e","e"],
};

var c16 = {
  phoneticFamilyName: ["e","e","e"],
  phoneticGivenName: ["e","e","e"],
};

var properties3 = {
  // please keep capital letters at the start of these names
  name: ["Taro Yamada", "Ichiro Suzuki"],
  familyName: ["Yamada","Suzuki"],
  givenName: ["Taro","Ichiro"],
  phoneticFamilyName: ["TestPhoneticFamilyYamada","TestPhoneticFamilySuzuki"],
  phoneticGivenName: ["TestPhoneticGivenTaro","TestPhoneticGivenIchiro"],
  nickname: ["phoneticNicktest"],
  tel: [{type: ["work"], value: "123456", carrier: "testCarrier"} , {type: ["home", "fax"], value: "+55 (31) 9876-3456"}, {type: ["home"], value: "+49 451 491934"}],
  adr: [adr1],
  email: [{type: ["work"], value: "x@y.com"}],
};

var properties4 = {
  name: ["dummyHonorificPrefix dummyTaro dummyYamada dummyHonorificSuffix", "dummyHonorificPrefix2"],
  familyName: ["dummyYamada"],
  givenName: ["dummyTaro"],
  phoneticFamilyName: ["dummyTestPhoneticFamilyYamada"],
  phoneticGivenName: ["dummyTestPhoneticGivenTaro"],
  honorificPrefix: ["dummyPhoneticHonorificPrefix","dummyPhoneticHonorificPrefix2"],
  honorificSuffix: ["dummyPhoneticHonorificSuffix"],
  additionalName: ["dummyPhoneticAdditionalName"],
  nickname: ["dummyPhoneticNickname"],
  tel: [{type: ["test"], value: "7932012345", carrier: "myCarrier", pref: 1},{type: ["home", "custom"], value: "7932012346", pref: 0}],
  email: [{type: ["test"], value: "a@b.c"}, {value: "b@c.d", pref: 1}],
  adr: [adr1, adr2],
  impp: [{type: ["aim"], value:"im1", pref: 1}, {value: "im2"}],
  org: ["org1", "org2"],
  jobTitle: ["boss", "superboss"],
  note: ["test note"],
  category: ["cat1", "cat2"],
  url: [{type: ["work", "work2"], value: "www.1.com", pref: 1}, {value:"www2.com"}],
  bday: new Date("1980, 12, 01"),
  anniversary: new Date("2000, 12, 01"),
  sex: "male",
  genderIdentity: "test",
  key: ["ERPJ394GJJWEVJ0349GJ09W3H4FG0WFW80VHW3408GH30WGH348G3H"]
};

var sample_id1;
var sample_id2;

var createResult1;
var createResult2;

var findResult1;
var findResult2;

// DOMRequest helper functions
function onUnwantedSuccess() {
  ok(false, "onUnwantedSuccess: shouldn't get here");
}

function onFailure() {
  ok(false, "in on Failure!");
  next();
}

// Validation helper functions
function checkStr(str1, str2, msg) {
  if (str1 ^ str2) {
    ok(false, "Expected both strings to be either present or absent");
    return;
  }
  if (!str1 || str1 == "null") {
    str1 = null;
  }
  if (!str2 || str2 == "null") {
    str2 = null;
  }
  is(str1, str2, msg);
}

function checkStrArray(str1, str2, msg) {
  function normalize_falsy(v) {
    if (!v || v == "null" || v == "undefined") {
      return "";
    }
    return v;
  }
  function optArray(val) {
    return Array.isArray(val) ? val : [val];
  }
  str1 = optArray(str1).map(normalize_falsy).filter(v => v != "");
  str2 = optArray(str2).map(normalize_falsy).filter(v => v != "");
  is(JSON.stringify(str1), JSON.stringify(str2), msg);
}

function checkPref(pref1, pref2) {
  // If on Android treat one preference as 0 and the other as undefined as matching
  if (isAndroid) {
    if ((!pref1 && pref2 == undefined) || (pref1 == undefined && !pref2)) {
      pref1 = false;
      pref2 = false;
    }
  }
  is(!!pref1, !!pref2, "Same pref");
}

function checkAddress(adr1, adr2) {
  if (adr1 ^ adr2) {
    ok(false, "Expected both adrs to be either present or absent");
    return;
  }
  checkStrArray(adr1.type, adr2.type, "Same type");
  checkStr(adr1.streetAddress, adr2.streetAddress, "Same streetAddress");
  checkStr(adr1.locality, adr2.locality, "Same locality");
  checkStr(adr1.region, adr2.region, "Same region");
  checkStr(adr1.postalCode, adr2.postalCode, "Same postalCode");
  checkStr(adr1.countryName, adr2.countryName, "Same countryName");
  checkPref(adr1.pref, adr2.pref);
}

function checkField(field1, field2) {
  if (field1 ^ field2) {
    ok(false, "Expected both fields to be either present or absent");
    return;
  }
  checkStrArray(field1.type, field2.type, "Same type");
  checkStr(field1.value, field2.value, "Same value");
  checkPref(field1.pref, field2.pref);
}

function checkTel(tel1, tel2) {
  if (tel1 ^ tel2) {
    ok(false, "Expected both tels to be either present or absent");
    return;
  }
  checkField(tel1, tel2);
  checkStr(tel1.carrier, tel2.carrier, "Same carrier");
}

function checkCategory(category1, category2) {
  // Android adds contacts to the a default category. This should be removed from the
  // results before comparing them
  if (isAndroid) {
    category1 = removeAndroidDefaultCategory(category1);
    category2 = removeAndroidDefaultCategory(category2);
  }
  checkStrArray(category1, category2, "Same Category")
}

function removeAndroidDefaultCategory(category) {
  if (!category) {
    return category;
  }

  var result = [];

  for (var i of category) {
    // Some devices may return the full group name (prefixed with "System Group: ")
    if (i != "My Contacts" && i != "System Group: My Contacts") {
      result.push(i);
    }
  }

  return result;
}

function checkArrayField(array1, array2, func, msg) {
  if (!!array1 ^ !!array2) {
    ok(false, "Expected both arrays to be either present or absent: " + JSON.stringify(array1) + " vs. " + JSON.stringify(array2) + ". (" + msg + ")");
    return;
  }
  if (!array1 && !array2)  {
    ok(true, msg);
    return;
  }
  is(array1.length, array2.length, "Same length");
  for (var i = 0; i < array1.length; ++i) {
    func(array1[i], array2[i], msg);
  }
}

function checkContacts(contact1, contact2) {
  if (!!contact1 ^ !!contact2) {
    ok(false, "Expected both contacts to be either present or absent");
    return;
  }
  checkStrArray(contact1.name, contact2.name, "Same name");
  checkStrArray(contact1.honorificPrefix, contact2.honorificPrefix, "Same honorificPrefix");
  checkStrArray(contact1.givenName, contact2.givenName, "Same givenName");
  checkStrArray(contact1.additionalName, contact2.additionalName, "Same additionalName");
  checkStrArray(contact1.familyName, contact2.familyName, "Same familyName");
  checkStrArray(contact1.phoneticFamilyName, contact2.phoneticFamilyName, "Same phoneticFamilyName");
  checkStrArray(contact1.phoneticGivenName, contact2.phoneticGivenName, "Same phoneticGivenName");
  checkStrArray(contact1.honorificSuffix, contact2.honorificSuffix, "Same honorificSuffix");
  checkStrArray(contact1.nickname, contact2.nickname, "Same nickname");
  checkCategory(contact1.category, contact2.category);
  checkStrArray(contact1.org, contact2.org, "Same org");
  checkStrArray(contact1.jobTitle, contact2.jobTitle, "Same jobTitle");
  is(contact1.bday ? contact1.bday.valueOf() : null, contact2.bday ? contact2.bday.valueOf() : null, "Same birthday");
  checkStrArray(contact1.note, contact2.note, "Same note");
  is(contact1.anniversary ? contact1.anniversary.valueOf() : null , contact2.anniversary ? contact2.anniversary.valueOf() : null, "Same anniversary");
  checkStr(contact1.sex, contact2.sex, "Same sex");
  checkStr(contact1.genderIdentity, contact2.genderIdentity, "Same genderIdentity");
  checkStrArray(contact1.key, contact2.key, "Same key");

  checkArrayField(contact1.adr, contact2.adr, checkAddress, "Same adr");
  checkArrayField(contact1.tel, contact2.tel, checkTel, "Same tel");
  checkArrayField(contact1.email, contact2.email, checkField, "Same email");
  checkArrayField(contact1.url, contact2.url, checkField, "Same url");
  checkArrayField(contact1.impp, contact2.impp, checkField, "Same impp");
}

function addContacts() {
  ok(true, "Adding 40 contacts");
  let req;
  for (let i = 0; i < 39; ++i) {
    properties1.familyName[0] = "Testname" + (i < 10 ? "0" + i : i);
    properties1.name = [properties1.givenName[0] + " " + properties1.familyName[0]];
    createResult1 = new mozContact(properties1);
    req = mozContacts.save(createResult1);
    req.onsuccess = function() {
      ok(createResult1.id, "The contact now has an ID.");
    };
    req.onerror = onFailure;
  };
  properties1.familyName[0] = "Testname39";
  properties1.name = [properties1.givenName[0] + " Testname39"];
  createResult1 = new mozContact(properties1);
  req = mozContacts.save(createResult1);
  req.onsuccess = function() {
    ok(createResult1.id, "The contact now has an ID.");
    checkStrArray(createResult1.name, properties1.name, "Same Name");
    next();
  };
  req.onerror = onFailure;
}

function getOne(msg) {
  return function() {
    ok(true, msg || "Retrieving one contact with getAll");
    let req = mozContacts.getAll({});

    let count = 0;
    req.onsuccess = function(event) {
      ok(true, "on success");
      if (req.result) {
        ok(true, "result is valid");
        count++;
        req.continue();
      } else {
        is(count, 1, "last contact - only one contact returned");
        next();
      }
    };
    req.onerror = onFailure;
  };
}

function getAll(msg) {
  return function() {
    ok(true, msg || "Retrieving 40 contacts with getAll");
    let req = mozContacts.getAll({
      sortBy: "familyName",
      sortOrder: "ascending"
    });
    let count = 0;
    let result;
    let props;
    req.onsuccess = function(event) {
      if (req.result) {
        ok(true, "result is valid");
        result = req.result;
        properties1.familyName[0] = "Testname" + (count < 10 ? "0" + count : count);
        is(result.familyName[0], properties1.familyName[0], "Same familyName");
        count++;
        req.continue();
      } else {
        is(count, 40, "last contact - 40 contacts returned");
        next();
      }
    };
    req.onerror = onFailure;
  };
}

function clearTemps() {
  sample_id1 = null;
  sample_id2 = null;
  createResult1 = null;
  createResult2 = null;
  findResult1 = null;
  findResult2 = null;
}

function clearDatabase() {
  ok(true, "Deleting database");
  let req = mozContacts.clear()
  req.onsuccess = function () {
    ok(true, "Deleted the database");
    next();
  }
  req.onerror = onFailure;
}

function checkCount(count, msg, then) {
  var request = mozContacts.getCount();
  request.onsuccess = function(e) {
    is(e.target.result, count, msg);
    then();
  };
  request.onerror = onFailure;
}

// Helper functions to run tests
var index = 0;

function next() {
  info("Step " + index);
  if (index >= steps.length) {
    ok(false, "Shouldn't get here!");
    return;
  }
  try {
    var i = index++;
    steps[i]();
  } catch(ex) {
    ok(false, "Caught exception", ex);
  }
}

SimpleTest.waitForExplicitFinish();

function start_tests() {
  // Skip tests on Android < 4.0 due to test failures on tbpl (see bugs 897924 & 888891)
  let androidVersion = SpecialPowers.Cc['@mozilla.org/system-info;1']
                                    .getService(SpecialPowers.Ci.nsIPropertyBag2)
                                    .getProperty('version');
  if (!isAndroid || androidVersion >= 14) {
    SpecialPowers.pushPermissions([
      {type: "contacts-write", allow: 1, context: document},
      {type: "contacts-read", allow: 1, context: document},
      {type: "contacts-create", allow: 1, context: document},
    ], function() {
      mozContacts = navigator.mozContacts;
      next();
    });
  } else {
    ok(true, "Skip tests on Android < 4.0 (bugs 897924 & 888891");
    SimpleTest.finish();
  }
}
