/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
  SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
  TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
});

const EXTENSION1_ID = "extension1@mozilla.com";
const EXTENSION2_ID = "extension2@mozilla.com";
const DEFAULT_SEARCH_STORE_TYPE = "default_search";
const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch";

AddonTestUtils.initMochitest(this);
SearchTestUtils.init(this);

const DEFAULT_ENGINE = {
  id: "basic",
  name: "basic",
  loadPath: "[addon]basic@search.mozilla.org",
  submissionUrl:
    "https://mochi.test:8888/browser/browser/components/search/test/browser/?search=&foo=1",
};
const ALTERNATE_ENGINE = {
  id: "simple",
  name: "Simple Engine",
  loadPath: "[addon]simple@search.mozilla.org",
  submissionUrl: "https://example.com/?sourceId=Mozilla-search&search=",
};
const ALTERNATE2_ENGINE = {
  id: "simple",
  name: "another",
  loadPath: "",
  submissionUrl: "",
};

async function restoreDefaultEngine() {
  let engine = Services.search.getEngineByName(DEFAULT_ENGINE.name);
  await Services.search.setDefault(
    engine,
    Ci.nsISearchService.CHANGE_REASON_UNKNOWN
  );
}

function clearTelemetry() {
  Services.telemetry.clearEvents();
  Services.fog.testResetFOG();
}

async function checkTelemetry(source, prevEngine, newEngine) {
  TelemetryTestUtils.assertEvents(
    [
      {
        object: "change_default",
        value: source,
        extra: {
          prev_id: prevEngine.id,
          new_id: newEngine.id,
          new_name: newEngine.name,
          new_load_path: newEngine.loadPath,
          // Telemetry has a limit of 80 characters.
          new_sub_url: newEngine.submissionUrl.slice(0, 80),
        },
      },
    ],
    { category: "search", method: "engine" }
  );

  let snapshot = await Glean.searchEngineDefault.changed.testGetValue();
  delete snapshot[0].timestamp;
  Assert.deepEqual(
    snapshot[0],
    {
      category: "search.engine.default",
      name: "changed",
      extra: {
        change_source: source,
        previous_engine_id: prevEngine.id,
        new_engine_id: newEngine.id,
        new_display_name: newEngine.name,
        new_load_path: newEngine.loadPath,
        new_submission_url: newEngine.submissionUrl,
      },
    },
    "Should have received the correct event details"
  );
}

add_setup(async function () {
  let searchExtensions = getChromeDir(getResolvedURI(gTestPath));
  searchExtensions.append("search-engines");

  await SearchTestUtils.useMochitestEngines(searchExtensions);

  SearchTestUtils.useMockIdleService();
  let response = await fetch(`resource://search-extensions/engines.json`);
  let json = await response.json();
  await SearchTestUtils.updateRemoteSettingsConfig(json.data);

  registerCleanupFunction(async () => {
    let settingsWritten = SearchTestUtils.promiseSearchNotification(
      "write-settings-to-disk-complete"
    );
    await SearchTestUtils.updateRemoteSettingsConfig();
    await settingsWritten;
  });
});

/* This tests setting a default engine. */
add_task(async function test_extension_setting_default_engine() {
  clearTelemetry();

  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  await ext1.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext1);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );

  await checkTelemetry("addon-install", DEFAULT_ENGINE, ALTERNATE_ENGINE);

  clearTelemetry();

  await ext1.unload();

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    `Default engine is ${DEFAULT_ENGINE.name}`
  );

  await checkTelemetry("addon-uninstall", ALTERNATE_ENGINE, DEFAULT_ENGINE);
});

