"use strict";

const { Preferences } = ChromeUtils.importESModule(
  "resource://gre/modules/Preferences.sys.mjs"
);

// ExtensionContent.jsm needs to know when it's running from xpcshell,
// to use the right timeout for content scripts executed at document_idle.
ExtensionTestUtils.mockAppInfo();

const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));

const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;

var originalReqLocales = Services.locale.requestedLocales;

registerCleanupFunction(() => {
  Preferences.reset("intl.accept_languages");
  Services.locale.requestedLocales = originalReqLocales;
});

add_task(async function test_i18n() {
  function runTests(assertEq) {
    let _ = browser.i18n.getMessage.bind(browser.i18n);

    let url = browser.runtime.getURL("/");
    assertEq(
      url,
      `moz-extension://${_("@@extension_id")}/`,
      "@@extension_id builtin message"
    );

    assertEq("Foo.", _("Foo"), "Simple message in selected locale.");

    assertEq("(bar)", _("bar"), "Simple message fallback in default locale.");

    assertEq("", _("some-unknown-locale-string"), "Unknown locale string.");

    assertEq("", _("@@unknown_builtin_string"), "Unknown built-in string.");
    assertEq(
      "",
      _("@@bidi_unknown_builtin_string"),
      "Unknown built-in bidi string."
    );

    assertEq("Føo.", _("Föo"), "Multi-byte message in selected locale.");

    let substitutions = [];
    substitutions[4] = "5";
    substitutions[13] = "14";

    assertEq(
      "'$0' '14' '' '5' '$$$$' '$'.",
      _("basic_substitutions", substitutions),
      "Basic numeric substitutions"
    );

    assertEq(
      "'$0' '' 'just a string' '' '$$$$' '$'.",
      _("basic_substitutions", "just a string"),
      "Basic numeric substitutions, with non-array value"
    );

    let values = _("named_placeholder_substitutions", [
      "(subst $1 $2)",
      "(2 $1 $2)",
    ]).split("\n");

    assertEq(
      "_foo_ (subst $1 $2) _bar_",
      values[0],
      "Named and numeric substitution"
    );

    assertEq(
      "(2 $1 $2)",
      values[1],
      "Numeric substitution amid named placeholders"
    );

    assertEq("$bad name$", values[2], "Named placeholder with invalid key");

    assertEq("", values[3], "Named placeholder with an invalid value");

    assertEq(
      "Accepted, but shouldn't break.",
      values[4],
      "Named placeholder with a strange content value"
    );

    assertEq("$foo", values[5], "Non-placeholder token that should be ignored");
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      default_locale: "jp",

      content_scripts: [
        { matches: ["http://*/*/file_sample.html"], js: ["content.js"] },
      ],
    },

    files: {
      "_locales/en_US/messages.json": {
        foo: {
          message: "Foo.",
          description: "foo",
        },

        föo: {
          message: "Føo.",
          description: "foo",
        },

        basic_substitutions: {
          message: "'$0' '$14' '$1' '$5' '$$$$$' '$$'.",
          description: "foo",
        },

        Named_placeholder_substitutions: {
          message:
            "$Foo$\n$2\n$bad name$\n$bad_value$\n$bad_content_value$\n$foo",
          description: "foo",
          placeholders: {
            foO: {
              content: "_foo_ $1 _bar_",
              description: "foo",
            },

            "bad name": {
              content: "Nope.",
              description: "bad name",
            },

            bad_value: "Nope.",

            bad_content_value: {
              content: ["Accepted, but shouldn't break."],
              description: "bad value",
            },
          },
        },

        broken_placeholders: {
          message: "$broken$",
          description: "broken placeholders",
          placeholders: "foo.",
        },
      },

      "_locales/jp/messages.json": {
        foo: {
          message: "(foo)",
          description: "foo",
        },

        bar: {
          message: "(bar)",
          description: "bar",
        },
      },

      "content.js":
        "new " +
        function (runTestsFn) {
          runTestsFn((...args) => {
            browser.runtime.sendMessage(["assertEq", ...args]);
          });

          browser.runtime.sendMessage(["content-script-finished"]);
        } +
        `(${runTests})`,
    },

    background:
      "new " +
      function (runTestsFn) {
        browser.runtime.onMessage.addListener(([msg, ...args]) => {
          if (msg == "assertEq") {
            browser.test.assertEq(...args);
          } else {
            browser.test.sendMessage(msg, ...args);
          }
        });

        runTestsFn(browser.test.assertEq.bind(browser.test));
      } +
      `(${runTests})`,
  });

  await extension.startup();

  let contentPage = await ExtensionTestUtils.loadContentPage(
    `${BASE_URL}/file_sample.html`
  );
  await extension.awaitMessage("content-script-finished");
  await contentPage.close();

  await extension.unload();
});

