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

/**
 * Tests to ensure that we correctly switch and update engines when
 * adding and removing application provided engines which overlap
 * with engines in the override allow list.
 */

"use strict";

const SEARCH_URL_BASE = "https://example.com/";
const SEARCH_URL_PARAMS = `?sourceId=enterprise&q={searchTerms}`;
const ENGINE_NAME = "Simple Engine";
const ALIAS = "star";

const ALLOWLIST = [
  {
    thirdPartyId: "simpleengine@tests.mozilla.org",
    overridesAppIdv2: "simple",
    urls: [
      { search_url: SEARCH_URL_BASE, search_url_get_params: SEARCH_URL_PARAMS },
    ],
  },
  {
    thirdPartyId: "opensearch@search.mozilla.org",
    engineName: ENGINE_NAME,
    overridesAppIdv2: "simple",
    urls: [
      { search_url: SEARCH_URL_BASE, search_url_get_params: SEARCH_URL_PARAMS },
    ],
  },
];

function createConfig(simpleEngineEnvironment) {
  return [
    {
      recordType: "engine",
      identifier: "basic",
      base: {
        name: "basic",
        urls: {
          search: {
            base: "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB",
            params: [
              {
                name: "sourceId",
                value: "Mozilla-search",
              },
            ],
            searchTermParamName: "search",
          },
        },
      },
      variants: [
        {
          environment: { allRegionsAndLocales: true },
        },
      ],
    },
    {
      recordType: "engine",
      identifier: "simple",
      base: {
        name: "Simple Engine",
        urls: {
          search: {
            base: "https://example.com",
            params: [
              {
                name: "sourceId",
                value: "Mozilla-search",
              },
            ],
            searchTermParamName: "search",
          },
        },
      },
      variants: [
        {
          environment: simpleEngineEnvironment,
        },
      ],
    },
    {
      recordType: "defaultEngines",
      globalDefault: "basic",
      specificDefaults: [],
    },
    {
      recordType: "engineOrders",
      orders: [],
    },
  ];
}

const CONFIG_SIMPLE_LOCALE_DE_V2 = createConfig({ locales: ["de"] });
const CONFIG_SIMPLE_EVERYWHERE_V2 = createConfig({
  allRegionsAndLocales: true,
});

let extension;
let notificationBoxStub;

add_setup(async function () {
  let server = useHttpServer();
  server.registerContentType("sjs", "sjs");
  Services.locale.availableLocales = [
    ...Services.locale.availableLocales,
    "en",
    "de",
  ];
  Services.locale.requestedLocales = ["en"];

  await Services.search.init();

  const settings = await RemoteSettings(SearchUtils.SETTINGS_ALLOWLIST_KEY);
  sinon.stub(settings, "get").returns(ALLOWLIST);

  notificationBoxStub = sinon.stub(
    Services.search.wrappedJSObject,
    "_showRemovalOfSearchEngineNotificationBox"
  );

  consoleAllowList.push("Failed to load");

  registerCleanupFunction(async () => {
    sinon.restore();
  });
});

/**
 * Tests that overrides are correctly applied when the deployment of the app
 * provided engine is extended into an area, or removed from an area, where a
 * user has the WebExtension installed and set as default.
 */
add_task(async function test_app_provided_engine_deployment_extended() {
  await runTestScenario({
    testOpenSearch: false,
    extend: async () => {
      info("Change configuration to include engine in user's environment");

      await SearchTestUtils.updateRemoteSettingsConfig(
        CONFIG_SIMPLE_EVERYWHERE_V2
      );
    },
    remove: async () => {
      info("Change configuration to remove engine from user's environment");

      await SearchTestUtils.updateRemoteSettingsConfig(
        CONFIG_SIMPLE_LOCALE_DE_V2
      );
    },
  });
});

/**
 * Tests that overrides are correctly applied when the deployment of the app
 * provided engine is extended into an area, or removed from an area, where a
 * user has the OpenSearch engine installed and set as default.
 */
add_task(
  async function test_app_provided_engine_deployment_extended_opensearch() {
    await runTestScenario({
      testOpenSearch: true,
      extend: async () => {
        info("Change configuration to include engine in user's environment");

        await SearchTestUtils.updateRemoteSettingsConfig(
          CONFIG_SIMPLE_EVERYWHERE_V2
        );
      },
      remove: async () => {
        info("Change configuration to remove engine from user's environment");

        await SearchTestUtils.updateRemoteSettingsConfig(
          CONFIG_SIMPLE_LOCALE_DE_V2
        );
      },
    });
  }
);

