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

"use strict";

const utilityProcessTest = () => {
  return Cc["@mozilla.org/utility-process-test;1"].createInstance(
    Ci.nsIUtilityProcessTest
  );
};

const kGenericUtilitySandbox = 0;
const kGenericUtilityActor = "unknown";

// Start a generic utility process with the given array of utility actor names
// registered.
async function startUtilityProcess(actors = []) {
  info("Start a UtilityProcess");
  return utilityProcessTest().startProcess(actors);
}

// Returns an array of process infos for utility processes of the given type
// or all utility processes if actor is not defined.
async function getUtilityProcesses(actor = undefined, options = {}) {
  let procInfos = (await ChromeUtils.requestProcInfo()).children.filter(p => {
    return (
      p.type === "utility" &&
      (actor == undefined ||
        p.utilityActors.find(a => a.actorName.startsWith(actor)))
    );
  });

  if (!options?.quiet) {
    info(`Utility process infos = ${JSON.stringify(procInfos)}`);
  }
  return procInfos;
}

async function tryGetUtilityPid(actor, options = {}) {
  let process = await getUtilityProcesses(actor, options);
  if (!options?.quiet) {
    Assert.lessOrEqual(
      process.length,
      1,
      `at most one ${actor} process exists`
    );
  }
  return process[0]?.pid;
}

async function checkUtilityExists(actor) {
  info(`Looking for a running ${actor} utility process`);
  const utilityPid = await tryGetUtilityPid(actor);
  Assert.greater(utilityPid, 0, `Found ${actor} utility process ${utilityPid}`);
  return utilityPid;
}

// "Cleanly stop" a utility process.  This will never leave a crash dump file.
// preferKill will "kill" the process (e.g. SIGABRT) instead of using the
// UtilityProcessManager.
// To "crash" -- i.e. shutdown and generate a crash dump -- use
// crashSomeUtility().
async function cleanUtilityProcessShutdown(actor, preferKill = false) {
  info(`${preferKill ? "Kill" : "Clean shutdown"} Utility Process ${actor}`);

  const utilityPid = await tryGetUtilityPid(actor);
  Assert.notStrictEqual(
    utilityPid,
    undefined,
    `Must have PID for ${actor} utility process`
  );

  const utilityProcessGone = TestUtils.topicObserved(
    "ipc:utility-shutdown",
    (subject, data) => parseInt(data, 10) === utilityPid
  );

  if (preferKill) {
    SimpleTest.expectChildProcessCrash();
    info(`Kill Utility Process ${utilityPid}`);
    const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
      Ci.nsIProcessToolsService
    );
    ProcessTools.kill(utilityPid);
  } else {
    info(`Stopping Utility Process ${utilityPid}`);
    await utilityProcessTest().stopProcess(actor);
  }

  let [subject, data] = await utilityProcessGone;
  ok(
    subject instanceof Ci.nsIPropertyBag2,
    "Subject needs to be a nsIPropertyBag2 to clean up properly"
  );
  is(
    parseInt(data, 10),
    utilityPid,
    `Should match the crashed PID ${utilityPid} with ${data}`
  );

  // Make sure the process is dead, otherwise there is a risk of race for
  // writing leak logs
  utilityProcessTest().noteIntentionalCrash(utilityPid);

  ok(!subject.hasKey("dumpID"), "There should be no dumpID");
}

async function killUtilityProcesses() {
  let utilityProcesses = await getUtilityProcesses();
  for (const utilityProcess of utilityProcesses) {
    for (const actor of utilityProcess.utilityActors) {
      info(`Stopping ${actor.actorName} utility process`);
      await cleanUtilityProcessShutdown(actor.actorName, /* preferKill */ true);
    }
  }
}

