var { MailServices } = ChromeUtils.import(
  "resource:///modules/MailServices.jsm"
);

// used by checkProgress to periodically check the progress of the import
var gGenericImportHelper;
/**
 * GenericImportHelper
 * The parent class of AbImportHelper, MailImportHelper, SettingsImportHelper
 *                     and FiltersImportHelper.
 *
 * @param aModuleType The type of import module. Should be addressbook or mail.
 * @param aModuleSearchString
 *                    The string to search the module names for, such as
 *                    "Text file" to find the import module for comma-separated
 *                    value, LDIF, and tab-delimited files.
 * @param aFile       An instance of nsIFile to import.
 *
 * @class
 * @class
 */
function GenericImportHelper(aModuleType, aModuleSearchString, aFile) {
  gGenericImportHelper = null;
  if (!["addressbook", "mail", "settings", "filters"].includes(aModuleType)) {
    do_throw("Unexpected type passed to the GenericImportHelper constructor");
  }
  this.mModuleType = aModuleType;
  this.mModuleSearchString = aModuleSearchString;
  this.mInterface = this._findInterface();
  Assert.ok(this.mInterface !== null);

  this.mFile = aFile; // checked in the beginImport method
}

GenericImportHelper.prototype = {
  interfaceType: Ci.nsIImportGeneric,
  /**
   * GenericImportHelper.beginImport
   * Imports the given address book export or mail data and invoke
   * checkProgress of child class to check the data,
   */
  beginImport() {
    Assert.ok(this.mFile instanceof Ci.nsIFile && this.mFile.exists());

    if (this.mModuleType == "addressbook") {
      this.mInterface.SetData("addressLocation", this.mFile);
    } else if (this.mModuleType == "mail") {
      this.mInterface.SetData("mailLocation", this.mFile);
    }

    Assert.ok(this.mInterface.WantsProgress());
    const error = Cc["@mozilla.org/supports-string;1"].createInstance(
      Ci.nsISupportsString
    );
    Assert.ok(this.mInterface.BeginImport(null, error));
    Assert.equal(error.data, "");
    do_test_pending();
    this.checkProgress();
  },
  /**
   * GenericImportHelper.getInterface
   *
   * @returns An nsIImportGeneric import interface.
   */
  getInterface() {
    return this.mInterface;
  },

  _findInterface() {
    var importService = Cc["@mozilla.org/import/import-service;1"].getService(
      Ci.nsIImportService
    );
    var count = importService.GetModuleCount(this.mModuleType);

    // Iterate through each import module until the one being searched for is
    // found and then return the ImportInterface of that module
    for (var i = 0; i < count; i++) {
      // Check if the current module fits the search string gets the interface
      if (
        importService
          .GetModuleName(this.mModuleType, i)
          .includes(this.mModuleSearchString)
      ) {
        return importService
          .GetModule(this.mModuleType, i)
          .GetImportInterface(this.mModuleType)
          .QueryInterface(this.interfaceType);
      }
    }
    return null; // it wasn't found
  },
  /**
   * GenericImportHelper.checkProgress
   * Checks the progress of an import every 200 milliseconds until it is
   * complete.  Checks the test results if there is an original address book,
   * otherwise evaluates the optional command, or calls do_test_finished().
   */
  checkProgress() {
    Assert.ok(
      this.mInterface && this.mInterface instanceof Ci.nsIImportGeneric
    );
    Assert.ok(this.mInterface.ContinueImport());
    // if the import isn't done, check again in 200 milliseconds.
    if (this.mInterface.GetProgress() != 100) {
      // use the helper object to check the progress of the import after 200 ms
      gGenericImportHelper = this;
      do_timeout(200, function () {
        gGenericImportHelper.checkProgress();
      });
    } else {
      // if it is done, check the results or finish the test.
      this.checkResults();
      do_test_finished();
    }
  },

  /**
   * GenericImportHelper.checkResults
   * Checks the results of the import.
   * Child class should implement this method.
   */
  checkResults() {},
};

