/**
   * Creates and returns a new line.
   *
   * @param createAndUpdateSeparately if true, creates a line, then sets the attributes as a
   *     separate operation. Otherwise, sets them all at once. We want to test both scenarios.
   */
  Line create(int index, Type type, int indent, boolean createAndUpdateSeparately) {
    //    info("Creating @" + index + " " +
    //      type + " " + indent + " " + createAndUpdateSeparately);

    Point<ContentNode> loc = doc.locate(index * 2 + 1);
    Line l;
    if (createAndUpdateSeparately) {
      l = Line.fromLineElement(doc.createElement(loc, "line", Attributes.EMPTY_MAP));
      update(index, type, indent);
    } else {
      l =
          Line.fromLineElement(
              doc.createElement(loc, "line", attributes(type, indent, false, true)));
    }
    assertNotNull(l);
    return l;
  }
  /** Deletes the line at the specified index. */
  Line delete(int index) {
    //    info("Deleting @" + index);

    assert index != 0 : "Code doesn't (yet) support killing the initial line";
    ContentElement e = getLineElement(index);
    Line line = Line.fromLineElement(e).next();
    doc.deleteNode(e);
    return line;
  }
  /**
   * Updates the attributes of the line at the specified index.
   *
   * @param alwaysSetRedundant if true, always set the listyle attribute even if it is not
   *     necessary. For example, if the listyle attribute was "decimal", but the type is "HEADING",
   *     the listyle attribute should normally be ignored and has no meaning. It won't make a
   *     difference if it is set or not. We want to test both scenarios.
   */
  void update(int index, Type type, int indent, boolean alwaysSetRedundant) {
    ContentElement e = getLineElement(index);
    //    info("Making @" + ((doc.getLocation(e) - 1)/2) + " " +
    //        type + " " + indent + " " + alwaysSetStyle);

    Map<String, String> updates = attributes(type, indent, alwaysSetRedundant, false);

    for (Map.Entry<String, String> pair : updates.entrySet()) {
      doc.setElementAttribute(e, pair.getKey(), pair.getValue());
    }
  }
 /** @return the first line object */
 Line getFirstLine() {
   return Line.getFirstLineOfContainer(doc.getDocumentElement().getFirstChild().asElement());
 }
 /** @return the line element for the given index. */
 ContentElement getLineElement(int index) {
   return doc.locate(index * 2 + 1).getNodeAfter().asElement();
 }
 /** @return index for the given line object (0 for the first line, etc). */
 int index(Line line) {
   return (doc.getLocation(line.getLineElement()) - 1) / 2;
 }
  /**
   * 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);
    }
  }