function audioTestData() {
  return [
    {
      src: "small-shot.ogg",
      expectations: {
        Android: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
        Linux: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
        WINNT: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
        Darwin: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
      },
    },
    {
      src: "small-shot.mp3",
      expectations: {
        Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" },
        Linux: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
        WINNT: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
        Darwin: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
      },
    },
    {
      src: "small-shot.m4a",
      expectations: {
        // Add Android after Bug 1934009
        Linux: {
          process: "Utility Generic",
          decoder: "ffmpeg audio decoder",
        },
        WINNT: {
          process: "Utility WMF",
          decoder: "wmf audio decoder",
        },
        Darwin: {
          process: "Utility AppleMedia",
          decoder: "apple coremedia decoder",
        },
      },
    },
    {
      src: "small-shot.flac",
      expectations: {
        Android: { process: "Utility Generic", decoder: "ffvpx audio decoder" },
        Linux: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
        WINNT: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
        Darwin: {
          process: "Utility Generic",
          decoder: "ffvpx audio decoder",
        },
      },
    },
  ];
}

function audioTestDataEME() {
  return [
    {
      src: {
        audioFile:
          "https://example.com/browser/ipc/glue/test/browser/short-aac-encrypted-audio.mp4",
        sourceBuffer: "audio/mp4",
      },
      expectations: {
        Linux: {
          process: "Utility Generic",
          decoder: "ffmpeg audio decoder",
        },
        WINNT: {
          process: "Utility WMF",
          decoder: "wmf audio decoder",
        },
        Darwin: {
          process: "Utility AppleMedia",
          decoder: "apple coremedia decoder",
        },
      },
    },
  ];
}

async function addMediaTab(src) {
  const tab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
    forceNewProcess: true,
  });
  const browser = gBrowser.getBrowserForTab(tab);
  await BrowserTestUtils.browserLoaded(browser, { wantLoad: "about:blank" });
  await SpecialPowers.spawn(browser, [src], createAudioElement);
  return tab;
}

async function addMediaTabWithEME(sourceBuffer, audioFile) {
  const tab = BrowserTestUtils.addTab(
    gBrowser,
    "https://example.com/browser/",
    {
      forceNewProcess: true,
    }
  );
  const browser = gBrowser.getBrowserForTab(tab);
  await BrowserTestUtils.browserLoaded(browser);
  await SpecialPowers.spawn(
    browser,
    [sourceBuffer, audioFile],
    createAudioElementEME
  );
  return tab;
}

async function play(
  tab,
  expectUtility,
  expectDecoder,
  expectContent = false,
  expectJava = false,
  expectError = false,
  withEME = false
) {
  let browser = tab.linkedBrowser;
  return SpecialPowers.spawn(
    browser,
    [
      expectUtility,
      expectDecoder,
      expectContent,
      expectJava,
      expectError,
      withEME,
    ],
    checkAudioDecoder
  );
}

async function stop(tab) {
  let browser = tab.linkedBrowser;
  await SpecialPowers.spawn(browser, [], async function () {
    let audio = content.document.querySelector("audio");
    audio.pause();
  });
}

async function createAudioElement(src) {
  const doc = typeof content !== "undefined" ? content.document : document;
  const ROOT = "https://example.com/browser/ipc/glue/test/browser";
  let audio = doc.createElement("audio");
  audio.setAttribute("controls", "true");
  audio.setAttribute("loop", true);
  audio.src = `${ROOT}/${src}`;
  doc.body.appendChild(audio);
}

async function createAudioElementEME(sourceBuffer, audioFile) {
  // Helper to clone data into content so the EME helper can use the data.
  function cloneIntoContent(data) {
    return Cu.cloneInto(data, content.wrappedJSObject);
  }

  // Load the EME helper into content.
  Services.scriptloader.loadSubScript(
    "chrome://mochitests/content/browser/ipc/glue/test/browser/eme_standalone.js",
    content
  );

  let audio = content.document.createElement("audio");
  audio.setAttribute("controls", "true");
  audio.setAttribute("loop", true);
  audio.setAttribute("_sourceBufferType", sourceBuffer);
  audio.setAttribute("_audioUrl", audioFile);
  content.document.body.appendChild(audio);

  let emeHelper = new content.wrappedJSObject.EmeHelper();
  emeHelper.SetKeySystem(
    content.wrappedJSObject.EmeHelper.GetClearkeyKeySystemString()
  );
  emeHelper.SetInitDataTypes(cloneIntoContent(["keyids", "cenc"]));
  emeHelper.SetAudioCapabilities(
    cloneIntoContent([{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }])
  );
  emeHelper.AddKeyIdAndKey(
    "2cdb0ed6119853e7850671c3e9906c3c",
    "808B9ADAC384DE1E4F56140F4AD76194"
  );
  emeHelper.onerror = error => {
    is(false, `Got unexpected error from EME helper: ${error}`);
  };
  await emeHelper.ConfigureEme(audio);
  // Done setting up EME.
}