add_task(async function test_i18n_negotiation() {
  function runTests(expected) {
    let _ = browser.i18n.getMessage.bind(browser.i18n);

    browser.test.assertEq(expected, _("foo"), "Got expected message");
  }

  let extensionData = {
    manifest: {
      default_locale: "en_US",

      content_scripts: [
        { matches: ["http://*/*/file_sample.html"], js: ["content.js"] },
      ],
    },

    files: {
      "_locales/en_US/messages.json": {
        foo: {
          message: "English.",
          description: "foo",
        },
      },

      "_locales/jp/messages.json": {
        foo: {
          message: "\u65e5\u672c\u8a9e",
          description: "foo",
        },
      },

      "content.js":
        "new " +
        function (runTestsFn) {
          browser.test.onMessage.addListener(expected => {
            runTestsFn(expected);

            browser.test.sendMessage("content-script-finished");
          });
          browser.test.sendMessage("content-ready");
        } +
        `(${runTests})`,
    },

    background:
      "new " +
      function (runTestsFn) {
        browser.test.onMessage.addListener(expected => {
          runTestsFn(expected);

          browser.test.sendMessage("background-script-finished");
        });
      } +
      `(${runTests})`,
  };

  // At the moment extension language negotiation is tied to Firefox language
  // negotiation result. That means that to test an extension in `fr`, we need
  // to mock `fr` being available in Firefox and then request it.
  //
  // In the future, we should provide some way for tests to decouple their
  // language selection from that of Firefox.
  Services.locale.availableLocales = ["en-US", "fr", "jp"];

  let contentPage = await ExtensionTestUtils.loadContentPage(
    `${BASE_URL}/file_sample.html`
  );

  for (let [lang, msg] of [
    ["en-US", "English."],
    ["jp", "\u65e5\u672c\u8a9e"],
  ]) {
    Services.locale.requestedLocales = [lang];

    let extension = ExtensionTestUtils.loadExtension(extensionData);
    await extension.startup();
    await extension.awaitMessage("content-ready");

    extension.sendMessage(msg);
    await extension.awaitMessage("background-script-finished");
    await extension.awaitMessage("content-script-finished");

    await extension.unload();
  }
  Services.locale.requestedLocales = originalReqLocales;

  await contentPage.close();
});

add_task(async function test_get_accept_languages() {
  function checkResults(source, results, expected) {
    browser.test.assertEq(
      expected.length,
      results.length,
      `got expected number of languages in ${source}`
    );
    results.forEach((lang, index) => {
      browser.test.assertEq(
        expected[index],
        lang,
        `got expected language in ${source}`
      );
    });
  }

  function background(checkResultsFn) {
    browser.test.onMessage.addListener(([msg, expected]) => {
      browser.i18n.getAcceptLanguages().then(results => {
        checkResultsFn("background", results, expected);

        browser.test.sendMessage("background-done");
      });
    });
  }

  function content(checkResultsFn) {
    browser.test.onMessage.addListener(([msg, expected]) => {
      browser.i18n.getAcceptLanguages().then(results => {
        checkResultsFn("contentScript", results, expected);

        browser.test.sendMessage("content-done");
      });
    });
    browser.test.sendMessage("content-loaded");
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      content_scripts: [
        {
          matches: ["http://*/*/file_sample.html"],
          run_at: "document_start",
          js: ["content_script.js"],
        },
      ],
    },

    background: `(${background})(${checkResults})`,

    files: {
      "content_script.js": `(${content})(${checkResults})`,
    },
  });

  let contentPage = await ExtensionTestUtils.loadContentPage(
    `${BASE_URL}/file_sample.html`
  );

  await extension.startup();
  await extension.awaitMessage("content-loaded");

  // TODO bug 1765375: ", en" is missing on Android.
  let expectedLangs =
    AppConstants.platform == "android" ? ["en-US"] : ["en-US", "en"];
  extension.sendMessage(["expect-results", expectedLangs]);
  await extension.awaitMessage("background-done");
  await extension.awaitMessage("content-done");

  expectedLangs = ["en-US", "en", "fr-CA", "fr"];
  Preferences.set("intl.accept_languages", expectedLangs.toString());
  extension.sendMessage(["expect-results", expectedLangs]);
  await extension.awaitMessage("background-done");
  await extension.awaitMessage("content-done");
  Preferences.reset("intl.accept_languages");

  await contentPage.close();

  await extension.unload();
});

