/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* import-globals-from ../shared-head.js */

// This Services declaration may shadow another from head.js, so define it as
// a var rather than a const.

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

// Load the shared head
const sharedHead = do_get_file("shared-head.js", false);
if (!sharedHead) {
  throw new Error("Could not load the shared head.");
}
Services.scriptloader.loadSubScript(
  Services.io.newFileURI(sharedHead).spec,
  this
);

add_setup(ProfilerTestUtils.assertProfilerInactive);

/**
 * This function takes a thread, and a sample tuple from the "data" array, and
 * inflates the frame to be an array of strings.
 *
 * @param {object} thread - The thread from the profile.
 * @param {Array} sample - The tuple from the thread.samples.data array.
 * @returns {Array<string>} An array of function names.
 */
function getInflatedStackLocations(thread, sample) {
  let stackTable = thread.stackTable;
  let frameTable = thread.frameTable;
  let stringTable = thread.stringTable;
  let SAMPLE_STACK_SLOT = thread.samples.schema.stack;
  let STACK_PREFIX_SLOT = stackTable.schema.prefix;
  let STACK_FRAME_SLOT = stackTable.schema.frame;
  let FRAME_LOCATION_SLOT = frameTable.schema.location;

  // Build the stack from the raw data and accumulate the locations in
  // an array.
  let stackIndex = sample[SAMPLE_STACK_SLOT];
  let locations = [];
  while (stackIndex !== null) {
    let stackEntry = stackTable.data[stackIndex];
    let frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]];
    locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]);
    stackIndex = stackEntry[STACK_PREFIX_SLOT];
  }

  // The profiler tree is inverted, so reverse the array.
  return locations.reverse();
}

/**
 * This utility matches up stacks to see if they contain a certain sequence of
 * stack frames. A correctly functioning profiler will have a certain sequence
 * of stacks, but we can't always determine exactly which stacks will show up
 * due to implementation changes, as well as memory addresses being arbitrary to
 * that particular build.
 *
 * This function triggers a test failure with a nice debug message when it
 * fails.
 *
 * @param {Array<string>} actualStackFrames - As generated by
 *     inflatedStackFrames.
 * @param {Array<string | RegExp>} expectedStackFrames - Matches a subset of
 *     actualStackFrames
 */
function expectStackToContain(
  actualStackFrames,
  expectedStackFrames,
  message = "The actual stack and expected stack do not match."
) {
  // Log the stacks that are being passed to this assertion, as it could be
  // useful for when these tests fail.
  console.log("Actual stack: ", actualStackFrames);
  console.log(
    "Expected to contain: ",
    expectedStackFrames.map(s => s.toString())
  );

  let actualIndex = 0;

  // Start walking the expected stack and look for matches.
  for (
    let expectedIndex = 0;
    expectedIndex < expectedStackFrames.length;
    expectedIndex++
  ) {
    const expectedStackFrame = expectedStackFrames[expectedIndex];

    while (true) {
      // Make sure that we haven't run out of actual stack frames.
      if (actualIndex >= actualStackFrames.length) {
        info(`Could not find a match for: "${expectedStackFrame.toString()}"`);
        Assert.ok(false, message);
      }

      const actualStackFrame = actualStackFrames[actualIndex];
      actualIndex++;

      const itMatches =
        typeof expectedStackFrame === "string"
          ? expectedStackFrame === actualStackFrame
          : actualStackFrame.match(expectedStackFrame);

      if (itMatches) {
        // We found a match, break out of this loop.
        break;
      }
      // Keep on looping looking for a match.
    }
  }

  Assert.ok(true, message);
}

/**
 * @param {Thread} thread
 * @param {string} filename - The filename used to trigger FileIO.
 * @returns {InflatedMarkers[]}
 */
function getInflatedFileIOMarkers(thread, filename) {
  const markers = ProfilerTestUtils.getInflatedMarkerData(thread);
  return markers.filter(
    marker =>
      marker.data?.type === "FileIO" &&
      marker.data?.filename?.endsWith(filename)
  );
}

/**
 * Checks properties common to all FileIO markers.
 *
 * @param {InflatedMarkers[]} markers
 * @param {string} filename
 */
function checkInflatedFileIOMarkers(markers, filename) {
  greater(markers.length, 0, "Found some markers");

  // See IOInterposeObserver::Observation::ObservedOperationString
  const validOperations = new Set([
    "write",
    "fsync",
    "close",
    "stat",
    "create/open",
    "read",
  ]);
  const validSources = new Set(["PoisonIOInterposer", "NSPRIOInterposer"]);

  for (const marker of markers) {
    try {
      ok(
        marker.name.startsWith("FileIO"),
        "Has a marker.name that starts with FileIO"
      );
      equal(marker.data.type, "FileIO", "Has a marker.data.type");
      ok(
        ProfilerTestUtils.isIntervalMarker(marker),
        "All FileIO markers are interval markers"
      );
      ok(
        validOperations.has(marker.data.operation),
        `The markers have a known operation - "${marker.data.operation}"`
      );
      ok(
        validSources.has(marker.data.source),
        `The FileIO marker has a known source "${marker.data.source}"`
      );
      ok(marker.data.filename.endsWith(filename));
      ok(Boolean(marker.data.stack), "A stack was collected");
    } catch (error) {
      console.error("Failing inflated FileIO marker:", marker);
      throw error;
    }
  }
}

/**
 * Do deep equality checks for schema, but then surface nice errors for a user to know
 * what to do if the check fails.
 */
function checkSchema(actual, expected) {
  const schemaName = expected.name;
  info(`Checking marker schema for "${schemaName}"`);

  try {
    ok(
      actual,
      `Schema was found for "${schemaName}". See the test output for more information.`
    );
    // Check individual properties to surface easier to debug errors.
    deepEqual(
      expected.display,
      actual.display,
      `The "display" property for ${schemaName} schema matches. See the test output for more information.`
    );
    if (expected.data) {
      ok(actual.data, `Schema was found for "${schemaName}"`);
      for (const expectedDatum of expected.data) {
        const actualDatum = actual.data.find(d => d.key === expectedDatum.key);
        deepEqual(
          expectedDatum,
          actualDatum,
          `The "${schemaName}" field "${expectedDatum.key}" matches expectations. See the test output for more information.`
        );
      }
      equal(
        expected.data.length,
        actual.data.length,
        "The expected and actual data have the same number of items"
      );
    }

    // Finally do a true deep equal.
    deepEqual(expected, actual, "The entire schema is deepEqual");
  } catch (error) {
    // The test results are not very human readable. This is a bit of a hacky
    // solution to make it more readable.
    dump("-----------------------------------------------------\n");
    dump("The expected marker schema:\n");
    dump("-----------------------------------------------------\n");
    dump(JSON.stringify(expected, null, 2));
    dump("\n");
    dump("-----------------------------------------------------\n");
    dump("The actual marker schema:\n");
    dump("-----------------------------------------------------\n");
    dump(JSON.stringify(actual, null, 2));
    dump("\n");
    dump("-----------------------------------------------------\n");
    dump("A marker schema was not equal to expectations. If you\n");
    dump("are modifying the schema, then please copy and paste\n");
    dump("the new schema into this test.\n");
    dump("-----------------------------------------------------\n");
    dump("Copy this: " + JSON.stringify(actual));
    dump("\n");
    dump("-----------------------------------------------------\n");

    throw error;
  }
}