async function checkAudioDecoder(
  expectedProcess,
  expectedDecoder,
  expectContent = false,
  expectJava = false,
  expectError = false,
  withEME = false
) {
  const doc = typeof content !== "undefined" ? content.document : document;
  let audio = doc.querySelector("audio");
  const checkPromise = new Promise((resolve, reject) => {
    const timeUpdateHandler = async () => {
      const debugInfo = await SpecialPowers.wrap(audio).mozRequestDebugInfo();
      const audioDecoderName = debugInfo.decoder.reader.audioDecoderName;

      const isExpectedDecoder =
        audioDecoderName.indexOf(`${expectedDecoder}`) == 0;
      ok(
        isExpectedDecoder,
        `playback ${audio.src} was from decoder '${audioDecoderName}', expected '${expectedDecoder}'`
      );

      const isExpectedProcess =
        audioDecoderName.indexOf(`(${expectedProcess} remote)`) > 0;
      const isJavaRemote = audioDecoderName.indexOf("(remote)") > 0;
      const isOk =
        (isExpectedProcess && !isJavaRemote && !expectContent && !expectJava) || // Running in Utility
        (expectJava && !isExpectedProcess && isJavaRemote) || // Running in Java remote
        (expectContent && !isExpectedProcess && !isJavaRemote); // Running in Content

      ok(
        isOk,
        `playback ${audio.src} was from process '${audioDecoderName}', expected '${expectedProcess}'`
      );

      if (isOk) {
        resolve();
      } else {
        reject();
      }
    };

    const startPlaybackHandler = async () => {
      ok(
        await audio.play().then(
          _ => true,
          _ => false
        ),
        "audio started playing"
      );

      audio.addEventListener("timeupdate", timeUpdateHandler, { once: true });
    };

    audio.addEventListener("error", async () => {
      info(
        `Received HTML media error: ${audio.error.code}: ${audio.error.message}`
      );
      if (expectError) {
        const w = typeof content !== "undefined" ? content.window : window;
        ok(
          audio.error.code === w.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED ||
            w.MediaError.MEDIA_ERR_DECODE,
          "Media supported but decoding failed"
        );
        resolve();
      } else {
        info(`Unexpected error`);
        reject();
      }
    });

    audio.addEventListener("canplaythrough", startPlaybackHandler, {
      once: true,
    });
  });

  if (!withEME) {
    // We need to make sure the decoder is ready before play()ing otherwise we
    // could get into bad situations
    audio.load();
  } else {
    // For EME we need to create and load content ourselves. We do this here
    // because if we do it in createAudioElementEME() above then we end up
    // with events fired before we get a chance to listen to them here
    async function once(target, name) {
      return new Promise(r => target.addEventListener(name, r, { once: true }));
    }

    // Setup MSE.
    const ms = new content.wrappedJSObject.MediaSource();
    audio.src = content.wrappedJSObject.URL.createObjectURL(ms);
    await once(ms, "sourceopen");
    const sb = ms.addSourceBuffer(audio.getAttribute("_sourceBufferType"));
    let fetchResponse = await content.fetch(audio.getAttribute("_audioUrl"));
    let dataBuffer = await fetchResponse.arrayBuffer();
    sb.appendBuffer(dataBuffer);
    await once(sb, "updateend");
    ms.endOfStream();
    await once(ms, "sourceended");
  }

  return checkPromise;
}

