/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */

requestLongerTimeout(2);

const gHttpTestRoot = "https://example.com/browser/dom/base/test/";

/**
 * Enable local telemetry recording for the duration of the tests.
 */
var gOldContentCanRecord = false;
var gOldParentCanRecord = false;
add_task(async function test_initialize() {
  let Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
    Ci.nsITelemetry
  );
  gOldParentCanRecord = Telemetry.canRecordExtended;
  Telemetry.canRecordExtended = true;

  await SpecialPowers.pushPrefEnv({
    set: [
      // Because canRecordExtended is a per-process variable, we need to make sure
      // that all of the pages load in the same content process. Limit the number
      // of content processes to at most 1 (or 0 if e10s is off entirely).
      ["dom.ipc.processCount", 1],
      ["layout.css.use-counters.enabled", true],
      ["layout.css.use-counters-unimplemented.enabled", true],
    ],
  });

  gOldContentCanRecord = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    function () {
      let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
        Ci.nsITelemetry
      );
      let old = telemetry.canRecordExtended;
      telemetry.canRecordExtended = true;
      return old;
    }
  );
  info("canRecord for content: " + gOldContentCanRecord);
});

add_task(async function () {
  const TESTS = [
    // Check that use counters are incremented by SVGs loaded directly in iframes.
    {
      type: "iframe",
      filename: "file_use_counter_svg_getElementById.svg",
      counters: [{ name: "SVGSVGELEMENT_GETELEMENTBYID" }],
    },
    {
      type: "iframe",
      filename: "file_use_counter_svg_currentScale.svg",
      counters: [
        { name: "SVGSVGELEMENT_CURRENTSCALE_getter" },
        { name: "SVGSVGELEMENT_CURRENTSCALE_setter" },
      ],
    },

    {
      type: "iframe",
      filename: "file_use_counter_style.html",
      counters: [
        // Check for longhands.
        { name: "CSS_PROPERTY_BackgroundImage" },
        // Check for shorthands.
        { name: "CSS_PROPERTY_Padding" },
        // Check for aliases.
        { name: "CSS_PROPERTY_MozTransform" },
        // Check for counted unknown properties.
        { name: "CSS_PROPERTY_WebkitPaddingStart" },
      ],
    },

    // Check that even loads from the imglib cache update use counters.  The
    // images should still be there, because we just loaded them in the last
    // set of tests.  But we won't get updated counts for the document
    // counters, because we won't be re-parsing the SVG documents.
    {
      type: "iframe",
      filename: "file_use_counter_svg_getElementById.svg",
      counters: [{ name: "SVGSVGELEMENT_GETELEMENTBYID" }],
      check_documents: false,
    },
    {
      type: "iframe",
      filename: "file_use_counter_svg_currentScale.svg",
      counters: [
        { name: "SVGSVGELEMENT_CURRENTSCALE_getter" },
        { name: "SVGSVGELEMENT_CURRENTSCALE_setter" },
      ],
      check_documents: false,
    },

    // Check that use counters are incremented by SVGs loaded as images.
    // Note that SVG images are not permitted to execute script, so we can only
    // check for properties here.
    {
      type: "img",
      filename: "file_use_counter_svg_getElementById.svg",
      counters: [{ name: "CSS_PROPERTY_Fill" }],
    },
    {
      type: "img",
      filename: "file_use_counter_svg_currentScale.svg",
      counters: [{ name: "CSS_PROPERTY_Fill" }],
    },

    // Check that use counters are incremented by directly loading SVGs
    // that reference patterns defined in another SVG file.
    {
      type: "direct",
      filename: "file_use_counter_svg_fill_pattern.svg",
      counters: [{ name: "CSS_PROPERTY_FillOpacity", xfail: true }],
    },

    // Check that use counters are incremented by directly loading SVGs
    // that reference patterns defined in the same file or in data: URLs.
    {
      type: "direct",
      filename: "file_use_counter_svg_fill_pattern_internal.svg",
      counters: [{ name: "CSS_PROPERTY_FillOpacity" }],
    },

    // Check that use counters are incremented in a display:none iframe.
    {
      type: "undisplayed-iframe",
      filename: "file_use_counter_svg_currentScale.svg",
      counters: [{ name: "SVGSVGELEMENT_CURRENTSCALE_getter" }],
    },

    // Check that a document that comes out of the bfcache reports any new use
    // counters recorded on it.
    {
      type: "direct",
      filename: "file_use_counter_bfcache.html",
      waitForExplicitFinish: true,
      counters: [{ name: "SVGSVGELEMENT_GETELEMENTBYID" }],
    },

    // // data: URLs don't correctly propagate to their referring document yet.
    // {
    //   type: "direct",
    //   filename: "file_use_counter_svg_fill_pattern_data.svg",
    //   counters: [
    //     { name: "PROPERTY_FILL_OPACITY" },
    //   ],
    // },
  ];

  for (let test of TESTS) {
    let file = test.filename;
    info(`checking ${file} (${test.type})`);

    let newTab = BrowserTestUtils.addTab(gBrowser, "about:blank");
    gBrowser.selectedTab = newTab;
    newTab.linkedBrowser.stop();

    // Hold on to the current values of the telemetry histograms we're
    // interested in.
    let before = await grabHistogramsFromContent(
      test.counters.map(c => c.name)
    );

    // Load the test file in the new tab, either directly or via
    // file_use_counter_outer{,_display_none}.html, depending on the test type.
    let url, targetElement;
    switch (test.type) {
      case "iframe":
        url = gHttpTestRoot + "file_use_counter_outer.html";
        targetElement = "content";
        break;
      case "undisplayed-iframe":
        url = gHttpTestRoot + "file_use_counter_outer_display_none.html";
        targetElement = "content";
        break;
      case "img":
        url = gHttpTestRoot + "file_use_counter_outer.html";
        targetElement = "display";
        break;
      case "direct":
        url = gHttpTestRoot + file;
        targetElement = null;
        break;
      default:
        throw `unexpected type ${test.type}`;
    }

    BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
    await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);

    if (test.waitForExplicitFinish) {
      if (test.type != "direct") {
        throw new Error(
          `cannot use waitForExplicitFinish with test type ${test.type}`
        );
      }

      // Wait until the tab changes its hash to indicate it has finished.
      await BrowserTestUtils.waitForLocationChange(gBrowser, url + "#finished");
    }

    if (targetElement) {
      // Inject our desired file into the target element of the newly-loaded page.
      await SpecialPowers.spawn(
        gBrowser.selectedBrowser,
        [{ file, targetElement }],
        function (opts) {
          let target = content.document.getElementById(opts.targetElement);
          target.src = opts.file;

          return new Promise(resolve => {
            let listener = event => {
              event.target.removeEventListener("load", listener, true);
              resolve();
            };
            target.addEventListener("load", listener, true);
          });
        }
      );
    }

    // Tear down the page.
    let tabClosed = BrowserTestUtils.waitForTabClosing(newTab);
    gBrowser.removeTab(newTab);
    await tabClosed;

    // Grab histograms again.
    let after = await grabHistogramsFromContent(
      test.counters.map(c => c.name),
      before.sentinel
    );

    // Compare before and after.
    for (let counter of test.counters) {
      let name = counter.name;
      let value = counter.value ?? 1;
      if (!counter.xfail) {
        is(
          after.page[name],
          before.page[name] + value,
          `page counts for ${name} after are correct`
        );
        is(
          after.document[name],
          before.document[name] + value,
          `document counts for ${name} after are correct`
        );
      }
    }

    if (test.check_documents ?? true) {
      ok(
        after.toplevel_docs >= before.toplevel_docs + 1,
        "top level destroyed document counts are correct"
      );
      // 2 documents for "img" tests: one for the outer html page containing the
      // <img> element, and one for the SVG image itself.
      ok(
        after.docs >= before.docs + (test.type == "img" ? 2 : 1),
        "destroyed document counts are correct"
      );
    }
  }
});