/**
 * AbImportHelper
 * A helper for Address Book imports. To use, supply at least the file and type.
 * If you would like the results checked, add a new array in the addressbook
 * JSON file in the resources folder and supply aAbName and aJsonName.
 * See AB_README for more information.
 *
 * @param aFile     An instance of nsIAbFile to import.
 * @param aModuleSearchString
 *                  The string to search the module names for, such as
 *                  "Text file" to find the import module for comma-separated
 *                  value, LDIF, and tab-delimited files.
 * Optional parameters: Include if you would like the import checked.
 * @param aAbName   The name the address book will have (the filename without
 *                  the extension).
 * @param aJsonName The name of the array in addressbook.json with the cards
 *                  to compare with the imported cards.
 * @class
 * @class
 */
function AbImportHelper(aFile, aModuleSearchString, aAbName, aJsonName) {
  GenericImportHelper.call(this, "addressbook", aModuleSearchString, aFile);

  this.mAbName = aAbName;
  /* Attribute notes:  The attributes listed in the declaration below are
   * supported by all three text export/import types.
   * The following are not supported: anniversaryYear, anniversaryMonth,
   * anniversaryDay, popularityIndex, isMailList, mailListURI, lastModifiedDate.
   */
  var supportedAttributes = [
    "FirstName",
    "LastName",
    "DisplayName",
    "NickName",
    "PrimaryEmail",
    "SecondEmail",
    "WorkPhone",
    "HomePhone",
    "FaxNumber",
    "PagerNumber",
    "CellularNumber",
    "HomeAddress",
    "HomeAddress2",
    "HomeCity",
    "HomeState",
    "HomeZipCode",
    "HomeCountry",
    "WorkAddress",
    "WorkAddress2",
    "WorkCity",
    "WorkState",
    "WorkZipCode",
    "WorkCountry",
    "JobTitle",
    "Department",
    "Company",
    "BirthYear",
    "BirthMonth",
    "BirthDay",
    "WebPage1",
    "WebPage2",
    "Custom1",
    "Custom2",
    "Custom3",
    "Custom4",
    "Notes",
    "_AimScreenName",
    "_vCard",
  ];

  // get the extra attributes supported for the given type of import
  if (this.mFile.leafName.toLowerCase().endsWith(".ldif")) {
    this.mSupportedAttributes = supportedAttributes;
  } else if (this.mFile.leafName.toLowerCase().endsWith(".csv")) {
    this.mSupportedAttributes = supportedAttributes;
    this.setFieldMap(this.getDefaultFieldMap(true));
  } else if (this.mFile.leafName.toLowerCase().endsWith(".vcf")) {
    this.mSupportedAttributes = supportedAttributes;
  }

  // get the "cards" from the JSON file, if necessary
  if (aJsonName) {
    this.mJsonCards = this.getJsonCards(aJsonName);
  }
}