/* This tests what happens when the engine you're setting it to is hidden. */
add_task(async function test_extension_setting_default_engine_hidden() {
  let engine = Services.search.getEngineByName(ALTERNATE_ENGINE.name);
  engine.hidden = true;

  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  await ext1.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext1);

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    "Default engine should have remained as the default"
  );
  is(
    ExtensionSettingsStore.getSetting("default_search", "defaultSearch"),
    null,
    "The extension should not have been recorded as having set the default search"
  );

  await ext1.unload();

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    `Default engine is ${DEFAULT_ENGINE.name}`
  );
  engine.hidden = false;
});

// Test the popup displayed when trying to add a non-built-in default
// search engine.
add_task(async function test_extension_setting_default_engine_external() {
  const NAME = "Example Engine";

  // Load an extension that tries to set the default engine,
  // and wait for the ensuing prompt.
  async function startExtension(win = window) {
    let extension = ExtensionTestUtils.loadExtension({
      manifest: {
        icons: {
          48: "icon.png",
          96: "icon@2x.png",
        },
        browser_specific_settings: {
          gecko: {
            id: EXTENSION1_ID,
          },
        },
        chrome_settings_overrides: {
          search_provider: {
            name: NAME,
            search_url: "https://example.com/?q={searchTerms}",
            is_default: true,
          },
        },
      },
      files: {
        "icon.png": "",
        "icon@2x.png": "",
      },
      useAddonManager: "temporary",
    });

    let [panel] = await Promise.all([
      promisePopupNotificationShown("addon-webext-defaultsearch", win),
      extension.startup(),
    ]);

    isnot(
      panel,
      null,
      "Doorhanger was displayed for non-built-in default engine"
    );

    return { panel, extension };
  }

  // First time around, don't accept the default engine.
  let { panel, extension } = await startExtension();
  ok(
    panel.getAttribute("icon").endsWith("/icon.png"),
    "expected custom icon set on the notification"
  );

  panel.secondaryButton.click();

  await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    "Default engine was not changed after rejecting prompt"
  );

  await extension.unload();

  clearTelemetry();

  // Do it again, this time accept the prompt.
  ({ panel, extension } = await startExtension());

  panel.button.click();
  await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");

  is(
    (await Services.search.getDefault()).name,
    NAME,
    "Default engine was changed after accepting prompt"
  );

  await checkTelemetry("addon-install", DEFAULT_ENGINE, {
    id: "other-Example Engine",
    name: "Example Engine",
    loadPath: "[addon]extension1@mozilla.com",
    submissionUrl: "https://example.com/?q=",
  });
  clearTelemetry();

  // Do this twice to make sure we're definitely handling disable/enable
  // correctly.  Disabling and enabling the addon here like this also
  // replicates the behavior when an addon is added then removed in the
  // blocklist.
  let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
  let addon = await AddonManager.getAddonByID(EXTENSION1_ID);
  await addon.disable();
  await disabledPromise;

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    `Default engine is ${DEFAULT_ENGINE.name} after disabling`
  );

  await checkTelemetry(
    "addon-uninstall",
    {
      id: "other-Example Engine",
      name: "Example Engine",
      loadPath: "[addon]extension1@mozilla.com",
      submissionUrl: "https://example.com/?q=",
    },
    DEFAULT_ENGINE
  );
  clearTelemetry();

  let opened = promisePopupNotificationShown(
    "addon-webext-defaultsearch",
    window
  );
  await addon.enable();
  panel = await opened;
  panel.button.click();
  await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");

  is(
    (await Services.search.getDefault()).name,
    NAME,
    `Default engine is ${NAME} after enabling`
  );

  await checkTelemetry("addon-install", DEFAULT_ENGINE, {
    id: "other-Example Engine",
    name: "Example Engine",
    loadPath: "[addon]extension1@mozilla.com",
    submissionUrl: "https://example.com/?q=",
  });

  await extension.unload();

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    "Default engine is reverted after uninstalling extension."
  );

  // One more time, this time close the window where the prompt
  // appears instead of explicitly accepting or denying it.
  let win = await BrowserTestUtils.openNewBrowserWindow();
  await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank");

  ({ extension } = await startExtension(win));

  await BrowserTestUtils.closeWindow(win);

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    "Default engine is unchanged when prompt is dismissed"
  );

  await extension.unload();
});