add_task(async function test_get_ui_language() {
  function getResults() {
    return {
      getUILanguage: browser.i18n.getUILanguage(),
      getMessage: browser.i18n.getMessage("@@ui_locale"),
    };
  }

  function checkResults(source, results, expected) {
    browser.test.assertEq(
      expected,
      results.getUILanguage,
      `Got expected getUILanguage result in ${source}`
    );
    browser.test.assertEq(
      expected,
      results.getMessage,
      `Got expected getMessage result in ${source}`
    );
  }

  function background(getResultsFn, checkResultsFn) {
    browser.test.onMessage.addListener(([msg, expected]) => {
      checkResultsFn("background", getResultsFn(), expected);

      browser.test.sendMessage("background-done");
    });
  }

  function content(getResultsFn, checkResultsFn) {
    browser.test.onMessage.addListener(([msg, expected]) => {
      checkResultsFn("contentScript", getResultsFn(), expected);

      browser.test.sendMessage("content-done");
    });
    browser.test.sendMessage("content-loaded");
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      content_scripts: [
        {
          matches: ["http://*/*/file_sample.html"],
          run_at: "document_start",
          js: ["content_script.js"],
        },
      ],
    },

    background: `(${background})(${getResults}, ${checkResults})`,

    files: {
      "content_script.js": `(${content})(${getResults}, ${checkResults})`,
    },
  });

  let contentPage = await ExtensionTestUtils.loadContentPage(
    `${BASE_URL}/file_sample.html`
  );

  await extension.startup();
  await extension.awaitMessage("content-loaded");

  extension.sendMessage(["expect-results", "en-US"]);

  await extension.awaitMessage("background-done");
  await extension.awaitMessage("content-done");

  // We don't currently have a good way to mock this.
  if (false) {
    Services.locale.requestedLocales = ["he"];

    extension.sendMessage(["expect-results", "he"]);

    await extension.awaitMessage("background-done");
    await extension.awaitMessage("content-done");
  }

  await contentPage.close();

  await extension.unload();
});

add_task(async function test_detect_language() {
  const af_string =
    " aam skukuza die naam beteken hy wat skoonvee of hy wat alles onderstebo keer wysig " +
    "bosveldkampe boskampe is kleiner afgeleë ruskampe wat oor min fasiliteite beskik daar is geen restaurante " +
    "of winkels nie en slegs oornagbesoekers word toegelaat bateleur";
  // String with intermixed French/English text
  const fr_en_string =
    "France is the largest country in Western Europe and the third-largest in Europe as a whole. " +
    "A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter " +
    "Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France, " +
    "Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus." +
    "Motoring events began soon after the construction of the first successful gasoline-fueled automobiles. The quick brown fox jumped over the lazy dog";

  function checkResult(source, result, expected) {
    browser.test.assertEq(
      expected.isReliable,
      result.isReliable,
      "result.confident is true"
    );
    browser.test.assertEq(
      expected.languages.length,
      result.languages.length,
      `result.languages contains the expected number of languages in ${source}`
    );
    expected.languages.forEach((lang, index) => {
      browser.test.assertEq(
        lang.percentage,
        result.languages[index].percentage,
        `element ${index} of result.languages array has the expected percentage in ${source}`
      );
      browser.test.assertEq(
        lang.language,
        result.languages[index].language,
        `element ${index} of result.languages array has the expected language in ${source}`
      );
    });
  }

  function backgroundScript(checkResultFn) {
    browser.test.onMessage.addListener(([msg, expected]) => {
      browser.i18n.detectLanguage(msg).then(result => {
        checkResultFn("background", result, expected);
        browser.test.sendMessage("background-done");
      });
    });
  }

  function content(checkResultFn) {
    browser.test.onMessage.addListener(([msg, expected]) => {
      browser.i18n.detectLanguage(msg).then(result => {
        checkResultFn("contentScript", result, expected);
        browser.test.sendMessage("content-done");
      });
    });
    browser.test.sendMessage("content-loaded");
  }

  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      content_scripts: [
        {
          matches: ["http://*/*/file_sample.html"],
          run_at: "document_start",
          js: ["content_script.js"],
        },
      ],
    },

    background: `(${backgroundScript})(${checkResult})`,

    files: {
      "content_script.js": `(${content})(${checkResult})`,
    },
  });

  let contentPage = await ExtensionTestUtils.loadContentPage(
    `${BASE_URL}/file_sample.html`
  );

  await extension.startup();
  await extension.awaitMessage("content-loaded");

  let expected = {
    isReliable: true,
    languages: [
      {
        language: "fr",
        percentage: 67,
      },
      {
        language: "en",
        percentage: 32,
      },
    ],
  };
  extension.sendMessage([fr_en_string, expected]);
  await extension.awaitMessage("background-done");
  await extension.awaitMessage("content-done");

  expected = {
    isReliable: true,
    languages: [
      {
        language: "af",
        percentage: 99,
      },
    ],
  };
  extension.sendMessage([af_string, expected]);
  await extension.awaitMessage("background-done");
  await extension.awaitMessage("content-done");

  await contentPage.close();

  await extension.unload();
});