add_task(async function () {
  let Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
    Ci.nsITelemetry
  );
  Telemetry.canRecordExtended = gOldParentCanRecord;

  await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [{ oldCanRecord: gOldContentCanRecord }],
    async function (arg) {
      await new Promise(resolve => {
        let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
          Ci.nsITelemetry
        );
        telemetry.canRecordExtended = arg.oldCanRecord;
        resolve();
      });
    }
  );
});

async function grabHistogramsFromContent(names, prev_sentinel = null) {
  // We don't have any way to get a notification when telemetry from the
  // document that was just closed has been reported. So instead, we
  // repeatedly poll for telemetry until we see that a specific use counter
  // histogram (CSS_PROPERTY_MarkerMid, the "sentinel") that likely is not
  // used by any other document that's open has been incremented.
  let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
    Ci.nsITelemetry
  );
  let gatheredHistograms;
  return BrowserTestUtils.waitForCondition(
    function () {
      // Document use counters are reported in the content process (when e10s
      // is enabled), and page use counters are reported in the parent process.
      let snapshots = telemetry.getSnapshotForHistograms("main", false);
      let checkGet = probe => {
        // When e10s is disabled, all histograms are reported in the parent
        // process.  Otherwise, all page use counters are reported in the parent
        // and document use counters are reported in the content process.
        let process =
          !Services.appinfo.browserTabsRemoteAutostart ||
          probe.endsWith("_PAGE") ||
          probe == "TOP_LEVEL_CONTENT_DOCUMENTS_DESTROYED"
            ? "parent"
            : "content";
        return snapshots[process][probe] ? snapshots[process][probe].sum : 0;
      };
      let page = Object.fromEntries(
        names.map(name => [name, checkGet(`USE_COUNTER2_${name}_PAGE`)])
      );
      let document = Object.fromEntries(
        names.map(name => [name, checkGet(`USE_COUNTER2_${name}_DOCUMENT`)])
      );
      gatheredHistograms = {
        page,
        document,
        docs: checkGet("CONTENT_DOCUMENTS_DESTROYED"),
        toplevel_docs: checkGet("TOP_LEVEL_CONTENT_DOCUMENTS_DESTROYED"),
        sentinel: {
          doc: checkGet("USE_COUNTER2_CSS_PROPERTY_MarkerMid_DOCUMENT"),
          page: checkGet("USE_COUNTER2_CSS_PROPERTY_MarkerMid_PAGE"),
        },
      };
      let sentinelChanged =
        !prev_sentinel ||
        (prev_sentinel.doc != gatheredHistograms.sentinel.doc &&
          prev_sentinel.page != gatheredHistograms.sentinel.page);
      return sentinelChanged;
    },
    "grabHistogramsFromContent",
    100,
    Infinity
  ).then(
    () => gatheredHistograms,
    function (msg) {
      throw msg;
    }
  );
}