AbImportHelper.prototype = {
  __proto__: GenericImportHelper.prototype,
  /**
   * AbImportHelper.getDefaultFieldMap
   * Returns the default field map.
   *
   * @param aSkipFirstRecord True if the first record of the text file should
   *                         be skipped.
   * @returns A default field map.
   */
  getDefaultFieldMap(aSkipFirstRecord) {
    var importService = Cc["@mozilla.org/import/import-service;1"].getService(
      Ci.nsIImportService
    );
    var fieldMap = importService.CreateNewFieldMap();

    fieldMap.DefaultFieldMap(fieldMap.numMozFields);
    this.mInterface
      .GetData("addressInterface")
      .QueryInterface(Ci.nsIImportAddressBooks)
      .InitFieldMap(fieldMap);
    fieldMap.skipFirstRecord = aSkipFirstRecord;

    return fieldMap;
  },

  /**
   * AbImportHelper.setFieldMap
   * Set the field map.
   *
   * @param aFieldMap The field map used for address book import.
   */
  setFieldMap(aFieldMap) {
    this.mInterface.SetData("fieldMap", aFieldMap);
  },

  /**
   * AbImportHelper.setAddressLocation
   * Set the the location of the address book.
   *
   * @param aLocation The location of the source address book.
   */
  setAddressBookLocation(aLocation) {
    this.mInterface.SetData("addressLocation", aLocation);
  },

  /**
   * AbImportHelper.setAddressDestination
   * Set the the destination of the address book.
   *
   * @param aDestination   URI of destination address book or null if
   *                       new address books will be created.
   */
  setAddressDestination(aDestination) {
    this.mInterface.SetData("addressDestination", aDestination);
  },

  /**
   * AbImportHelper.checkResults
   * Checks the results of the import.
   * Ensures the an address book was created, then compares the supported
   * attributes of each card with the card(s) in the JSON array.
   * Calls do_test_finished() when done
   */
  checkResults() {
    if (!this.mJsonCards) {
      do_throw("The address book must be setup before checking results");
    }
    // When do_test_pending() was called and there is an error the test hangs.
    // This try/catch block will catch any errors and call do_throw() with the
    // error to throw the error and avoid the hang.
    try {
      // make sure an address book was created
      var newAb = this.getAbByName(this.mAbName);
      Assert.ok(newAb !== null);
      Assert.ok(newAb.QueryInterface(Ci.nsIAbDirectory));
      // get the imported card(s) and check each one
      var count = 0;
      for (let importedCard of newAb.childCards) {
        this.compareCards(this.mJsonCards[count], importedCard);
        count++;
      }
      // make sure there are the same number of cards in the address book and
      // the JSON array
      Assert.equal(count, this.mJsonCards.length);
      do_test_finished();
    } catch (e) {
      do_throw(e);
    }
  },
  /**
   * AbImportHelper.getAbByName
   * Returns the Address Book (if any) with the given name.
   *
   * @param aName The name of the Address Book to find.
   * @returns An nsIAbDirectory, if found.
   *         null if the requested Address Book could not be found.
   */
  getAbByName(aName) {
    Assert.ok(aName && aName.length > 0);

    for (let data of MailServices.ab.directories) {
      if (data.dirName == aName) {
        return data;
      }
    }
    return null;
  },
  /**
   * AbImportHelper.compareCards
   * Compares a JSON "card" with an imported card and throws an error if the
   * values of a supported attribute are different.
   *
   * @param aJsonCard The object decoded from addressbook.json.
   * @param aCard     The imported card to compare with.
   */
  compareCards(aJsonCard, aCard) {
    for (let [key, value] of Object.entries(aJsonCard)) {
      if (!this.mSupportedAttributes.includes(key)) {
        continue;
      }
      if (key == "_vCard") {
        equal(
          aCard
            .getProperty(key, "")
            .replace(
              /UID:[a-f0-9-]{36}/i,
              "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
            ),
          `BEGIN:VCARD\r\n${value.join("\r\n")}\r\nEND:VCARD\r\n`,
          "_vCard should be correct"
        );
      } else {
        equal(aCard.getProperty(key, ""), value, `${key} should be correct`);
      }
    }
  },
  /**
   * AbImportHelper.getJsonCards
   * Gets an array of "cards" from the JSON file addressbook.json located in the
   * mailnews/import/test/resources folder.  The array should contain objects
   * with the expected properties and values of the cards in the imported
   * address book.
   * See addressbook.json for an example and AB_README for more details.
   *
   * @param aName The name of the array in addressbook.json.
   * @returns An array of "cards".
   */
  getJsonCards(aName) {
    if (!aName) {
      do_throw("Error - getJSONAb requires an address book name");
    }
    var file = do_get_file("resources/addressbook.json");
    if (!file || !file.exists() || !file.isFile()) {
      do_throw("Unable to get JSON file");
    }

    var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
      Ci.nsIFileInputStream
    );
    fis.init(file, 0x01, 0o444, 0);
    var istream = Cc[
      "@mozilla.org/intl/converter-input-stream;1"
    ].createInstance(Ci.nsIConverterInputStream);
    var replacementChar =
      Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER;
    istream.init(fis, "UTF-8", 1024, replacementChar);
    var json = "";
    var str = {};
    // get the entire file into the json string
    while (istream.readString(4096, str) != 0) {
      json += str.value;
    }
    // close the input streams
    istream.close();
    fis.close();
    // decode the JSON and get the array of cards
    var arr = JSON.parse(json)[aName];
    Assert.ok(arr && arr.length > 0);
    return arr;
  },

  setSupportedAttributes(attributes) {
    this.mSupportedAttributes = attributes;
  },
};

/**
 * MailImportHelper
 * A helper for mail imports.
 *
 * @param aFile      An instance of nsIFile to import.
 * @param aModuleSearchString
 *                   The string to search the module names for, such as
 *                   "Outlook Express", etc.
 * @param aExpected  An instance of nsIFile to compare with the imported
 *                   folders.
 *
 * @class
 * @class
 */
