/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

/*
 * Test initializing from the search settings.
 */

"use strict";

const legacyUseSavedOrderPrefName =
  SearchUtils.BROWSER_SEARCH_PREF + "useDBForOrder";

var settingsTemplate;

/**
 * Test reading from search.json.mozlz4
 */
add_task(async function setup() {
  Services.prefs
    .getDefaultBranch(SearchUtils.BROWSER_SEARCH_PREF + "param.")
    .setCharPref("test", "expected");

  await SearchTestUtils.useTestEngines("data1");
  await AddonTestUtils.promiseStartupManager();
  await Services.search.init();
});

async function loadSettingsFile(settingsFile, setVersion, setHashes) {
  settingsTemplate = await readJSONFile(do_get_file(settingsFile));
  if (setVersion) {
    settingsTemplate.version = SearchUtils.SETTINGS_VERSION;
  }

  if (setHashes) {
    settingsTemplate.metaData.hash = SearchUtils.getVerificationHash(
      settingsTemplate.metaData.current
    );
    settingsTemplate.metaData.privateHash = SearchUtils.getVerificationHash(
      settingsTemplate.metaData.private
    );
  }

  delete settingsTemplate.visibleDefaultEngines;

  await promiseSaveSettingsData(settingsTemplate);
}

/**
 * Start the search service and confirm the engine properties match the expected values.
 *
 * @param {string} settingsFile
 *   The path to the settings file to use.
 * @param {boolean} setVersion
 *   True if to set the version in the copied settings file.
 * @param {boolean} expectedUseDBValue
 *   The value expected for the `useSavedOrder` metadata attribute.
 */
async function checkLoadSettingProperties(
  settingsFile,
  setVersion,
  expectedUseDBValue
) {
  info("init search service");
  let ss = Services.search.wrappedJSObject;

  await loadSettingsFile(settingsFile, setVersion);

  const settingsFileWritten = promiseAfterSettings();

  await ss.reset();
  await Services.search.init();

  await settingsFileWritten;

  let engines = await ss.getEngines();

  Assert.equal(
    engines[0].name,
    "engine1",
    "Should have loaded the correct first engine"
  );
  Assert.equal(engines[0].alias, "testAlias", "Should have set the alias");
  Assert.equal(engines[0].hidden, false, "Should have not hidden the engine");
  Assert.equal(engines[0].id, "engine1@search.mozilla.orgdefault");

  Assert.equal(
    engines[1].name,
    "engine2",
    "Should have loaded the correct second engine"
  );
  Assert.equal(engines[1].alias, "", "Should have not set the alias");
  Assert.equal(engines[1].hidden, true, "Should have hidden the engine");
  Assert.equal(engines[1].id, "engine2@search.mozilla.orgdefault");

  // The extra engine is the second in the list.
  isSubObjectOf(EXPECTED_ENGINE.engine, engines[2]);
  Assert.ok(engines[2].id, "test-addon-id@mozilla.orgdefault");

  let engineFromSS = ss.getEngineByName(EXPECTED_ENGINE.engine.name);
  Assert.ok(!!engineFromSS);
  isSubObjectOf(EXPECTED_ENGINE.engine, engineFromSS);

  Assert.equal(
    engineFromSS.getSubmission("foo").uri.spec,
    "http://www.google.com/search?q=foo",
    "Should have the correct URL with no mozparams"
  );

  Assert.equal(
    ss._settings.getMetaDataAttribute("useSavedOrder"),
    expectedUseDBValue,
    "Should have set the useSavedOrder metadata correctly."
  );

  let migratedSettingsFile = await promiseSettingsData();

  Assert.equal(
    migratedSettingsFile.engines[0].id,
    "engine1@search.mozilla.orgdefault"
  );

  removeSettingsFile();
}

add_task(async function test_legacy_setting_engine_properties() {
  Services.prefs.setBoolPref(legacyUseSavedOrderPrefName, true);

  let legacySettings = await readJSONFile(
    do_get_file("data/search-legacy.json")
  );

  // Assert the engine ids have not been migrated yet
  for (let engine of legacySettings.engines) {
    Assert.ok(!("id" in engine));
  }
  Assert.ok(!("defaultEngineId" in legacySettings.metaData));
  Assert.ok(!("privateDefaultEngineId" in legacySettings.metaData));

  await checkLoadSettingProperties("data/search-legacy.json", false, true);

  Assert.ok(
    !Services.prefs.prefHasUserValue(legacyUseSavedOrderPrefName),
    "Should have cleared the legacy pref."
  );
});

