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

"use strict";

// Test the TargetCommand API around processes

const TEST_URL =
  "data:text/html;charset=utf-8," + encodeURIComponent(`<div id="test"></div>`);

add_task(async function () {
  // Enabled fission's pref as the TargetCommand is almost disabled without it
  await pushPref("devtools.browsertoolbox.scope", "everything");
  // We use a commands object for the main process
  await pushPref("devtools.chrome.enabled", true);
  // Disable the preloaded process as it gets created lazily and may interfere
  // with process count assertions
  await pushPref("dom.ipc.processPrelaunch.enabled", false);
  // This preference helps destroying the content process when we close the tab
  await pushPref("dom.ipc.keepProcessesAlive.web", 1);

  const commands = await CommandsFactory.forMainProcess();
  const targetCommand = commands.targetCommand;
  await targetCommand.startListening();

  await testProcesses(targetCommand, targetCommand.targetFront);

  targetCommand.destroy();
  // Wait for all the targets to be fully attached so we don't have pending requests.
  await Promise.all(
    targetCommand.getAllTargets(targetCommand.ALL_TYPES).map(t => t.initialized)
  );

  await commands.destroy();
});

add_task(async function () {
  const commands = await CommandsFactory.forMainProcess();
  const targetCommand = commands.targetCommand;
  await targetCommand.startListening();

  const created = [];
  const destroyed = [];
  const onAvailable = ({ targetFront }) => {
    created.push(targetFront);
  };
  const onDestroyed = ({ targetFront }) => {
    destroyed.push(targetFront);
  };
  await targetCommand.watchTargets({
    types: [targetCommand.TYPES.PROCESS],
    onAvailable,
    onDestroyed,
  });
  Assert.greater(created.length, 1, "We get many content process targets");

  targetCommand.stopListening();

  await waitFor(
    () => created.length == destroyed.length,
    "Wait for the destruction of all content process targets when calling stopListening"
  );
  is(
    created.length,
    destroyed.length,
    "Got notification of destruction for all previously reported targets"
  );

  targetCommand.destroy();
  // Wait for all the targets to be fully attached so we don't have pending requests.
  await Promise.all(
    targetCommand.getAllTargets(targetCommand.ALL_TYPES).map(t => t.initialized)
  );

  await commands.destroy();
});

async function testProcesses(targetCommand, target) {
  info("Test TargetCommand against processes");
  const { TYPES } = targetCommand;

  // Note that ppmm also includes the parent process, which is considered as a frame rather than a process
  const originalProcessesCount = Services.ppmm.childCount - 1;
  const processes = await targetCommand.getAllTargets([TYPES.PROCESS]);
  is(
    processes.length,
    originalProcessesCount,
    "Get a target for all content processes"
  );

  const processes2 = await targetCommand.getAllTargets([TYPES.PROCESS]);
  is(
    processes2.length,
    originalProcessesCount,
    "retrieved the same number of processes"
  );
  function sortFronts(f1, f2) {
    return f1.actorID < f2.actorID;
  }
  processes.sort(sortFronts);
  processes2.sort(sortFronts);
  for (let i = 0; i < processes.length; i++) {
    is(processes[i], processes2[i], `process ${i} targets are the same`);
  }

  // Assert that watchTargets will call the create callback for all existing frames
  const targets = new Set();

  const pidRegExp = /^\d+$/;

  const onAvailable = ({ targetFront }) => {
    if (targets.has(targetFront)) {
      ok(false, "The same target is notified multiple times via onAvailable");
    }
    is(
      targetFront.targetType,
      TYPES.PROCESS,
      "We are only notified about process targets"
    );
    ok(
      targetFront == target ? targetFront.isTopLevel : !targetFront.isTopLevel,
      "isTopLevel property is correct"
    );
    ok(
      pidRegExp.test(targetFront.processID),
      `Target has processID of expected shape (${targetFront.processID})`
    );
    targets.add(targetFront);
  };
  const onDestroyed = ({ targetFront }) => {
    if (!targets.has(targetFront)) {
      ok(
        false,
        "A target is declared destroyed via onDestroy without being notified via onAvailable"
      );
    }
    is(
      targetFront.targetType,
      TYPES.PROCESS,
      "We are only notified about process targets"
    );
    ok(
      !targetFront.isTopLevel,
      "We are never notified about the top level target destruction"
    );
    targets.delete(targetFront);
  };
  await targetCommand.watchTargets({
    types: [TYPES.PROCESS],
    onAvailable,
    onDestroyed,
  });
  is(
    targets.size,
    originalProcessesCount,
    "retrieved the same number of processes via watchTargets"
  );
  for (let i = 0; i < processes.length; i++) {
    ok(
      targets.has(processes[i]),
      `process ${i} targets are the same via watchTargets`
    );
  }

  const previousTargets = new Set(targets);
  // Assert that onAvailable is called for processes created *after* the call to watchTargets
  const onProcessCreated = new Promise(resolve => {
    const onAvailable2 = ({ targetFront }) => {
      if (previousTargets.has(targetFront)) {
        return;
      }
      targetCommand.unwatchTargets({
        types: [TYPES.PROCESS],
        onAvailable: onAvailable2,
      });
      resolve(targetFront);
    };
    targetCommand.watchTargets({
      types: [TYPES.PROCESS],
      onAvailable: onAvailable2,
    });
  });
  info("open new tab in new process");
  const tab1 = await BrowserTestUtils.openNewForegroundTab({
    gBrowser,
    url: TEST_URL,
    forceNewProcess: true,
  });
  info("wait for process target to be created");
  const createdTarget = await onProcessCreated;
  // For some reason, creating a new tab purges processes created from previous tests
  // so it is not reasonable to assert the size of `targets` as it may be lower than expected.
  ok(targets.has(createdTarget), "The new tab process is in the list");

  const processCountAfterTabOpen = targets.size;

  // Assert that onDestroy is called for destroyed processes
  const onProcessDestroyed = new Promise(resolve => {
    const onAvailable3 = () => {};
    const onDestroyed3 = ({ targetFront }) => {
      resolve(targetFront);
      targetCommand.unwatchTargets({
        types: [TYPES.PROCESS],
        onAvailable: onAvailable3,
        onDestroyed: onDestroyed3,
      });
    };
    targetCommand.watchTargets({
      types: [TYPES.PROCESS],
      onAvailable: onAvailable3,
      onDestroyed: onDestroyed3,
    });
  });

  BrowserTestUtils.removeTab(tab1);

  const destroyedTarget = await onProcessDestroyed;
  is(
    targets.size,
    processCountAfterTabOpen - 1,
    "The closed tab's process has been reported as destroyed"
  );
  ok(
    !targets.has(destroyedTarget),
    "The destroyed target is no longer in the list"
  );
  is(
    destroyedTarget,
    createdTarget,
    "The destroyed target is the one that has been reported as created"
  );

  targetCommand.unwatchTargets({
    types: [TYPES.PROCESS],
    onAvailable,
    onDestroyed,
  });

  // Ensure that getAllTargets still works after the call to unwatchTargets
  const processes3 = await targetCommand.getAllTargets([TYPES.PROCESS]);
  is(
    processes3.length,
    processCountAfterTabOpen - 1,
    "getAllTargets reports a new target"
  );
}
