/**
   * Performs a randomized test of renumbering logic.
   *
   * @param testIterations number of test iterations on the same document. Each iteration does a
   *     substantial amount of work (depending on document size).
   * @param seed initial random seed.
   */
  void doRandomTest(int testIterations, int seed) {
    ContentDocument.performExpensiveChecks = false;
    ContentDocument.validateLocalOps = false;
    IndexedDocumentImpl.performValidation = false;

    final int LEVELS = 4;
    final int MAX_RUN = 3;
    final int ITERS_PER_BATCH_RENDER = 6;
    final int DECIMALS_TO_OTHERS = 4; // ratio of decimal bullets to other stuff
    final int UPDATE_TO_ADD_REMOVE = 4; // ratio of updates to node adds/removals

    assertNull(scheduledTask);

    int maxRand = 5;
    Random r = new Random(seed);

    // For each iteration
    for (int iter = 0; iter < testIterations; iter++) {
      info("Iter: " + iter);

      // Repeat several times for a single batch render, to make sure we are
      // able to handle multiple overlapping, redundant updates.
      // Times two because we are alternating between two documents to test
      // the ability of the renumberer to handle more than one document
      // correctly.
      int innerIters = (r.nextInt(ITERS_PER_BATCH_RENDER) + 1) * 2;
      for (int inner = 0; inner < innerIters; inner++) {
        doc = doc1; // (inner % 2 == 0) ? doc1 : doc2;

        int totalLines = (doc.size() - 2) / 2;

        Line line = getFirstLine();

        // Pick a random section of the document to perform a bunch of random
        // changes to
        int i = 0;
        int a = r.nextInt(totalLines);
        int b = r.nextInt(totalLines);
        int startSection = Math.min(a, b);
        int endSection = Math.max(a, b);

        while (i < startSection) {
          i++;
          line = line.next();
        }

        while (i < endSection && line != null) {
          // Pick a random indentation to set
          int level = r.nextInt(LEVELS);
          // Length of run of elements to update
          int length;
          // Whether we are making them numbered items or doing something else
          boolean decimal;

          if (r.nextInt(DECIMALS_TO_OTHERS) == 0) {
            // No need making it a long run for non-numbered items.
            length = r.nextInt(2);
            decimal = false;
          } else {
            decimal = true;
            length = r.nextInt(MAX_RUN - 1) + 1;
          }

          while (length > 0 && i < endSection && line != null) {
            boolean fiftyFifty = i % 2 == 0;
            // If we're numbering these lines, then DECIMAL, otherwise choose a
            // random other type.
            Type type = decimal ? Type.DECIMAL : Type.values()[r.nextInt(Type.values().length - 1)];

            // Randomly decide to add/remove, or to update
            if (r.nextInt(UPDATE_TO_ADD_REMOVE) == 0) {

              int index = index(line);
              // Randomly decide to add or remove.
              // Include some constraints to ensure the document doesn't get too small or too large.
              boolean add =
                  index == 0 || totalLines < SIZE / 2
                      ? true
                      : (totalLines > SIZE * 2 ? false : r.nextBoolean());

              if (add) {
                line = create(index, type, level, r.nextBoolean());
              } else {
                line = delete(index);
                if (line == null) {
                  // We just deleted the last line.
                  continue;
                }
              }
              assert line != null;

            } else {
              update(index(line), type, level, fiftyFifty);
            }

            length--;
            i++;
            line = line.next();
          }
        }
      }

      check(iter);
    }
  }