waitForExplicitFinish();
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");

var pageSource =
  "<html><body>" +
  '<img id="testImg" src="' +
  TESTROOT +
  'big.png">' +
  "</body></html>";

var oldDiscardingPref, oldTab, newTab;
var prefBranch = Services.prefs.getBranch("image.mem.");

var gWaitingForDiscard = false;
var gScriptedObserver;
var gClonedRequest;

function ImageObserver(decodeCallback, discardCallback) {
  this.decodeComplete = function onDecodeComplete(aRequest) {
    decodeCallback();
  };

  this.discard = function onDiscard(request) {
    if (!gWaitingForDiscard) {
      return;
    }

    this.synchronous = false;
    discardCallback();
  };

  this.synchronous = true;
}

function currentRequest() {
  let img = gBrowser
    .getBrowserForTab(newTab)
    .contentWindow.document.getElementById("testImg");
  return img.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
}

function isImgDecoded() {
  let request = currentRequest();
  return !!(request.imageStatus & Ci.imgIRequest.STATUS_DECODE_COMPLETE);
}

// Ensure that the image is decoded by drawing it to a canvas.
function forceDecodeImg() {
  let doc = gBrowser.getBrowserForTab(newTab).contentWindow.document;
  let img = doc.getElementById("testImg");
  let canvas = doc.createElement("canvas");
  let ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);
}

function runAfterAsyncEvents(aCallback) {
  function handlePostMessage(aEvent) {
    if (aEvent.data == "next") {
      window.removeEventListener("message", handlePostMessage);
      aCallback();
    }
  }

  window.addEventListener("message", handlePostMessage);

  // We'll receive the 'message' event after everything else that's currently in
  // the event queue (which is a stronger guarantee than setTimeout, because
  // setTimeout events may be coalesced). This lets us ensure that we run
  // aCallback *after* any asynchronous events are delivered.
  window.postMessage("next", "*");
}

function test() {
  // Enable the discarding pref.
  oldDiscardingPref = prefBranch.getBoolPref("discardable");
  prefBranch.setBoolPref("discardable", true);

  // Create and focus a new tab.
  oldTab = gBrowser.selectedTab;
  newTab = BrowserTestUtils.addTab(gBrowser, "data:text/html," + pageSource);
  gBrowser.selectedTab = newTab;

  // Run step2 after the tab loads.
  gBrowser.getBrowserForTab(newTab).addEventListener("pageshow", step2);
}

function step2() {
  // Create the image observer.
  var observer = new ImageObserver(
    () => runAfterAsyncEvents(step3), // DECODE_COMPLETE
    () => runAfterAsyncEvents(step5)
  ); // DISCARD
  gScriptedObserver = Cc["@mozilla.org/image/tools;1"]
    .getService(Ci.imgITools)
    .createScriptedObserver(observer);

  // Clone the current imgIRequest with our new observer.
  var request = currentRequest();
  gClonedRequest = request.clone(gScriptedObserver);

  // Check that the image is decoded.
  forceDecodeImg();

  // The DECODE_COMPLETE notification is delivered asynchronously. ImageObserver will
  // eventually call step3.
}

function step3() {
  ok(isImgDecoded(), "Image should initially be decoded.");

  // Focus the old tab, then fire a memory-pressure notification.  This should
  // cause the decoded image in the new tab to be discarded.
  gBrowser.selectedTab = oldTab;

  // Allow time to process the tab change.
  runAfterAsyncEvents(step4);
}

function step4() {
  gWaitingForDiscard = true;

  var os = Services.obs;
  os.notifyObservers(null, "memory-pressure", "heap-minimize");

  // The DISCARD notification is delivered asynchronously. ImageObserver will
  // eventually call step5. (Or else, sadly, the test will time out.)
}

function step5() {
  ok(true, "Image should be discarded.");

  // And we're done.
  gBrowser.removeTab(newTab);
  prefBranch.setBoolPref("discardable", oldDiscardingPref);

  gClonedRequest.cancelAndForgetObserver(0);

  finish();
}