add_task(
  async function test_app_provided_engine_deployment_extended_restart_only() {
    await runTestScenario({
      testOpenSearch: false,
      extend: async () => {
        info(
          "Change configuration with restart to include engine in user's environment"
        );

        await SearchTestUtils.setRemoteSettingsConfig(
          CONFIG_SIMPLE_EVERYWHERE_V2
        );
        await restartSearchService(true);
      },
      remove: async () => {
        info(
          "Change configuration with restart to remove engine from user's environment"
        );

        await SearchTestUtils.setRemoteSettingsConfig(
          CONFIG_SIMPLE_LOCALE_DE_V2
        );
        await restartSearchService(true);
        // Ensure settings have been saved before the engines are added, so that
        // we know we won't have race conditions when `addEnginesFromExtension`
        // loads the settings itself.
        await promiseAfterSettings();

        // Simulate the add-on manager starting up and telling the
        // search service about the add-on again.
        await simulateExtensionStartup(false);
      },
    });
  }
);

add_task(
  async function test_app_provided_engine_deployment_extended_restart_only_startup_extension() {
    await runTestScenario({
      testOpenSearch: false,
      extend: async () => {
        info(
          "Change configuration with restart to include engine in user's environment"
        );

        SearchTestUtils.setRemoteSettingsConfig(CONFIG_SIMPLE_EVERYWHERE_V2);
        await restartSearchService(true);
      },
      remove: async () => {
        info(
          "Change configuration with restart to remove engine from user's environment"
        );

        SearchTestUtils.setRemoteSettingsConfig(CONFIG_SIMPLE_LOCALE_DE_V2);
        await restartSearchService(false);
        // Simulate the add-on manager starting up and telling the
        // search service about the add-on again.
        //
        // In this test, it does this before init() is called, to
        // simulate this being a startup extension.
        await simulateExtensionStartup(false);

        await Services.search.init();
      },
    });
  }
);

add_task(
  async function test_app_provided_engine_deployment_extended_opensearch_restart_only() {
    await runTestScenario({
      testOpenSearch: true,
      extend: async () => {
        info(
          "Change configuration with restart to include engine in user's enonment"
        );

        SearchTestUtils.setRemoteSettingsConfig(CONFIG_SIMPLE_EVERYWHERE_V2);
        await restartSearchService(true);
      },
      remove: async () => {
        info("Change configuration with restart to remove engine from user's");

        SearchTestUtils.setRemoteSettingsConfig(CONFIG_SIMPLE_LOCALE_DE_V2);
        await restartSearchService(true);
      },
    });
  }
);

/**
 * Tests that overrides are correctly applied when the user's environment changes
 * e.g. they have the WebExtension installed and change to a locale where the
 * application provided engine is (or is not) available.
 */
add_task(async function test_user_environment_changes() {
  await runTestScenario({
    testOpenSearch: false,
    extend: async () => {
      info("Change locale to de");

      await promiseSetLocale("de");
    },
    remove: async () => {
      info("Change locale to en");

      await promiseSetLocale("en");
    },
  });
});

async function runTestScenario({ extend, remove, testOpenSearch = false }) {
  // Setup the test.
  await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_SIMPLE_LOCALE_DE_V2);
  notificationBoxStub.resetHistory();

  info(
    `Install ${
      testOpenSearch ? "OpenSearch" : "WebExtension"
    } based engine and set as default`
  );

  let result = await installThirdPartyEngineAsDefault(testOpenSearch);
  let engine = result.engine;
  // Overwrite the global value of extension.
  extension = result.extension;

  // Set a user defined alias.
  engine.alias = ALIAS;

  // Assert the pre-conditions.
  await assertEngineCorrectlySet({
    expectedId: engine.id,
    expectedAlias: ALIAS,
    appEngineOverriden: false,
  });

  // Make changes and assert them.
  await assertCorrectlySwitchedWhenExtended(extend, testOpenSearch);

  // Prepare for removing engines.
  notificationBoxStub.resetHistory();

  await assertCorrectlySwitchedWhenRemoved(remove, engine.id, testOpenSearch);

  let settingsData = await promiseSettingsData();
  Assert.ok(
    settingsData.engines.every(e => !e._metaData.overriddenBy),
    "Should have cleared the overridden by flag after removal"
  );
}

async function restartSearchService(initSearchService = false) {
  info("Restarting search service.");
  await promiseAfterSettings();
  Services.search.wrappedJSObject.reset();
  if (initSearchService) {
    await Services.search.init();
  }
}

async function simulateExtensionStartup(trySetDefault) {
  let extensionData = {
    ...extension.extension,
    startupReason: "APP_STARTUP",
  };
  if (trySetDefault) {
    await Services.search.maybeSetAndOverrideDefault(extensionData);
  } else {
    await Services.search.addEnginesFromExtension(extensionData);
  }
}