function MailImportHelper(aFile, aModuleSearchString, aExpected) {
  GenericImportHelper.call(this, "mail", aModuleSearchString, aFile);
  this.mExpected = aExpected;
}

MailImportHelper.prototype = {
  __proto__: GenericImportHelper.prototype,
  interfaceType: Ci.nsIImportGeneric,
  _checkEqualFolder(expectedFolder, actualFolder) {
    Assert.equal(expectedFolder.leafName, actualFolder.name);

    let expectedSubFolders = [];
    for (let entry of expectedFolder.directoryEntries) {
      if (entry.isDirectory()) {
        expectedSubFolders.push(entry);
      }
    }
    let actualSubFolders = actualFolder.subFolders;
    Assert.equal(expectedSubFolders.length, actualSubFolders.length);
    for (let i = 0; i < expectedSubFolders.length; i++) {
      this._checkEqualFolder(expectedSubFolders[i], actualSubFolders[i]);
    }
  },

  checkResults() {
    let rootFolder = MailServices.accounts.localFoldersServer.rootFolder;
    Assert.ok(rootFolder.containsChildNamed(this.mFile.leafName));
    let importedFolder = rootFolder.getChildNamed(this.mFile.leafName);
    Assert.notEqual(importedFolder, null);

    this._checkEqualFolder(this.mExpected, importedFolder);
  },
};

/**
 * SettingsImportHelper
 * A helper for settings imports.
 *
 * @param aFile      An instance of nsIFile to import, can be null.
 * @param aModuleSearchString
 *                   The string to search the module names for, such as
 *                   "Outlook Express", etc.
 * @param aExpected  An array of object which has incomingServer, identity
 *                   and smtpSever to compare with imported nsIMsgAccount.
 *
 * @class
 * @class
 */
function SettingsImportHelper(aFile, aModuleSearchString, aExpected) {
  GenericImportHelper.call(this, "settings", aModuleSearchString, aFile);
  this.mExpected = aExpected;
}

SettingsImportHelper.prototype = {
  __proto__: GenericImportHelper.prototype,
  interfaceType: Ci.nsIImportSettings,
  /**
   * SettingsImportHelper.beginImport
   * Imports settings from a specific file or auto-located if the file is null,
   * and compare the import results with the expected array.
   */
  beginImport() {
    this._ensureNoAccounts();
    if (this.mFile) {
      this.mInterface.SetLocation(this.mFile);
    } else {
      Assert.equal(true, this.mInterface.AutoLocate({}, {}));
    }
    Assert.equal(true, this.mInterface.Import({}));
    this.checkResults();
  },

  _ensureNoAccounts() {
    for (let account of MailServices.accounts.accounts) {
      MailServices.accounts.removeAccount(account);
    }
  },

  _checkSmtpServer(expected, actual) {
    Assert.equal(expected.port, actual.port);
    Assert.equal(expected.username, actual.username);
    Assert.equal(expected.authMethod, actual.authMethod);
    Assert.equal(expected.socketType, actual.socketType);
  },

  _checkIdentity(expected, actual) {
    Assert.equal(expected.fullName, actual.fullName);
    Assert.equal(expected.email, actual.email);
    Assert.equal(expected.replyTo, actual.replyTo);
    Assert.equal(expected.organization, actual.organization);
  },

  _checkPop3IncomingServer(expected, actual) {
    Assert.equal(expected.leaveMessagesOnServer, actual.leaveMessagesOnServer);
    Assert.equal(
      expected.deleteMailLeftOnServer,
      actual.deleteMailLeftOnServer
    );
    Assert.equal(expected.deleteByAgeFromServer, actual.deleteByAgeFromServer);
    Assert.equal(
      expected.numDaysToLeaveOnServer,
      actual.numDaysToLeaveOnServer
    );
  },

  _checkIncomingServer(expected, actual) {
    Assert.equal(expected.type, actual.type);
    Assert.equal(expected.port, actual.port);
    Assert.equal(expected.username, actual.username);
    Assert.equal(expected.isSecure, actual.isSecure);
    Assert.equal(expected.hostName, actual.hostName);
    Assert.equal(expected.prettyName, actual.prettyName);
    Assert.equal(expected.authMethod, actual.authMethod);
    Assert.equal(expected.socketType, actual.socketType);
    Assert.equal(expected.doBiff, actual.doBiff);
    Assert.equal(expected.biffMinutes, actual.biffMinutes);

    if (expected.type == "pop3") {
      this._checkPop3IncomingServer(
        expected,
        actual.QueryInterface(Ci.nsIPop3IncomingServer)
      );
    }
  },

  _checkAccount(expected, actual) {
    this._checkIncomingServer(expected.incomingServer, actual.incomingServer);

    Assert.equal(1, actual.identities.length);
    let actualIdentity = actual.identities[0];
    this._checkIdentity(expected.identity, actualIdentity);

    if (expected.incomingServer.type != "nntp") {
      let actualSmtpServer = MailServices.smtp.getServerByKey(
        actualIdentity.smtpServerKey
      );
      this._checkSmtpServer(expected.smtpServer, actualSmtpServer);
    }
  },

  _isLocalMailAccount(account) {
    return (
      account.incomingServer.type == "none" &&
      account.incomingServer.username == "nobody" &&
      account.incomingServer.hostName == "Local Folders"
    );
  },

  _findExpectedAccount(account) {
    return this.mExpected.filter(function (expectedAccount) {
      return (
        expectedAccount.incomingServer.type == account.incomingServer.type &&
        expectedAccount.incomingServer.username ==
          account.incomingServer.username &&
        expectedAccount.incomingServer.hostName ==
          account.incomingServer.hostName
      );
    });
  },

  checkResults() {
    for (let actualAccount of MailServices.accounts.accounts) {
      if (this._isLocalMailAccount(actualAccount)) {
        continue;
      }
      let expectedAccounts = this._findExpectedAccount(actualAccount);
      Assert.notEqual(null, expectedAccounts);
      Assert.equal(1, expectedAccounts.length);
      this._checkAccount(expectedAccounts[0], actualAccount);
    }
  },
};