/* This tests that uninstalling add-ons maintains the proper
 * search default. */
add_task(async function test_extension_setting_multiple_default_engine() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  let ext2 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE2_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  await ext1.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext1);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );

  await ext2.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext2);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );

  await ext2.unload();

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );

  await ext1.unload();

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    `Default engine is ${DEFAULT_ENGINE.name}`
  );
});

/* This tests that uninstalling add-ons in reverse order maintains the proper
 * search default. */
add_task(
  async function test_extension_setting_multiple_default_engine_reversed() {
    let ext1 = ExtensionTestUtils.loadExtension({
      manifest: {
        chrome_settings_overrides: {
          search_provider: {
            name: ALTERNATE_ENGINE.name,
            search_url: "https://example.com/?q={searchTerms}",
            is_default: true,
          },
        },
      },
      useAddonManager: "temporary",
    });

    let ext2 = ExtensionTestUtils.loadExtension({
      manifest: {
        chrome_settings_overrides: {
          search_provider: {
            name: ALTERNATE2_ENGINE.name,
            search_url: "https://example.com/?q={searchTerms}",
            is_default: true,
          },
        },
      },
      useAddonManager: "temporary",
    });

    await ext1.startup();
    await AddonTestUtils.waitForSearchProviderStartup(ext1);

    is(
      (await Services.search.getDefault()).name,
      ALTERNATE_ENGINE.name,
      `Default engine is ${ALTERNATE_ENGINE.name}`
    );

    await ext2.startup();
    await AddonTestUtils.waitForSearchProviderStartup(ext2);

    is(
      (await Services.search.getDefault()).name,
      ALTERNATE2_ENGINE.name,
      `Default engine is ${ALTERNATE2_ENGINE.name}`
    );

    await ext1.unload();

    is(
      (await Services.search.getDefault()).name,
      ALTERNATE2_ENGINE.name,
      `Default engine is ${ALTERNATE2_ENGINE.name}`
    );

    await ext2.unload();

    is(
      (await Services.search.getDefault()).name,
      DEFAULT_ENGINE.name,
      `Default engine is ${DEFAULT_ENGINE.name}`
    );
  }
);

/* This tests that when the user changes the search engine and the add-on
 * is unistalled, search stays with the user's choice. */
add_task(async function test_user_changing_default_engine() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  await ext1.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext1);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );

  let engine = Services.search.getEngineByName(ALTERNATE2_ENGINE.name);
  await Services.search.setDefault(
    engine,
    Ci.nsISearchService.CHANGE_REASON_UNKNOWN
  );
  // This simulates the preferences UI when the setting is changed.
  ExtensionSettingsStore.select(
    ExtensionSettingsStore.SETTING_USER_SET,
    DEFAULT_SEARCH_STORE_TYPE,
    DEFAULT_SEARCH_SETTING_NAME
  );

  await ext1.unload();

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );
  restoreDefaultEngine();
});

/* This tests that when the user changes the search engine while it is
 * disabled, user choice is maintained when the add-on is reenabled. */
add_task(async function test_user_change_with_disabling() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: {
        gecko: {
          id: EXTENSION1_ID,
        },
      },
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  await ext1.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext1);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );

  let engine = Services.search.getEngineByName(ALTERNATE2_ENGINE.name);
  await Services.search.setDefault(
    engine,
    Ci.nsISearchService.CHANGE_REASON_UNKNOWN
  );
  // This simulates the preferences UI when the setting is changed.
  ExtensionSettingsStore.select(
    ExtensionSettingsStore.SETTING_USER_SET,
    DEFAULT_SEARCH_STORE_TYPE,
    DEFAULT_SEARCH_SETTING_NAME
  );

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );

  let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
  let addon = await AddonManager.getAddonByID(EXTENSION1_ID);
  await addon.disable();
  await disabledPromise;

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );

  let processedPromise = awaitEvent("searchEngineProcessed", EXTENSION1_ID);
  await addon.enable();
  await processedPromise;

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );
  await ext1.unload();
  await restoreDefaultEngine();
});