/**
 * Asserts that overrides are handled correctly when a WebExtension is
 * installed, and an application provided engine is added for the user.
 *
 * This is designed to be used prior to assertCorrectlySwitchedWhenRemoved.
 *
 * @param {Function} changeFn
 *   A function that applies the change to cause the application provided
 *   engine to be added for the user.
 * @param {boolean} testOpenSearch
 *   Set to true to test OpenSearch based engines.
 */
async function assertCorrectlySwitchedWhenExtended(
  changeFn,
  testOpenSearch = false
) {
  await changeFn();

  await assertEngineCorrectlySet({
    expectedId: "simple",
    expectedAlias: ALIAS,
    appEngineOverriden: true,
  });
  Assert.ok(
    notificationBoxStub.notCalled,
    "Should not have attempted to display a notification box"
  );

  info("Test restarting search service ensure settings are kept.");
  await restartSearchService(true);
  if (!testOpenSearch) {
    await simulateExtensionStartup(true);
  }

  Assert.ok(
    notificationBoxStub.notCalled,
    "Should not have attempted to display a notification box"
  );
  await assertEngineCorrectlySet({
    expectedId: "simple",
    expectedAlias: ALIAS,
    appEngineOverriden: true,
  });
}

/**
 * Asserts that overrides are handled correctly when a WebExtension is
 * installed and overriding an application provided engine, and then the
 * application provided engine is removed from the user.
 *
 * This is designed to be used after to assertCorrectlySwitchedWhenExtended.
 *
 * @param {Function} changeFn
 *   A function that applies the change to cause the application provided
 *   engine to be removed for the user.
 * @param {string} engineId
 *   The id of the engine.
 * @param {boolean} testOpenSearch
 *   Set to true to test OpenSearch based engines.
 */
async function assertCorrectlySwitchedWhenRemoved(
  changeFn,
  engineId,
  testOpenSearch = false
) {
  await changeFn();

  await assertEngineCorrectlySet({
    expectedId: engineId,
    expectedAlias: ALIAS,
    appEngineOverriden: false,
  });

  info("Test restarting search service to remove application provided engine");

  await restartSearchService(false);

  if (!testOpenSearch) {
    await simulateExtensionStartup(false);
  }

  await Services.search.init();

  await assertEngineCorrectlySet({
    expectedId: engineId,
    expectedAlias: ALIAS,
    appEngineOverriden: false,
  });

  if (testOpenSearch) {
    await Services.search.removeEngine(Services.search.getEngineById(engineId));
  } else {
    await extension.unload();
  }
}

async function assertEngineCorrectlySet({
  expectedAlias = "",
  expectedId,
  appEngineOverriden,
}) {
  let engines = await Services.search.getEngines();
  Assert.equal(
    engines.filter(e => e.name == ENGINE_NAME).length,
    1,
    "Should only be one engine with matching name after changing configuration"
  );

  let defaultEngine = await Services.search.getDefault();
  Assert.equal(
    defaultEngine.id,
    expectedId,
    "Should have kept the third party engine as default"
  );
  Assert.equal(
    decodeURI(defaultEngine.getSubmission("{searchTerms}").uri.spec),
    SEARCH_URL_BASE + SEARCH_URL_PARAMS,
    "Should have used the third party engine's URLs"
  );
  Assert.equal(
    !!defaultEngine.wrappedJSObject.getAttr("overriddenBy"),
    appEngineOverriden,
    "Should have correctly overridden or not."
  );

  Assert.equal(
    defaultEngine.telemetryId,
    appEngineOverriden ? "simple-addon" : "other-Simple Engine",
    "Should set the correct telemetry Id"
  );

  Assert.equal(
    defaultEngine.alias,
    expectedAlias,
    "Should have the correct alias"
  );
}

async function installThirdPartyEngineAsDefault(useOpenSearch) {
  let engine;
  let extension = null;

  if (useOpenSearch) {
    engine = await SearchTestUtils.installOpenSearchEngine({
      url: `${gHttpURL}/sjs/engineMaker.sjs?${JSON.stringify({
        baseURL: SEARCH_URL_BASE,
        queryString: SEARCH_URL_PARAMS,
        name: ENGINE_NAME,
        method: "GET",
      })}`,
    });
  } else {
    extension = await SearchTestUtils.installSearchExtension(
      {
        name: ENGINE_NAME,
        search_url: SEARCH_URL_BASE,
        search_url_get_params: SEARCH_URL_PARAMS,
      },
      { skipUnload: true }
    );
    await extension.awaitStartup();

    engine = Services.search.getEngineById(
      "simpleengine@tests.mozilla.orgdefault"
    );
  }

  await Services.search.setDefault(
    engine,
    Ci.nsISearchService.CHANGE_REASON_UNKNOWN
  );

  return { engine, extension };
}
