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

"use strict";

const { AddonTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/AddonTestUtils.sys.mjs"
);
const { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);

let delay = () => new Promise(resolve => setTimeout(resolve, 0));

const kSearchFormURL = "https://example.com/searchform";
const kSearchEngineURL = "https://example.com/?search={searchTerms}";
const kSearchSuggestURL = "https://example.com/?suggest={searchTerms}";
const kSearchTerm = "foo";
const kSearchTermIntl = "日";
const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";

AddonTestUtils.init(this);
AddonTestUtils.createAppInfo(
  "xpcshell@tests.mozilla.org",
  "XPCShell",
  "42",
  "42"
);

add_task(async function setup() {
  await AddonTestUtils.promiseStartupManager();
  await Services.search.init();
});

add_task(async function test_extension_adding_engine() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      icons: {
        16: "foo.ico",
        32: "foo32.ico",
      },
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch",
          keyword: "MozSearch",
          search_form: kSearchFormURL,
          search_url: kSearchEngineURL,
          suggest_url: kSearchSuggestURL,
        },
      },
    },
    useAddonManager: "temporary",
  });

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

  let engine = Services.search.getEngineByName("MozSearch");
  ok(engine, "Engine should exist.");

  let { baseURI } = ext1.extension;
  equal(engine.iconURI.spec, baseURI.resolve("foo.ico"), "icon path matches");
  let icons = engine.getIcons();
  equal(icons.length, 2, "both icons avialable");
  equal(icons[0].url, baseURI.resolve("foo.ico"), "icon path matches");
  equal(icons[1].url, baseURI.resolve("foo32.ico"), "icon path matches");

  let expectedSuggestURL = kSearchSuggestURL.replace(
    "{searchTerms}",
    kSearchTerm
  );
  let submissionSuggest = engine.getSubmission(
    kSearchTerm,
    URLTYPE_SUGGEST_JSON
  );
  let encodedSubmissionURL = engine.getSubmission(kSearchTermIntl).uri.spec;
  let testSubmissionURL = kSearchEngineURL.replace(
    "{searchTerms}",
    encodeURIComponent(kSearchTermIntl)
  );
  equal(
    encodedSubmissionURL,
    testSubmissionURL,
    "Encoded UTF-8 URLs should match"
  );

  equal(
    submissionSuggest.uri.spec,
    expectedSuggestURL,
    "Suggest URLs should match"
  );

  equal(engine.searchForm, kSearchFormURL, "Search form URLs should match");
  await ext1.unload();
  await delay();

  engine = Services.search.getEngineByName("MozSearch");
  ok(!engine, "Engine should not exist");
});

add_task(async function test_extension_adding_engine_with_spaces() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch     ",
          keyword: "MozSearch",
          search_url: "https://example.com/?q={searchTerms}",
        },
      },
    },
    useAddonManager: "temporary",
  });

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

  let engine = Services.search.getEngineByName("MozSearch");
  ok(engine, "Engine should exist.");

  await ext1.unload();
  await delay();

  engine = Services.search.getEngineByName("MozSearch");
  ok(!engine, "Engine should not exist");
});

add_task(async function test_upgrade_default_position_engine() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch",
          keyword: "MozSearch",
          search_url: "https://example.com/?q={searchTerms}",
        },
      },
      browser_specific_settings: {
        gecko: {
          id: "testengine@mozilla.com",
        },
      },
      version: "0.1",
    },
    useAddonManager: "temporary",
  });

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

  let engine = Services.search.getEngineByName("MozSearch");
  await Services.search.setDefault(
    engine,
    Ci.nsISearchService.CHANGE_REASON_UNKNOWN
  );
  await Services.search.moveEngine(engine, 1);

  await ext1.upgrade({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch",
          keyword: "MozSearch",
          search_url: "https://example.com/?q={searchTerms}",
        },
      },
      browser_specific_settings: {
        gecko: {
          id: "testengine@mozilla.com",
        },
      },
      version: "0.2",
    },
    useAddonManager: "temporary",
  });
  await AddonTestUtils.waitForSearchProviderStartup(ext1);

  engine = Services.search.getEngineByName("MozSearch");
  equal(
    Services.search.defaultEngine,
    engine,
    "Default engine should still be MozSearch"
  );
  equal(
    (await Services.search.getEngines()).map(e => e.name).indexOf(engine.name),
    1,
    "Engine is in position 1"
  );

  await ext1.unload();
  await delay();

  engine = Services.search.getEngineByName("MozSearch");
  ok(!engine, "Engine should not exist");
});