async function runMochitestUtilityAudio(
  src,
  {
    expectUtility,
    expectDecoder,
    expectContent = false,
    expectJava = false,
    expectError = false,
  } = {}
) {
  info(`Add media: ${src}`);
  await createAudioElement(src);
  let audio = document.querySelector("audio");
  ok(audio, "Found an audio element created");

  info(`Play media: ${src}`);
  await checkAudioDecoder(
    expectUtility,
    expectDecoder,
    expectContent,
    expectJava,
    expectError
  );

  info(`Pause media: ${src}`);
  await audio.pause();

  info(`Remove media: ${src}`);
  document.body.removeChild(audio);
}

async function crashSomeUtility(utilityPid, actorsCheck) {
  SimpleTest.expectChildProcessCrash();

  const crashMan = Services.crashmanager;
  const utilityProcessGone = TestUtils.topicObserved(
    "ipc:utility-shutdown",
    (subject, data) => {
      info(`ipc:utility-shutdown: data=${data} subject=${subject}`);
      return parseInt(data, 10) === utilityPid;
    }
  );

  info("prune any previous crashes");
  const future = new Date(Date.now() + 1000 * 60 * 60 * 24);
  await crashMan.pruneOldCrashes(future);

  info("crash Utility Process");
  const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
    Ci.nsIProcessToolsService
  );

  info(`Crash Utility Process ${utilityPid}`);
  ProcessTools.crash(utilityPid);

  info(`Waiting for utility process ${utilityPid} to go away.`);
  let [subject, data] = await utilityProcessGone;
  Assert.strictEqual(
    parseInt(data, 10),
    utilityPid,
    `Should match the crashed PID ${utilityPid} with ${data}`
  );
  ok(
    subject instanceof Ci.nsIPropertyBag2,
    "Subject needs to be a nsIPropertyBag2 to clean up properly"
  );

  // Make sure the process is dead, otherwise there is a risk of race for
  // writing leak logs
  utilityProcessTest().noteIntentionalCrash(utilityPid);

  const dumpID = subject.getPropertyAsAString("dumpID");
  ok(dumpID, "There should be a dumpID");

  await crashMan.ensureCrashIsPresent(dumpID);
  await crashMan.getCrashes().then(crashes => {
    is(crashes.length, 1, "There should be only one record");
    const crash = crashes[0];
    ok(
      crash.isOfType(
        crashMan.processTypes[Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY],
        crashMan.CRASH_TYPE_CRASH
      ),
      "Record should be a utility process crash"
    );
    Assert.strictEqual(crash.id, dumpID, "Record should have an ID");
    ok(
      actorsCheck(crash.metadata.UtilityActorsName),
      `Record should have the correct actors name for: ${crash.metadata.UtilityActorsName}`
    );
  });

  let minidumpDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
  minidumpDirectory.append("minidumps");

  let dumpfile = minidumpDirectory.clone();
  dumpfile.append(dumpID + ".dmp");
  if (dumpfile.exists()) {
    info(`Removal of ${dumpfile.path}`);
    dumpfile.remove(false);
  }

  let extrafile = minidumpDirectory.clone();
  extrafile.append(dumpID + ".extra");
  info(`Removal of ${extrafile.path}`);
  if (extrafile.exists()) {
    extrafile.remove(false);
  }
}

// Crash a utility process and generate a crash dump.  To close a utility
// process (forcefully or not) without a generating a crash, use
// cleanUtilityProcessShutdown.
async function crashSomeUtilityActor(
  actor,
  actorsCheck = () => {
    return true;
  }
) {
  // Get PID for utility type
  const procInfos = await getUtilityProcesses(actor);
  Assert.equal(
    procInfos.length,
    1,
    `exactly one ${actor} utility process should be found`
  );
  const utilityPid = procInfos[0].pid;
  return crashSomeUtility(utilityPid, actorsCheck);
}

function isNightlyOnly() {
  const { AppConstants } = ChromeUtils.importESModule(
    "resource://gre/modules/AppConstants.sys.mjs"
  );
  return AppConstants.NIGHTLY_BUILD;
}
