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

/* This list allows pre-existing or 'unfixable' issues to remain, while we
 * detect newly occurring issues in shipping files. It is a list of objects
 * specifying conditions under which an error should be ignored.
 *
 * As each issue is found in the exceptions list, it is removed from the list.
 * At the end of the test, there is an assertion that all items have been
 * removed from the exceptions list, thus ensuring there are no stale
 * entries. */
let gExceptionsList = [
  {
    file: "layout_errors.properties",
    key: "ImageMapRectBoundsError",
    type: "double-quote",
  },
  {
    file: "layout_errors.properties",
    key: "ImageMapCircleWrongNumberOfCoords",
    type: "double-quote",
  },
  {
    file: "layout_errors.properties",
    key: "ImageMapCircleNegativeRadius",
    type: "double-quote",
  },
  {
    file: "layout_errors.properties",
    key: "ImageMapPolyWrongNumberOfCoords",
    type: "double-quote",
  },
  {
    file: "layout_errors.properties",
    key: "ImageMapPolyOddNumberOfCoords",
    type: "double-quote",
  },
  {
    file: "dom.properties",
    key: "ImportMapExternalNotSupported",
    type: "single-quote",
  },
  // dom.properties is packaged twice so we need to have two exceptions for this string.
  {
    file: "dom.properties",
    key: "ImportMapExternalNotSupported",
    type: "single-quote",
  },
  {
    file: "dom.properties",
    key: "MathML_DeprecatedMathVariantWarning",
    type: "single-quote",
  },
  // dom.properties is packaged twice so we need to have two exceptions for this string.
  {
    file: "dom.properties",
    key: "MathML_DeprecatedMathVariantWarning",
    type: "single-quote",
  },
  // These error messages contain references to the CSP keywords like 'unsafe-eval',
  // and those keywords contain actual single-quotes: https://w3c.github.io/webappsec-csp/#grammardef-keyword-source
  {
    file: "csp.properties",
    key: "CSPInlineStyleViolation2",
    type: "single-quote",
  },
  {
    file: "csp.properties",
    key: "CSPROInlineStyleViolation2",
    type: "single-quote",
  },
  {
    file: "csp.properties",
    key: "CSPInlineScriptViolation2",
    type: "single-quote",
  },
  {
    file: "csp.properties",
    key: "CSPROInlineScriptViolation2",
    type: "single-quote",
  },
  {
    file: "csp.properties",
    key: "CSPEventHandlerScriptViolation2",
    type: "single-quote",
  },
  {
    file: "csp.properties",
    key: "CSPROEventHandlerScriptViolation2",
    type: "single-quote",
  },
  {
    file: "csp.properties",
    key: "CSPEvalScriptViolation",
    type: "single-quote",
  },
  {
    file: "csp.properties",
    key: "CSPROEvalScriptViolation",
    type: "single-quote",
  },
  {
    file: "csp.properties",
    key: "CSPWasmEvalScriptViolation",
    type: "single-quote",
  },
  {
    file: "csp.properties",
    key: "CSPROWasmEvalScriptViolation",
    type: "single-quote",
  },
];

/**
 * Check if an error should be ignored due to matching one of the exceptions
 * defined in gExceptionsList.
 *
 * @param filepath The URI spec of the locale file
 * @param key The key of the entity that is being checked
 * @param type The type of error that has been found
 * @return true if the error should be ignored, false otherwise.
 */
function ignoredError(filepath, key, type) {
  for (let index in gExceptionsList) {
    let exceptionItem = gExceptionsList[index];
    if (
      filepath.endsWith(exceptionItem.file) &&
      key == exceptionItem.key &&
      type == exceptionItem.type
    ) {
      gExceptionsList.splice(index, 1);
      return true;
    }
  }
  return false;
}

function testForError(filepath, key, str, pattern, type, helpText) {
  if (str.match(pattern) && !ignoredError(filepath, key, type)) {
    ok(false, `${filepath} with key=${key} has a misused ${type}. ${helpText}`);
  }
}

function testForErrors(filepath, key, str) {
  testForError(
    filepath,
    key,
    str,
    /(\w|^)'\w/,
    "apostrophe",
    "Strings with apostrophes should use foo\u2019s instead of foo's."
  );
  testForError(
    filepath,
    key,
    str,
    /\w\u2018\w/,
    "incorrect-apostrophe",
    "Strings with apostrophes should use foo\u2019s instead of foo\u2018s."
  );
  testForError(
    filepath,
    key,
    str,
    /'.+'/,
    "single-quote",
    "Single-quoted strings should use Unicode \u2018foo\u2019 instead of 'foo'."
  );
  testForError(
    filepath,
    key,
    str,
    /"/,
    "double-quote",
    'Double-quoted strings should use Unicode \u201cfoo\u201d instead of "foo".'
  );
  testForError(
    filepath,
    key,
    str,
    /\.\.\./,
    "ellipsis",
    "Strings with an ellipsis should use the Unicode \u2026 character instead of three periods."
  );
}

async function getAllTheFiles(extension) {
  let appDirGreD = Services.dirsvc.get("GreD", Ci.nsIFile);
  let appDirXCurProcD = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
  if (appDirGreD.contains(appDirXCurProcD)) {
    return generateURIsFromDirTree(appDirGreD, [extension]);
  }
  if (appDirXCurProcD.contains(appDirGreD)) {
    return generateURIsFromDirTree(appDirXCurProcD, [extension]);
  }
  let urisGreD = await generateURIsFromDirTree(appDirGreD, [extension]);
  let urisXCurProcD = await generateURIsFromDirTree(appDirXCurProcD, [
    extension,
  ]);
  return Array.from(new Set(urisGreD.concat(urisXCurProcD)));
}

add_task(async function checkAllTheProperties() {
  // This asynchronously produces a list of URLs (sadly, mostly sync on our
  // test infrastructure because it runs against jarfiles there, and
  // our zipreader APIs are all sync)
  let uris = await getAllTheFiles(".properties");
  ok(
    uris.length,
    `Found ${uris.length} .properties files to scan for misused characters`
  );

  for (let uri of uris) {
    let bundle = Services.strings.createBundle(uri.spec);

    for (let entity of bundle.getSimpleEnumeration()) {
      testForErrors(uri.spec, entity.key, entity.value);
    }
  }
});

add_task(async function checkAllTheFluents() {
  let uris = await getAllTheFiles(".ftl");

  let domParser = new DOMParser();
  domParser.forceEnableDTD();

  for (let uri of uris) {
    let rawContents = await fetchFile(uri.spec);
    const resource = new FluentResource(rawContents);

    for (const info of resource.textElements()) {
      const key = info.attr ? `${info.id}.${info.attr}` : info.id;

      const stripped_val = domParser.parseFromString(
        "<!DOCTYPE html>" + info.text,
        "text/html"
      ).documentElement.textContent;

      testForErrors(uri.spec, key, stripped_val);
    }
  }
});

add_task(async function ensureExceptionsListIsEmpty() {
  is(gExceptionsList.length, 0, "No remaining exceptions exist");
});