add_task(async function test_extension_get_params() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch",
          keyword: "MozSearch",
          search_url: kSearchEngineURL,
          search_url_get_params: "foo=bar&bar=foo",
          suggest_url: kSearchSuggestURL,
          suggest_url_get_params: "foo=bar&bar=foo",
        },
      },
    },
    useAddonManager: "temporary",
  });

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

  let engine = Services.search.getEngineByName("MozSearch");
  ok(engine, "Engine should exist.");

  let url = engine.wrappedJSObject._getURLOfType("text/html");
  equal(url.method, "GET", "Search URLs method is GET");

  let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
  let submission = engine.getSubmission(kSearchTerm);
  equal(
    submission.uri.spec,
    `${expectedURL}&foo=bar&bar=foo`,
    "Search URLs should match"
  );

  let expectedSuggestURL = kSearchSuggestURL.replace(
    "{searchTerms}",
    kSearchTerm
  );
  let submissionSuggest = engine.getSubmission(
    kSearchTerm,
    URLTYPE_SUGGEST_JSON
  );
  equal(
    submissionSuggest.uri.spec,
    `${expectedSuggestURL}&foo=bar&bar=foo`,
    "Suggest URLs should match"
  );

  await ext1.unload();
});

add_task(async function test_extension_post_params() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch",
          keyword: "MozSearch",
          search_url: kSearchEngineURL,
          search_url_post_params: "foo=bar&bar=foo",
          suggest_url: kSearchSuggestURL,
          suggest_url_post_params: "foo=bar&bar=foo",
        },
      },
    },
    useAddonManager: "temporary",
  });

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

  let engine = Services.search.getEngineByName("MozSearch");
  ok(engine, "Engine should exist.");

  let url = engine.wrappedJSObject._getURLOfType("text/html");
  equal(url.method, "POST", "Search URLs method is POST");

  let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
  let submission = engine.getSubmission(kSearchTerm);
  equal(submission.uri.spec, expectedURL, "Search URLs should match");
  // postData is a nsIMIMEInputStream which contains a nsIStringInputStream.
  equal(
    submission.postData.data.data,
    "foo=bar&bar=foo",
    "Search postData should match"
  );

  let expectedSuggestURL = kSearchSuggestURL.replace(
    "{searchTerms}",
    kSearchTerm
  );
  let submissionSuggest = engine.getSubmission(
    kSearchTerm,
    URLTYPE_SUGGEST_JSON
  );
  equal(
    submissionSuggest.uri.spec,
    expectedSuggestURL,
    "Suggest URLs should match"
  );
  equal(
    submissionSuggest.postData.data.data,
    "foo=bar&bar=foo",
    "Suggest postData should match"
  );

  await ext1.unload();
});

add_task(async function test_extension_no_query_params() {
  const ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch",
          keyword: "MozSearch",
          search_url: "https://example.com/{searchTerms}",
          suggest_url: "https://example.com/suggest/{searchTerms}",
        },
      },
    },
    useAddonManager: "temporary",
  });

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

  let engine = Services.search.getEngineByName("MozSearch");
  ok(engine, "Engine should exist.");

  const encodedSubmissionURL = engine.getSubmission(kSearchTermIntl).uri.spec;
  const testSubmissionURL =
    "https://example.com/" + encodeURIComponent(kSearchTermIntl);
  equal(
    encodedSubmissionURL,
    testSubmissionURL,
    "Encoded UTF-8 URLs should match"
  );

  const expectedSuggestURL = "https://example.com/suggest/" + kSearchTerm;
  let submissionSuggest = engine.getSubmission(
    kSearchTerm,
    URLTYPE_SUGGEST_JSON
  );
  equal(
    submissionSuggest.uri.spec,
    expectedSuggestURL,
    "Suggest URLs should match"
  );

  await ext1.unload();
  await delay();

  engine = Services.search.getEngineByName("MozSearch");
  ok(!engine, "Engine should not exist");
});

add_task(async function test_extension_empty_suggestUrl() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      default_locale: "en",
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch",
          keyword: "MozSearch",
          search_url: kSearchEngineURL,
          search_url_post_params: "foo=bar&bar=foo",
          suggest_url: "__MSG_suggestUrl__",
          suggest_url_get_params: "__MSG_suggestUrlGetParams__",
        },
      },
    },
    useAddonManager: "temporary",
    files: {
      "_locales/en/messages.json": {
        suggestUrl: {
          message: "",
        },
        suggestUrlGetParams: {
          message: "",
        },
      },
    },
  });

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

  let engine = Services.search.getEngineByName("MozSearch");
  ok(engine, "Engine should exist.");

  let url = engine.wrappedJSObject._getURLOfType("text/html");
  equal(url.method, "POST", "Search URLs method is POST");

  let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
  let submission = engine.getSubmission(kSearchTerm);
  equal(submission.uri.spec, expectedURL, "Search URLs should match");
  // postData is a nsIMIMEInputStream which contains a nsIStringInputStream.
  equal(
    submission.postData.data.data,
    "foo=bar&bar=foo",
    "Search postData should match"
  );

  let submissionSuggest = engine.getSubmission(
    kSearchTerm,
    URLTYPE_SUGGEST_JSON
  );
  ok(!submissionSuggest, "There should be no suggest URL.");

  await ext1.unload();
});