add_task(
  async function test_legacy_setting_migration_with_undefined_metaData_current_and_private() {
    let ss = Services.search.wrappedJSObject;

    await loadSettingsFile("data/search-legacy.json", false);
    const settingsFileWritten = promiseAfterSettings();

    await ss.reset();
    await Services.search.init();

    await settingsFileWritten;

    let migratedSettingsFile = await promiseSettingsData();

    Assert.equal(
      migratedSettingsFile.metaData.defaultEngineId,
      "",
      "When there is no metaData.current attribute in settings file, the migration should set the defaultEngineId to an empty string."
    );
    Assert.equal(
      migratedSettingsFile.metaData.privateDefaultEngineId,
      "",
      "When there is no metaData.private attribute in settings file, the migration should set the privateDefaultEngineId to an empty string."
    );

    removeSettingsFile();
  }
);

add_task(
  async function test_legacy_setting_migration_with_correct_metaData_current_and_private_hashes() {
    let ss = Services.search.wrappedJSObject;

    await loadSettingsFile(
      "data/search-legacy-correct-default-engine-hashes.json",
      false,
      true
    );
    const settingsFileWritten = promiseAfterSettings();

    await ss.reset();
    await Services.search.init();

    await settingsFileWritten;

    let migratedSettingsFile = await promiseSettingsData();

    Assert.equal(
      migratedSettingsFile.metaData.defaultEngineId,
      "engine2@search.mozilla.orgdefault",
      "When the metaData.current and associated hash are correct, the migration should set the defaultEngineId to the engine id."
    );
    Assert.equal(
      migratedSettingsFile.metaData.privateDefaultEngineId,
      "engine2@search.mozilla.orgdefault",
      "When the metaData.private and associated hash are correct, the migration should set the privateDefaultEngineId to the private engine id."
    );

    removeSettingsFile();
  }
);

add_task(
  async function test_legacy_setting_migration_with_incorrect_metaData_current_and_private_hashes_app_provided() {
    let ss = Services.search.wrappedJSObject;

    // Here we are testing correct migration for the case that a user has set
    // their default engine to an application provided engine (but not the app
    // default).
    //
    // In this case we should ignore invalid hashes for the default engines,
    // and allow the select default to remain. This covers the case where
    // a user has copied a profile from a different directory.
    // See SearchService._getEngineDefault for more details.

    await loadSettingsFile(
      "data/search-legacy-wrong-default-engine-hashes.json",
      false,
      false
    );
    const settingsFileWritten = promiseAfterSettings();

    await ss.reset();
    await Services.search.init();

    await settingsFileWritten;

    let migratedSettingsFile = await promiseSettingsData();

    Assert.equal(
      migratedSettingsFile.metaData.defaultEngineId,
      "engine2@search.mozilla.orgdefault",
      "Should ignore invalid metaData.hash when the default engine is application provided."
    );
    Assert.equal(
      Services.search.defaultEngine.name,
      "engine2",
      "Should have the correct engine set as default"
    );

    Assert.equal(
      migratedSettingsFile.metaData.privateDefaultEngineId,
      "engine2@search.mozilla.orgdefault",
      "Should ignore invalid metaData.privateHash when the default private engine is application provided."
    );
    Assert.equal(
      Services.search.defaultPrivateEngine.name,
      "engine2",
      "Should have the correct engine set as default private"
    );

    removeSettingsFile();
  }
);

add_task(
  async function test_legacy_setting_migration_with_incorrect_metaData_current_and_private_hashes_third_party() {
    let ss = Services.search.wrappedJSObject;

    // This test is checking that if the user has set a third-party engine as
    // default, and the verification hash is invalid, then we do not copy
    // the default engine setting.

    await loadSettingsFile(
      "data/search-legacy-wrong-third-party-engine-hashes.json",
      false,
      false
    );
    const settingsFileWritten = promiseAfterSettings();

    await ss.reset();
    await Services.search.init();

    await settingsFileWritten;

    let migratedSettingsFile = await promiseSettingsData();

    Assert.equal(
      migratedSettingsFile.metaData.defaultEngineId,
      "",
      "Should reset the default engine when metaData.hash is invalid and the engine is not application provided."
    );
    Assert.equal(
      Services.search.defaultEngine.name,
      "engine1",
      "Should have reset the default engine"
    );

    Assert.equal(
      migratedSettingsFile.metaData.privateDefaultEngineId,
      "",
      "Should reset the default engine when metaData.privateHash is invalid and the engine is not application provided."
    );
    Assert.equal(
      Services.search.defaultPrivateEngine.name,
      "engine1",
      "Should have reset the default private engine"
    );

    removeSettingsFile();
  }
);

