// Functions for checking results returned by
// Debugger.Memory.prototype.takeCensus and
// HeapSnapshot.prototype.takeCensus. Adapted from js/src/jit-test/lib/census.js.

export const Census = {};
function dumpn(msg) {
  dump("DBG-TEST: Census.sys.mjs: " + msg + "\n");
}

// Census.walkCensus(subject, name, walker)
//
// Use |walker| to check |subject|, a census object of the sort returned by
// Debugger.Memory.prototype.takeCensus: a tree of objects with integers at the
// leaves. Use |name| as the name for |subject| in diagnostic messages. Return
// the number of leaves of |subject| we visited.
//
// A walker is an object with three methods:
//
// - enter(prop): Return the walker we should use to check the property of the
//   subject census named |prop|. This is for recursing into the subobjects of
//   the subject.
//
// - done(): Called after we have called 'enter' on every property of the
//   subject.
//
// - check(value): Check |value|, a leaf in the subject.
//
// Walker methods are expected to simply throw if a node we visit doesn't look
// right.
Census.walkCensus = (subject, name, walker) => walk(subject, name, walker, 0);
function walk(subject, name, walker, count) {
  if (typeof subject === "object") {
    dumpn(name);
    for (const prop in subject) {
      count = walk(
        subject[prop],
        name + "[" + uneval(prop) + "]",
        walker.enter(prop),
        count
      );
    }
    walker.done();
  } else {
    dumpn(name + " = " + uneval(subject));
    walker.check(subject);
    count++;
  }

  return count;
}

// A walker that doesn't check anything.
Census.walkAnything = {
  enter: () => Census.walkAnything,
  done: () => undefined,
  check: () => undefined,
};

// A walker that requires all leaves to be zeros.
Census.assertAllZeros = {
  enter: () => Census.assertAllZeros,
  done: () => undefined,
  check: elt => {
    if (elt !== 0) {
      throw new Error("Census mismatch: expected zero, found " + elt);
    }
  },
};

function expectedObject() {
  throw new Error(
    "Census mismatch: subject has leaf where basis has nested object"
  );
}

function expectedLeaf() {
  throw new Error(
    "Census mismatch: subject has nested object where basis has leaf"
  );
}

// Return a function that, given a 'basis' census, returns a census walker that
// compares the subject census against the basis. The returned walker calls the
// given |compare|, |missing|, and |extra| functions as follows:
//
// - compare(subjectLeaf, basisLeaf): Check a leaf of the subject against the
//   corresponding leaf of the basis.
//
// - missing(prop, value): Called when the subject is missing a property named
//   |prop| which is present in the basis with value |value|.
//
// - extra(prop): Called when the subject has a property named |prop|, but the
//   basis has no such property. This should return a walker that can check
//   the subject's value.
function makeBasisChecker({ compare, missing, extra }) {
  return function makeWalker(basis) {
    if (typeof basis === "object") {
      const unvisited = new Set(Object.getOwnPropertyNames(basis));
      return {
        enter: prop => {
          unvisited.delete(prop);
          if (prop in basis) {
            return makeWalker(basis[prop]);
          }

          return extra(prop);
        },

        done: () => unvisited.forEach(prop => missing(prop, basis[prop])),
        check: expectedObject,
      };
    }

    return {
      enter: expectedLeaf,
      done: expectedLeaf,
      check: elt => compare(elt, basis),
    };
  };
}

function missingProp(prop) {
  throw new Error(
    "Census mismatch: subject lacks property present in basis: " + prop
  );
}

function extraProp(prop) {
  throw new Error(
    "Census mismatch: subject has property not present in basis: " + prop
  );
}

// Return a walker that checks that the subject census has counts all equal to
// |basis|.
Census.assertAllEqual = makeBasisChecker({
  compare: (a, b) => {
    if (a !== b) {
      throw new Error("Census mismatch: expected " + a + " got " + b);
    }
  },
  missing: missingProp,
  extra: extraProp,
});

function ok(val) {
  if (!val) {
    throw new Error("Census mismatch: expected truthy, got " + val);
  }
}
/* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */

// Return a walker that checks that the subject census has at least as many
// items of each category as |basis|.
Census.assertAllNotLessThan = makeBasisChecker({
  compare: (subject, basis) => ok(subject >= basis),
  missing: missingProp,
  extra: () => Census.walkAnything,
});

// Return a walker that checks that the subject census has at most as many
// items of each category as |basis|.
Census.assertAllNotMoreThan = makeBasisChecker({
  compare: (subject, basis) => ok(subject <= basis),
  missing: missingProp,
  extra: () => Census.walkAnything,
});

// Return a walker that checks that the subject census has within |fudge|
// items of each category of the count in |basis|.
Census.assertAllWithin = function (fudge, basis) {
  return makeBasisChecker({
    compare: (subject, base) => ok(Math.abs(subject - base) <= fudge),
    missing: missingProp,
    extra: () => Census.walkAnything,
  })(basis);
};