/* This tests that when two add-ons are installed that change default
 * search and the first one is disabled, before the second one is installed,
 * when the first one is reenabled, the second add-on keeps the search. */
add_task(async function test_two_addons_with_first_disabled_before_second() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: {
        gecko: {
          id: EXTENSION1_ID,
        },
      },
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  let ext2 = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: {
        gecko: {
          id: EXTENSION2_ID,
        },
      },
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE2_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  await ext1.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext1);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );

  let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
  let addon1 = await AddonManager.getAddonByID(EXTENSION1_ID);
  await addon1.disable();
  await disabledPromise;

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    `Default engine is ${DEFAULT_ENGINE.name}`
  );

  await ext2.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext2);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );

  let enabledPromise = awaitEvent("ready", EXTENSION1_ID);
  await addon1.enable();
  await enabledPromise;

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );
  await ext2.unload();
  await ext1.unload();

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    `Default engine is ${DEFAULT_ENGINE.name}`
  );
});

/* This tests that when two add-ons are installed that change default
 * search and the first one is disabled, the second one maintains
 * the search. */
add_task(async function test_two_addons_with_first_disabled() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: {
        gecko: {
          id: EXTENSION1_ID,
        },
      },
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  let ext2 = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: {
        gecko: {
          id: EXTENSION2_ID,
        },
      },
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE2_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  await ext1.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext1);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );

  await ext2.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext2);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );

  let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
  let addon1 = await AddonManager.getAddonByID(EXTENSION1_ID);
  await addon1.disable();
  await disabledPromise;

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );

  let enabledPromise = awaitEvent("ready", EXTENSION1_ID);
  await addon1.enable();
  await enabledPromise;

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );
  await ext2.unload();
  await ext1.unload();

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    `Default engine is ${DEFAULT_ENGINE.name}`
  );
});

/* This tests that when two add-ons are installed that change default
 * search and the second one is disabled, the first one properly
 * gets the search. */
add_task(async function test_two_addons_with_second_disabled() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: {
        gecko: {
          id: EXTENSION1_ID,
        },
      },
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  let ext2 = ExtensionTestUtils.loadExtension({
    manifest: {
      browser_specific_settings: {
        gecko: {
          id: EXTENSION2_ID,
        },
      },
      chrome_settings_overrides: {
        search_provider: {
          name: ALTERNATE2_ENGINE.name,
          search_url: "https://example.com/?q={searchTerms}",
          is_default: true,
        },
      },
    },
    useAddonManager: "temporary",
  });

  await ext1.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext1);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );

  await ext2.startup();
  await AddonTestUtils.waitForSearchProviderStartup(ext2);

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );

  let disabledPromise = awaitEvent("shutdown", EXTENSION2_ID);
  let addon2 = await AddonManager.getAddonByID(EXTENSION2_ID);
  await addon2.disable();
  await disabledPromise;

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );

  let defaultPromise = SearchTestUtils.promiseSearchNotification(
    "engine-default",
    "browser-search-engine-modified"
  );
  // No prompt, because this is switching to an app-provided engine.
  await addon2.enable();
  await defaultPromise;

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE2_ENGINE.name,
    `Default engine is ${ALTERNATE2_ENGINE.name}`
  );
  await ext2.unload();

  is(
    (await Services.search.getDefault()).name,
    ALTERNATE_ENGINE.name,
    `Default engine is ${ALTERNATE_ENGINE.name}`
  );
  await ext1.unload();

  is(
    (await Services.search.getDefault()).name,
    DEFAULT_ENGINE.name,
    `Default engine is ${DEFAULT_ENGINE.name}`
  );
});