add_task(async function test_current_setting_engine_properties() {
  await checkLoadSettingProperties("data/search.json", true, false);
});

add_task(async function test_settings_metadata_properties() {
  let ss = Services.search.wrappedJSObject;

  await loadSettingsFile("data/search.json");

  const settingsFileWritten = promiseAfterSettings();
  await ss.reset();
  await Services.search.init();

  await settingsFileWritten;

  let metaDataProperties = [
    "locale",
    "region",
    "channel",
    "experiment",
    "distroID",
  ];

  for (let name of metaDataProperties) {
    Assert.notEqual(
      ss._settings.getMetaDataAttribute(`${name}`),
      undefined,
      `Search settings should have ${name} property defined.`
    );
  }

  removeSettingsFile();
});

add_task(async function test_settings_write_when_settings_changed() {
  let ss = Services.search.wrappedJSObject;
  await loadSettingsFile("data/search.json");

  const settingsFileWritten = promiseAfterSettings();
  await ss.reset();
  await Services.search.init();
  await settingsFileWritten;

  Assert.ok(
    ss._settings.isCurrentAndCachedSettingsEqual(),
    "Settings and cached settings should be the same after search service initializaiton."
  );

  const settingsFileWritten2 = promiseAfterSettings();
  ss._settings.setMetaDataAttribute("value", "test");

  Assert.ok(
    !ss._settings.isCurrentAndCachedSettingsEqual(),
    "Settings should differ from cached settings after a new attribute is set."
  );

  await settingsFileWritten2;
  info("Settings write complete");

  Assert.ok(
    ss._settings.isCurrentAndCachedSettingsEqual(),
    "Settings and cached settings should be the same after new attribte on settings is written."
  );

  removeSettingsFile();
});

add_task(async function test_set_and_get_engine_metadata_attribute() {
  let ss = Services.search.wrappedJSObject;
  await loadSettingsFile("data/search.json");

  const settingsFileWritten = promiseAfterSettings();
  await ss.reset();
  await Services.search.init();
  await settingsFileWritten;

  let engines = await ss.getEngines();
  const settingsFileWritten2 = promiseAfterSettings();
  ss._settings.setEngineMetaDataAttribute(engines[0].name, "value", "test");
  await settingsFileWritten2;

  Assert.equal(
    "test",
    ss._settings.getEngineMetaDataAttribute(engines[0].name, "value"),
    `${engines[0].name}'s metadata property "value" should be set as "test" after calling getEngineMetaDataAttribute.`
  );

  let userSettings = await ss._settings.get();
  let engine = userSettings.engines.find(e => e._name == engines[0].name);

  Assert.equal(
    "test",
    engine._metaData.value,
    `${engines[0].name}'s metadata property "value" should be set as "test" from settings file.`
  );

  removeSettingsFile();
});

add_task(
  async function test_settings_write_prevented_when_settings_unchanged() {
    let ss = Services.search.wrappedJSObject;
    await loadSettingsFile("data/search.json");

    const settingsFileWritten = promiseAfterSettings();
    await ss.reset();
    await Services.search.init();
    await settingsFileWritten;

    Assert.ok(
      ss._settings.isCurrentAndCachedSettingsEqual(),
      "Settings and cached settings should be the same after search service initializaiton."
    );

    // Update settings.
    const settingsFileWritten2 = promiseAfterSettings();
    ss._settings.setMetaDataAttribute("value", "test");

    Assert.ok(
      !ss._settings.isCurrentAndCachedSettingsEqual(),
      "Settings should differ from cached settings after a new attribute is set."
    );
    await settingsFileWritten2;

    // Set the same attribute as before to ensure there was no change.
    // Settings write should be prevented.
    let promiseWritePrevented = SearchTestUtils.promiseSearchNotification(
      "write-prevented-when-settings-unchanged"
    );
    ss._settings.setMetaDataAttribute("value", "test");

    Assert.ok(
      ss._settings.isCurrentAndCachedSettingsEqual(),
      "Settings and cached settings should be the same."
    );
    await promiseWritePrevented;

    removeSettingsFile();
  }
);