/**
 * FiltersImportHelper
 * A helper for filter imports.
 *
 * @param aFile      An instance of nsIFile to import.
 * @param aModuleSearchString
 *                   The string to search the module names for, such as
 *                   "Outlook Express", etc.
 * @param aExpected  The number of filters that should exist after import.
 *
 * @class
 * @class
 */
function FiltersImportHelper(aFile, aModuleSearchString, aExpected) {
  GenericImportHelper.call(this, "filters", aModuleSearchString, aFile);
  this.mExpected = aExpected;
}

FiltersImportHelper.prototype = {
  __proto__: GenericImportHelper.prototype,
  interfaceType: Ci.nsIImportFilters,

  /**
   * FiltersImportHelper.beginImport
   * Imports filters from a specific file/folder or auto-located if the file is null,
   * and compare the import results with the expected array.
   */
  beginImport() {
    if (this.mFile) {
      this.mInterface.SetLocation(this.mFile);
    } else {
      Assert.equal(true, this.mInterface.AutoLocate({}, {}));
    }
    Assert.equal(true, this.mInterface.Import({}));
    this.checkResults();
  },

  _loopOverFilters(aFilterList, aCondition) {
    let result = 0;
    for (let i = 0; i < aFilterList.filterCount; i++) {
      let filter = aFilterList.getFilterAt(i);
      if (aCondition(filter)) {
        result++;
      }
    }
    return result;
  },

  checkResults() {
    let expected = this.mExpected;
    let server = MailServices.accounts.localFoldersServer;
    let filterList = server.getFilterList(null);
    if ("count" in expected) {
      Assert.equal(filterList.filterCount, expected.count);
    }
    if ("enabled" in expected) {
      Assert.equal(
        this._loopOverFilters(filterList, f => f.enabled),
        expected.enabled
      );
    }
    if ("incoming" in expected) {
      Assert.equal(
        this._loopOverFilters(
          filterList,
          f => f.filterType & Ci.nsMsgFilterType.InboxRule
        ),
        expected.incoming
      );
    }
    if ("outgoing" in expected) {
      Assert.equal(
        this._loopOverFilters(
          filterList,
          f => f.filterType & Ci.nsMsgFilterType.PostOutgoing
        ),
        expected.outgoing
      );
    }
  },
};
