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

"use strict";

/**
 * Tests the behavior of the debugger statement.
 */

// Use distinct origins in order to use distinct processes when fission is enabled
const TAB_URL = URL_ROOT_COM + "doc_inline-debugger-statement.html";
const IFRAME_URL = URL_ROOT_ORG + "doc_inline-debugger-statement.html";

add_task(async () => {
  const tab = await addTab(TAB_URL);
  const tabBrowsingContext = tab.linkedBrowser.browsingContext;

  const iframeBrowsingContext = await SpecialPowers.spawn(
    tabBrowsingContext,
    [IFRAME_URL],
    async function(url) {
      const iframe = content.document.createElement("iframe");
      const onLoad = new Promise(r =>
        iframe.addEventListener("load", r, { once: true })
      );
      iframe.src = url;
      content.document.body.appendChild(iframe);
      await onLoad;
      return iframe.browsingContext;
    }
  );

  const target = await TargetFactory.forTab(tab);
  await target.attach();
  const { client } = target;

  info("## Test debugger statement against the top level tab document");
  // This function, by calling the debugger statement function, will bump the increment
  const threadFront = await testEarlyDebuggerStatement(
    client,
    tabBrowsingContext,
    target
  );
  await testDebuggerStatement(client, tabBrowsingContext, threadFront, 1);

  info("## Test debugger statement againt a distinct origin iframe");
  if (isFissionEnabled()) {
    // We have to use the watcher in order to create the frame target
    // and also have to attach to it in order to later be able to
    // create the thread front
    const watcher = await target.getWatcher();
    await watcher.watchTargets("frame");
    const iframeTarget = await target.getBrowsingContextTarget(
      iframeBrowsingContext.id
    );
    await iframeTarget.attach();

    // This function, by calling the debugger statement function, will bump the increment
    const iframeThreadFront = await testEarlyDebuggerStatement(
      client,
      iframeBrowsingContext,
      iframeTarget
    );
    await testDebuggerStatement(
      client,
      iframeBrowsingContext,
      iframeThreadFront,
      1
    );
  } else {
    // But in this case, the increment will be 0 as the previous call to `testEarlyDebuggerStatement`
    // bumped the tab's document increment and not the iframe's one.
    await testDebuggerStatement(client, iframeBrowsingContext, threadFront, 0);
  }

  await target.destroy();
});

async function testEarlyDebuggerStatement(
  client,
  browsingContext,
  targetFront
) {
  const onPaused = function(packet) {
    ok(false, "Pause shouldn't be called before we've attached!");
  };

  // using the DevToolsClient to listen to the pause packet, as the
  // threadFront is not yet attached.
  client.on("paused", onPaused);

  // This should continue without nesting an event loop and calling
  // the onPaused hook, because we haven't attached yet.
  const increment = await SpecialPowers.spawn(
    browsingContext,
    [],
    async function() {
      content.wrappedJSObject.runDebuggerStatement();
      // Pile up another setTimeout in order to guarantee that the other one ran
      await new Promise(r => content.setTimeout(r));
      return content.wrappedJSObject.increment;
    }
  );
  is(increment, 1, "As the thread wasn't paused, setTimeout worked");

  client.off("paused", onPaused);

  // Now attach and resume...
  const threadFront = await targetFront.attachThread();
  await threadFront.resume();
  ok(true, "Pause wasn't called before we've attached.");

  return threadFront;
}

async function testDebuggerStatement(
  client,
  browsingContext,
  threadFront,
  incrementOriginalValue
) {
  const onPaused = threadFront.once("paused");

  // Reach around the debugging protocol and execute the debugger statement.
  // Not that this will be paused and spawn will only resolve once
  // the thread will be resumed
  const onResumed = SpecialPowers.spawn(browsingContext, [], function() {
    content.wrappedJSObject.runDebuggerStatement();
  });

  info("Waiting for paused event");
  await onPaused;
  ok(true, "The pause handler was triggered on a debugger statement.");

  // Pile up another setTimeout in order to guarantee that the other did not run
  /* eslint-disable-next-line mozilla/no-arbitrary-setTimeout */
  await new Promise(r => setTimeout(r, 1000));

  let increment = await SpecialPowers.spawn(
    browsingContext,
    [],
    async function() {
      return content.wrappedJSObject.increment;
    }
  );
  is(
    increment,
    incrementOriginalValue,
    "setTimeout are frozen while the thread is paused"
  );

  await threadFront.resume();
  await onResumed;

  increment = await SpecialPowers.spawn(browsingContext, [], async function() {
    // Pile up another setTimeout in order to guarantee that the other did run
    await new Promise(r => content.setTimeout(r));
    return content.wrappedJSObject.increment;
  });
  is(
    increment,
    incrementOriginalValue + 1,
    "setTimeout are resumed after the thread is resumed"
  );
}