add_task(async function test_extension_empty_suggestUrl_with_params() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      default_locale: "en",
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch",
          keyword: "MozSearch",
          search_url: kSearchEngineURL,
          search_url_post_params: "foo=bar&bar=foo",
          suggest_url: "__MSG_suggestUrl__",
          suggest_url_get_params: "__MSG_suggestUrlGetParams__",
        },
      },
    },
    useAddonManager: "temporary",
    files: {
      "_locales/en/messages.json": {
        suggestUrl: {
          message: "",
        },
        suggestUrlGetParams: {
          message: "abc",
        },
      },
    },
  });

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

  let engine = Services.search.getEngineByName("MozSearch");
  ok(engine, "Engine should exist.");

  let url = engine.wrappedJSObject._getURLOfType("text/html");
  equal(url.method, "POST", "Search URLs method is POST");

  let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
  let submission = engine.getSubmission(kSearchTerm);
  equal(submission.uri.spec, expectedURL, "Search URLs should match");
  // postData is a nsIMIMEInputStream which contains a nsIStringInputStream.
  equal(
    submission.postData.data.data,
    "foo=bar&bar=foo",
    "Search postData should match"
  );

  let submissionSuggest = engine.getSubmission(
    kSearchTerm,
    URLTYPE_SUGGEST_JSON
  );
  ok(!submissionSuggest, "There should be no suggest URL.");

  await ext1.unload();
});

async function checkBadUrl(searchProviderKey, urlValue) {
  let normalized = await ExtensionTestUtils.normalizeManifest({
    chrome_settings_overrides: {
      search_provider: {
        name: "MozSearch",
        keyword: "MozSearch",
        search_url: "https://example.com/",
        [searchProviderKey]: urlValue,
      },
    },
  });

  ok(
    /Error processing chrome_settings_overrides\.search_provider[^:]*: .* must match/.test(
      normalized.error
    ),
    `Expected error for ${searchProviderKey}:${urlValue} "${normalized.error}"`
  );
}

async function checkValidUrl(urlValue) {
  let normalized = await ExtensionTestUtils.normalizeManifest({
    chrome_settings_overrides: {
      search_provider: {
        name: "MozSearch",
        keyword: "MozSearch",
        search_form: urlValue,
        search_url: urlValue,
        suggest_url: urlValue,
      },
    },
  });
  equal(normalized.error, undefined, `Valid search_provider url: ${urlValue}`);
}

add_task(async function test_extension_not_allow_http() {
  await checkBadUrl("search_form", "http://example.com/{searchTerms}");
  await checkBadUrl("search_url", "http://example.com/{searchTerms}");
  await checkBadUrl("suggest_url", "http://example.com/{searchTerms}");
});

add_task(async function test_manifest_disallows_http_localhost_prefix() {
  await checkBadUrl("search_url", "http://localhost.example.com");
  await checkBadUrl("search_url", "http://localhost.example.com/");
  await checkBadUrl("search_url", "http://127.0.0.1.example.com/");
  await checkBadUrl("search_url", "http://localhost:1234@example.com/");
});

add_task(async function test_manifest_allow_http_for_localhost() {
  await checkValidUrl("http://localhost");
  await checkValidUrl("http://localhost/");
  await checkValidUrl("http://localhost:/");
  await checkValidUrl("http://localhost:1/");
  await checkValidUrl("http://localhost:65535/");

  await checkValidUrl("http://127.0.0.1");
  await checkValidUrl("http://127.0.0.1:");
  await checkValidUrl("http://127.0.0.1:/");
  await checkValidUrl("http://127.0.0.1/");
  await checkValidUrl("http://127.0.0.1:80/");

  await checkValidUrl("http://[::1]");
  await checkValidUrl("http://[::1]:");
  await checkValidUrl("http://[::1]:/");
  await checkValidUrl("http://[::1]/");
  await checkValidUrl("http://[::1]:80/");
});

add_task(async function test_extension_allow_http_for_localhost() {
  let ext1 = ExtensionTestUtils.loadExtension({
    manifest: {
      chrome_settings_overrides: {
        search_provider: {
          name: "MozSearch",
          keyword: "MozSearch",
          search_url: "http://localhost/{searchTerms}",
          suggest_url: "http://localhost/suggest/{searchTerms}",
        },
      },
    },
    useAddonManager: "temporary",
  });

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

  let engine = Services.search.getEngineByName("MozSearch");
  ok(engine, "Engine should exist.");

  await ext1.unload();
});

add_task(async function test_search_favicon_mv3() {
  Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
  let normalized = await ExtensionTestUtils.normalizeManifest({
    manifest_version: 3,
    chrome_settings_overrides: {
      search_provider: {
        name: "HTTP Icon in MV3",
        search_url: "https://example.org/",
        favicon_url: "https://example.org/icon.png",
      },
    },
  });
  Assert.ok(
    normalized.error.endsWith("must be a relative URL"),
    "Should have an error"
  );
  normalized = await ExtensionTestUtils.normalizeManifest({
    manifest_version: 3,
    chrome_settings_overrides: {
      search_provider: {
        name: "HTTP Icon in MV3",
        search_url: "https://example.org/",
        favicon_url: "/icon.png",
      },
    },
  });
  Assert.ok(!normalized.error, "Should not have an error");
});