/**
 * Test that the JSON settings written in the profile is correct.
 */
add_task(async function test_settings_write() {
  let ss = Services.search.wrappedJSObject;
  info("test settings writing");

  await loadSettingsFile("data/search.json");

  const settingsFileWritten = promiseAfterSettings();
  await ss.reset();
  await Services.search.init();
  await settingsFileWritten;

  let settingsData = await promiseSettingsData();

  // Remove buildID and locale, as they are no longer used.
  delete settingsTemplate.buildID;
  delete settingsTemplate.locale;

  for (let engine of settingsTemplate.engines) {
    // Remove _shortName from the settings template, as it is no longer supported,
    // but older settings used to have it, so we keep it in the template as an
    // example.
    if ("_shortName" in engine) {
      delete engine._shortName;
    }
    if ("_urls" in engine) {
      // Only app-provided engines support purpose & mozparams, others do not,
      // so filter them out of the expected template.
      for (let urls of engine._urls) {
        urls.params = urls.params.filter(p => !p.purpose && !p.mozparam);
        // resultDomain is also no longer supported.
        if ("resultDomain" in urls) {
          delete urls.resultDomain;
        }
      }
    }
    // Remove queryCharset, if it is the same as the default, as we don't save
    // it in that case.
    if (engine?.queryCharset == SearchUtils.DEFAULT_QUERY_CHARSET) {
      delete engine.queryCharset;
    }
  }

  // Note: the file is copied with an old version number, which should have
  // been updated on write.
  settingsTemplate.version = SearchUtils.SETTINGS_VERSION;

  isSubObjectOf(settingsTemplate, settingsData, (prop, value) => {
    if (prop != "_iconURL" && prop != "{}") {
      return false;
    }
    // Skip items that are to do with icons for extensions, as we can't
    // control the uuid.
    return value.startsWith("moz-extension://");
  });
});

async function settings_write_check(disableFn) {
  let ss = Services.search.wrappedJSObject;

  sinon.stub(ss._settings, "_write").returns(Promise.resolve());

  // Simulate the search service being initialized.
  disableFn(true);

  ss._settings.setMetaDataAttribute("value", "test");

  Assert.ok(
    ss._settings._write.notCalled,
    "Should not have attempted to _write"
  );

  // Wait for two periods of the normal delay to ensure we still do not write.
  await new Promise(r =>
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    setTimeout(r, SearchSettings.SETTNGS_INVALIDATION_DELAY * 2)
  );

  Assert.ok(
    ss._settings._write.notCalled,
    "Should not have attempted to _write"
  );

  disableFn(false);

  await TestUtils.waitForCondition(
    () => ss._settings._write.calledOnce,
    "Should attempt to write the settings."
  );

  sinon.restore();
}

add_task(async function test_settings_write_prevented_during_init() {
  await settings_write_check(disable => {
    let status = disable ? "success" : "failed";
    Services.search.wrappedJSObject.forceInitializationStatusForTests(status);
  });
});

add_task(async function test_settings_write_prevented_during_reload() {
  await settings_write_check(
    disable => (Services.search.wrappedJSObject._reloadingEngines = disable)
  );
});

var EXPECTED_ENGINE = {
  engine: {
    name: "Test search engine",
    alias: "",
    description: "A test search engine (based on Google search)",
    searchForm: "http://www.google.com/",
    wrappedJSObject: {
      _extensionID: "test-addon-id@mozilla.org",
      _iconURL:
        "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAA" +
        "AIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9F" +
        "sfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2F" +
        "Ptft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2Fgg" +
        "M%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F" +
        "%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJ" +
        "vvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%" +
        "2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%" +
        "2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%" +
        "2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%" +
        "2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYS" +
        "BHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWc" +
        "TxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4j" +
        "wA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsgg" +
        "A7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7" +
        "kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%" +
        "2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFE" +
        "MwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%" +
        "2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCT" +
        "IYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesA" +
        "AN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOc" +
        "AAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v" +
        "8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAA" +
        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
      _urls: [
        {
          type: "application/x-suggestions+json",
          method: "GET",
          template:
            "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox" +
            "&hl={moz:locale}&q={searchTerms}",
          params: "",
        },
        {
          type: "text/html",
          method: "GET",
          template: "http://www.google.com/search",
          params: [
            {
              name: "q",
              value: "{searchTerms}",
              purpose: undefined,
            },
          ],
        },
      ],
    },
  },
};
