"use strict";

// These tests probably use too much abstraction and too little copy-paste.
// Reader beware.
//
// TODO:
//
// * Lots and lots and lots more different types of ranges
// * insertBefore() with DocumentFragments
// * Fill out other insert/remove tests
// * normalize() (https://www.w3.org/Bugs/Public/show_bug.cgi?id=13843)

// Give a textual description of the range we're testing, for the test names.
function describeRange(startContainer, startOffset, endContainer, endOffset) {
  if (startContainer == endContainer && startOffset == endOffset) {
    return "range collapsed at (" + startContainer + ", " + startOffset + ")";
  } else if (startContainer == endContainer) {
    return "range on " + startContainer + " from " + startOffset + " to " + endOffset;
  } else {
    return "range from (" + startContainer + ", " + startOffset + ") to (" + endContainer + ", " + endOffset + ")";
  }
}

// Lists of the various types of nodes we'll want to use.  We use strings that
// we can later eval(), so that we can produce legible test names.
var textNodes = [
  "paras[0].firstChild",
  "paras[1].firstChild",
  "foreignTextNode",
  "xmlTextNode",
  "detachedTextNode",
  "detachedForeignTextNode",
  "detachedXmlTextNode",
];
var commentNodes = [
  "comment",
  "foreignComment",
  "xmlComment",
  "detachedComment",
  "detachedForeignComment",
  "detachedXmlComment",
];
var characterDataNodes = textNodes.concat(commentNodes);

// This function is slightly scary, but it works well enough, so . . .
// sourceTests is an array of test data that will be altered in mysterious ways
// before being passed off to doTest, descFn is something that takes an element
// of sourceTests and produces the first part of a human-readable description
// of the test, testFn is the function that doTest will call to do the actual
// work and tell it what results to expect.
function doTests(sourceTests, descFn, testFn) {
  var tests = [];
  for (var i = 0; i < sourceTests.length; i++) {
    var params = sourceTests[i];
    var len = params.length;
    tests.push([
      descFn(params) + ", with unselected " + describeRange(params[len - 4], params[len - 3], params[len - 2], params[len - 1]),
      // The closure here ensures that the params that testFn get are the
      // current version of params, not the version from the last
      // iteration of this loop.  We test that none of the parameters
      // evaluate to undefined to catch bugs in our eval'ing, like
      // mistyping a property name.
      function(params) { return function() {
        var evaledParams = params.map(eval);
        for (var i = 0; i < evaledParams.length; i++) {
          assert_true(typeof evaledParams[i] != "undefined",
            "Test bug: " + params[i] + " is undefined");
        }
        return testFn.apply(null, evaledParams);
      } }(params),
      false,
      params[len - 4],
      params[len - 3],
      params[len - 2],
      params[len - 1]
    ]);
    tests.push([
      descFn(params) + ", with selected " + describeRange(params[len - 4], params[len - 3], params[len - 2], params[len - 1]),
      function(params) { return function(selectedRange) {
        var evaledParams = params.slice(0, len - 4).map(eval);
        for (var i = 0; i < evaledParams.length; i++) {
          assert_true(typeof evaledParams[i] != "undefined",
            "Test bug: " + params[i] + " is undefined");
        }
        // Override input range with the one that was actually selected when computing the expected result.
        evaledParams = evaledParams.concat([selectedRange.startContainer, selectedRange.startOffset, selectedRange.endContainer, selectedRange.endOffset]);
        return testFn.apply(null, evaledParams);
      } }(params),
      true,
      params[len - 4],
      params[len - 3],
      params[len - 2],
      params[len - 1]
    ]);
  }
  generate_tests(doTest, tests);
}

// Set up the range, call the callback function to do the DOM modification and
// tell us what to expect.  The callback function needs to return a
// four-element array with the expected start/end containers/offsets, and
// receives no arguments.  useSelection tells us whether the Range should be
// added to a Selection and the Selection tested to ensure that the mutation
// affects user selections as well as other ranges; every test is run with this
// both false and true, because when it's set to true WebKit and Opera fail all
// tests' sanity checks, which is unhelpful.  The last four parameters just
// tell us what range to build.
function doTest(callback, useSelection, startContainer, startOffset, endContainer, endOffset) {
  // Recreate all the test nodes in case they were altered by the last test
  // run.
  setupRangeTests();
  startContainer = eval(startContainer);
  startOffset = eval(startOffset);
  endContainer = eval(endContainer);
  endOffset = eval(endOffset);

  var ownerDoc = startContainer.nodeType == Node.DOCUMENT_NODE
    ? startContainer
    : startContainer.ownerDocument;
  var range = ownerDoc.createRange();
  range.setStart(startContainer, startOffset);
  range.setEnd(endContainer, endOffset);

  if (useSelection) {
    getSelection().removeAllRanges();
    getSelection().addRange(range);

    // Some browsers refuse to add a range unless it results in an actual visible selection.
    if (!getSelection().rangeCount)
        return;

    // Override range with the one that was actually selected as it differs in some browsers.
    range = getSelection().getRangeAt(0);
  }

  var expected = callback(range);

  assert_equals(range.startContainer, expected[0],
    "Wrong start container");
  assert_equals(range.startOffset, expected[1],
    "Wrong start offset");
  assert_equals(range.endContainer, expected[2],
    "Wrong end container");
  assert_equals(range.endOffset, expected[3],
    "Wrong end offset");
}


// Now we get to the specific tests.

function testSplitText(oldNode, offset, startContainer, startOffset, endContainer, endOffset) {
  // Save these for later
  var originalStartOffset = startOffset;
  var originalEndOffset = endOffset;
  var originalLength = oldNode.length;

  var newNode;
  try {
    newNode = oldNode.splitText(offset);
  } catch (e) {
    // Should only happen if offset is negative
    return [startContainer, startOffset, endContainer, endOffset];
  }

  // First we adjust for replacing data:
  //
  // "Replace data with offset offset, count count, and data the empty
  // string."
  //
  // That translates to offset = offset, count = originalLength - offset,
  // data = "".  node is oldNode.
  //
  // "For every boundary point whose node is node, and whose offset is
  // greater than offset but less than or equal to offset plus count, set its
  // offset to offset."
  if (startContainer == oldNode
  && startOffset > offset
  && startOffset <= originalLength) {
    startOffset = offset;
  }

  if (endContainer == oldNode
  && endOffset > offset
  && endOffset <= originalLength) {
    endOffset = offset;
  }

  // "For every boundary point whose node is node, and whose offset is
  // greater than offset plus count, add the length of data to its offset,
  // then subtract count from it."
  //
  // Can't happen: offset plus count is originalLength.

  // Now we insert a node, if oldNode's parent isn't null: "For each boundary
  // point whose node is the new parent of the affected node and whose offset
  // is greater than the new index of the affected node, add one to the
  // boundary point's offset."
  if (startContainer == oldNode.parentNode
  && startOffset > 1 + indexOf(oldNode)) {
    startOffset++;
  }

  if (endContainer == oldNode.parentNode
  && endOffset > 1 + indexOf(oldNode)) {
    endOffset++;
  }

  // Finally, the splitText stuff itself:
  //
  // "If parent is not null, run these substeps:
  //
  //   * "For each range whose start node is node and start offset is greater
  //   than offset, set its start node to new node and decrease its start
  //   offset by offset.
  //
  //   * "For each range whose end node is node and end offset is greater
  //   than offset, set its end node to new node and decrease its end offset
  //   by offset.
  //
  //   * "For each range whose start node is parent and start offset is equal
  //   to the index of node + 1, increase its start offset by one.
  //
  //   * "For each range whose end node is parent and end offset is equal to
  //   the index of node + 1, increase its end offset by one."
  if (oldNode.parentNode) {
    if (startContainer == oldNode && originalStartOffset > offset) {
      startContainer = newNode;
      startOffset = originalStartOffset - offset;
    }

    if (endContainer == oldNode && originalEndOffset > offset) {
      endContainer = newNode;
      endOffset = originalEndOffset - offset;
    }

    if (startContainer == oldNode.parentNode
    && startOffset == 1 + indexOf(oldNode)) {
      startOffset++;
    }

    if (endContainer == oldNode.parentNode
    && endOffset == 1 + indexOf(oldNode)) {
      endOffset++;
    }
  }

  return [startContainer, startOffset, endContainer, endOffset];
}

// The offset argument is unsigned, so per WebIDL -1 should wrap to 4294967295,
// which is probably longer than the length, so it should throw an exception.
// This is no different from the other cases where the offset is longer than
// the length, and the wrapping complicates my testing slightly, so I won't
// bother testing negative values here or in other cases.
var splitTextTests = [];
for (var i = 0; i < textNodes.length; i++) {
  var node = textNodes[i];
  splitTextTests.push([node, 376, node, 0, node, 1]);
  splitTextTests.push([node, 0, node, 0, node, 0]);
  splitTextTests.push([node, 1, node, 1, node, 1]);
  splitTextTests.push([node, node + ".length", node, node + ".length", node, node + ".length"]);
  splitTextTests.push([node, 1, node, 1, node, 3]);
  splitTextTests.push([node, 2, node, 1, node, 3]);
  splitTextTests.push([node, 3, node, 1, node, 3]);
}

splitTextTests.push(
  ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0]", 0],
  ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0]", 1],
  ["paras[0].firstChild", 1, "paras[0]", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 2, "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 3, "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 2, "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 3, "paras[0]", 0, "paras[0].firstChild", 3]
);


function testReplaceDataAlgorithm(node, offset, count, data, callback, startContainer, startOffset, endContainer, endOffset) {
  // Mutation works the same any time DOM Core's "replace data" algorithm is
  // invoked.  node, offset, count, data are as in that algorithm.  The
  // callback is what does the actual setting.  Not to be confused with
  // testReplaceData, which tests the replaceData() method.

  // Barring any provision to the contrary, the containers and offsets must
  // not change.
  var expectedStartContainer = startContainer;
  var expectedStartOffset = startOffset;
  var expectedEndContainer = endContainer;
  var expectedEndOffset = endOffset;

  var originalParent = node.parentNode;
  var originalData = node.data;

  var exceptionThrown = false;
  try {
    callback();
  } catch (e) {
    // Should only happen if offset is greater than length
    exceptionThrown = true;
  }

  assert_equals(node.parentNode, originalParent,
    "Sanity check failed: changing data changed the parent");

  // "User agents must run the following steps whenever they replace data of
  // a CharacterData node, as though they were written in the specification
  // for that algorithm after all other steps. In particular, the steps must
  // not be executed if the algorithm threw an exception."
  if (exceptionThrown) {
    assert_equals(node.data, originalData,
      "Sanity check failed: exception thrown but data changed");
  } else {
    assert_equals(node.data,
      originalData.substr(0, offset) + data + originalData.substr(offset + count),
      "Sanity check failed: data not changed as expected");
  }

  // "For every boundary point whose node is node, and whose offset is
  // greater than offset but less than or equal to offset plus count, set
  // its offset to offset."
  if (!exceptionThrown
  && startContainer == node
  && startOffset > offset
  && startOffset <= offset + count) {
    expectedStartOffset = offset;
  }

  if (!exceptionThrown
  && endContainer == node
  && endOffset > offset
  && endOffset <= offset + count) {
    expectedEndOffset = offset;
  }

  // "For every boundary point whose node is node, and whose offset is
  // greater than offset plus count, add the length of data to its offset,
  // then subtract count from it."
  if (!exceptionThrown
  && startContainer == node
  && startOffset > offset + count) {
    expectedStartOffset += data.length - count;
  }

  if (!exceptionThrown
  && endContainer == node
  && endOffset > offset + count) {
    expectedEndOffset += data.length - count;
  }

  return [expectedStartContainer, expectedStartOffset, expectedEndContainer, expectedEndOffset];
}

function testInsertData(node, offset, data, startContainer, startOffset, endContainer, endOffset) {
  return testReplaceDataAlgorithm(node, offset, 0, data,
    function() { node.insertData(offset, data) },
    startContainer, startOffset, endContainer, endOffset);
}

var insertDataTests = [];
for (var i = 0; i < characterDataNodes.length; i++) {
  var node = characterDataNodes[i];
  insertDataTests.push([node, 376, '"foo"', node, 0, node, 1]);
  insertDataTests.push([node, 0, '"foo"', node, 0, node, 0]);
  insertDataTests.push([node, 1, '"foo"', node, 1, node, 1]);
  insertDataTests.push([node, node + ".length", '"foo"', node, node + ".length", node, node + ".length"]);
  insertDataTests.push([node, 1, '"foo"', node, 1, node, 3]);
  insertDataTests.push([node, 2, '"foo"', node, 1, node, 3]);
  insertDataTests.push([node, 3, '"foo"', node, 1, node, 3]);

  insertDataTests.push([node, 376, '""', node, 0, node, 1]);
  insertDataTests.push([node, 0, '""', node, 0, node, 0]);
  insertDataTests.push([node, 1, '""', node, 1, node, 1]);
  insertDataTests.push([node, node + ".length", '""', node, node + ".length", node, node + ".length"]);
  insertDataTests.push([node, 1, '""', node, 1, node, 3]);
  insertDataTests.push([node, 2, '""', node, 1, node, 3]);
  insertDataTests.push([node, 3, '""', node, 1, node, 3]);
}

insertDataTests.push(
  ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0]", 0],
  ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0]", 1],
  ["paras[0].firstChild", 1, '"foo"', "paras[0]", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 2, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 3, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 2, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 3, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3]
);


function testAppendData(node, data, startContainer, startOffset, endContainer, endOffset) {
  return testReplaceDataAlgorithm(node, node.length, 0, data,
    function() { node.appendData(data) },
    startContainer, startOffset, endContainer, endOffset);
}

var appendDataTests = [];
for (var i = 0; i < characterDataNodes.length; i++) {
  var node = characterDataNodes[i];
  appendDataTests.push([node, '"foo"', node, 0, node, 1]);
  appendDataTests.push([node, '"foo"', node, 0, node, 0]);
  appendDataTests.push([node, '"foo"', node, 1, node, 1]);
  appendDataTests.push([node, '"foo"', node, 0, node, node + ".length"]);
  appendDataTests.push([node, '"foo"', node, 1, node, node + ".length"]);
  appendDataTests.push([node, '"foo"', node, node + ".length", node, node + ".length"]);
  appendDataTests.push([node, '"foo"', node, 1, node, 3]);

  appendDataTests.push([node, '""', node, 0, node, 1]);
  appendDataTests.push([node, '""', node, 0, node, 0]);
  appendDataTests.push([node, '""', node, 1, node, 1]);
  appendDataTests.push([node, '""', node, 0, node, node + ".length"]);
  appendDataTests.push([node, '""', node, 1, node, node + ".length"]);
  appendDataTests.push([node, '""', node, node + ".length", node, node + ".length"]);
  appendDataTests.push([node, '""', node, 1, node, 3]);
}

appendDataTests.push(
  ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0]", 0],
  ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0]", 1],
  ["paras[0].firstChild", '""', "paras[0]", 1, "paras[0]", 1],
  ["paras[0].firstChild", '""', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", '""', "paras[0]", 0, "paras[0].firstChild", 3],

  ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0]", 0],
  ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0]", 1],
  ["paras[0].firstChild", '"foo"', "paras[0]", 1, "paras[0]", 1],
  ["paras[0].firstChild", '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", '"foo"', "paras[0]", 0, "paras[0].firstChild", 3]
);


function testDeleteData(node, offset, count, startContainer, startOffset, endContainer, endOffset) {
  return testReplaceDataAlgorithm(node, offset, count, "",
    function() { node.deleteData(offset, count) },
    startContainer, startOffset, endContainer, endOffset);
}

var deleteDataTests = [];
for (var i = 0; i < characterDataNodes.length; i++) {
  var node = characterDataNodes[i];
  deleteDataTests.push([node, 376, 2, node, 0, node, 1]);
  deleteDataTests.push([node, 0, 2, node, 0, node, 0]);
  deleteDataTests.push([node, 1, 2, node, 1, node, 1]);
  deleteDataTests.push([node, node + ".length", 2, node, node + ".length", node, node + ".length"]);
  deleteDataTests.push([node, 1, 2, node, 1, node, 3]);
  deleteDataTests.push([node, 2, 2, node, 1, node, 3]);
  deleteDataTests.push([node, 3, 2, node, 1, node, 3]);

  deleteDataTests.push([node, 376, 0, node, 0, node, 1]);
  deleteDataTests.push([node, 0, 0, node, 0, node, 0]);
  deleteDataTests.push([node, 1, 0, node, 1, node, 1]);
  deleteDataTests.push([node, node + ".length", 0, node, node + ".length", node, node + ".length"]);
  deleteDataTests.push([node, 1, 0, node, 1, node, 3]);
  deleteDataTests.push([node, 2, 0, node, 1, node, 3]);
  deleteDataTests.push([node, 3, 0, node, 1, node, 3]);

  deleteDataTests.push([node, 376, 631, node, 0, node, 1]);
  deleteDataTests.push([node, 0, 631, node, 0, node, 0]);
  deleteDataTests.push([node, 1, 631, node, 1, node, 1]);
  deleteDataTests.push([node, node + ".length", 631, node, node + ".length", node, node + ".length"]);
  deleteDataTests.push([node, 1, 631, node, 1, node, 3]);
  deleteDataTests.push([node, 2, 631, node, 1, node, 3]);
  deleteDataTests.push([node, 3, 631, node, 1, node, 3]);
}

deleteDataTests.push(
  ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0]", 0],
  ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0]", 1],
  ["paras[0].firstChild", 1, 2, "paras[0]", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, 2, "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 2, 2, "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 3, 2, "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, 2, "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 2, 2, "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 3, 2, "paras[0]", 0, "paras[0].firstChild", 3]
);


function testReplaceData(node, offset, count, data, startContainer, startOffset, endContainer, endOffset) {
  return testReplaceDataAlgorithm(node, offset, count, data,
    function() { node.replaceData(offset, count, data) },
    startContainer, startOffset, endContainer, endOffset);
}

var replaceDataTests = [];
for (var i = 0; i < characterDataNodes.length; i++) {
  var node = characterDataNodes[i];
  replaceDataTests.push([node, 376, 0, '"foo"', node, 0, node, 1]);
  replaceDataTests.push([node, 0, 0, '"foo"', node, 0, node, 0]);
  replaceDataTests.push([node, 1, 0, '"foo"', node, 1, node, 1]);
  replaceDataTests.push([node, node + ".length", 0, '"foo"', node, node + ".length", node, node + ".length"]);
  replaceDataTests.push([node, 1, 0, '"foo"', node, 1, node, 3]);
  replaceDataTests.push([node, 2, 0, '"foo"', node, 1, node, 3]);
  replaceDataTests.push([node, 3, 0, '"foo"', node, 1, node, 3]);

  replaceDataTests.push([node, 376, 0, '""', node, 0, node, 1]);
  replaceDataTests.push([node, 0, 0, '""', node, 0, node, 0]);
  replaceDataTests.push([node, 1, 0, '""', node, 1, node, 1]);
  replaceDataTests.push([node, node + ".length", 0, '""', node, node + ".length", node, node + ".length"]);
  replaceDataTests.push([node, 1, 0, '""', node, 1, node, 3]);
  replaceDataTests.push([node, 2, 0, '""', node, 1, node, 3]);
  replaceDataTests.push([node, 3, 0, '""', node, 1, node, 3]);

  replaceDataTests.push([node, 376, 1, '"foo"', node, 0, node, 1]);
  replaceDataTests.push([node, 0, 1, '"foo"', node, 0, node, 0]);
  replaceDataTests.push([node, 1, 1, '"foo"', node, 1, node, 1]);
  replaceDataTests.push([node, node + ".length", 1, '"foo"', node, node + ".length", node, node + ".length"]);
  replaceDataTests.push([node, 1, 1, '"foo"', node, 1, node, 3]);
  replaceDataTests.push([node, 2, 1, '"foo"', node, 1, node, 3]);
  replaceDataTests.push([node, 3, 1, '"foo"', node, 1, node, 3]);

  replaceDataTests.push([node, 376, 1, '""', node, 0, node, 1]);
  replaceDataTests.push([node, 0, 1, '""', node, 0, node, 0]);
  replaceDataTests.push([node, 1, 1, '""', node, 1, node, 1]);
  replaceDataTests.push([node, node + ".length", 1, '""', node, node + ".length", node, node + ".length"]);
  replaceDataTests.push([node, 1, 1, '""', node, 1, node, 3]);
  replaceDataTests.push([node, 2, 1, '""', node, 1, node, 3]);
  replaceDataTests.push([node, 3, 1, '""', node, 1, node, 3]);

  replaceDataTests.push([node, 376, 47, '"foo"', node, 0, node, 1]);
  replaceDataTests.push([node, 0, 47, '"foo"', node, 0, node, 0]);
  replaceDataTests.push([node, 1, 47, '"foo"', node, 1, node, 1]);
  replaceDataTests.push([node, node + ".length", 47, '"foo"', node, node + ".length", node, node + ".length"]);
  replaceDataTests.push([node, 1, 47, '"foo"', node, 1, node, 3]);
  replaceDataTests.push([node, 2, 47, '"foo"', node, 1, node, 3]);
  replaceDataTests.push([node, 3, 47, '"foo"', node, 1, node, 3]);

  replaceDataTests.push([node, 376, 47, '""', node, 0, node, 1]);
  replaceDataTests.push([node, 0, 47, '""', node, 0, node, 0]);
  replaceDataTests.push([node, 1, 47, '""', node, 1, node, 1]);
  replaceDataTests.push([node, node + ".length", 47, '""', node, node + ".length", node, node + ".length"]);
  replaceDataTests.push([node, 1, 47, '""', node, 1, node, 3]);
  replaceDataTests.push([node, 2, 47, '""', node, 1, node, 3]);
  replaceDataTests.push([node, 3, 47, '""', node, 1, node, 3]);
}

replaceDataTests.push(
  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0]", 0],
  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0]", 1],
  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 2, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 3, 0, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 2, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 3, 0, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],

  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0]", 0],
  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0]", 1],
  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 2, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 3, 1, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 2, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 3, 1, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],

  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0]", 0],
  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0]", 1],
  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 2, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 3, 47, '"foo"', "paras[0].firstChild", 1, "paras[0]", 1],
  ["paras[0].firstChild", 1, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 2, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3],
  ["paras[0].firstChild", 3, 47, '"foo"', "paras[0]", 0, "paras[0].firstChild", 3]
);


// There are lots of ways to set data, so we pass a callback that does the
// actual setting.
function testDataChange(node, attr, op, rval, startContainer, startOffset, endContainer, endOffset) {
  return testReplaceDataAlgorithm(node, 0, node.length, op == "=" ? rval : node[attr] + rval,
    function() {
      if (op == "=") {
        node[attr] = rval;
      } else if (op == "+=") {
        node[attr] += rval;
      } else {
        throw "Unknown op " + op;
      }
    },
    startContainer, startOffset, endContainer, endOffset);
}

var dataChangeTests = [];
var dataChangeTestAttrs = ["data", "textContent", "nodeValue"];
for (var i = 0; i < characterDataNodes.length; i++) {
  var node = characterDataNodes[i];
  var dataChangeTestRanges = [
    [node, 0, node, 0],
    [node, 0, node, 1],
    [node, 1, node, 1],
    [node, 0, node, node + ".length"],
    [node, 1, node, node + ".length"],
    [node, node + ".length", node, node + ".length"],
  ];

  for (var j = 0; j < dataChangeTestRanges.length; j++) {
    for (var k = 0; k < dataChangeTestAttrs.length; k++) {
      dataChangeTests.push([
        node,
        '"' + dataChangeTestAttrs[k] + '"',
        '"="',
        '""',
      ].concat(dataChangeTestRanges[j]));

      dataChangeTests.push([
        node,
        '"' + dataChangeTestAttrs[k] + '"',
        '"="',
        '"foo"',
      ].concat(dataChangeTestRanges[j]));

      dataChangeTests.push([
        node,
        '"' + dataChangeTestAttrs[k] + '"',
        '"="',
        node + "." + dataChangeTestAttrs[k],
      ].concat(dataChangeTestRanges[j]));

      dataChangeTests.push([
        node,
        '"' + dataChangeTestAttrs[k] + '"',
        '"+="',
        '""',
      ].concat(dataChangeTestRanges[j]));

      dataChangeTests.push([
        node,
        '"' + dataChangeTestAttrs[k] + '"',
        '"+="',
        '"foo"',
      ].concat(dataChangeTestRanges[j]));

      dataChangeTests.push([
        node,
        '"' + dataChangeTestAttrs[k] + '"',
        '"+="',
        node + "." + dataChangeTestAttrs[k]
      ].concat(dataChangeTestRanges[j]));
    }
  }
}


// Now we test node insertions and deletions, as opposed to just data changes.
// To avoid loads of repetition, we define modifyForRemove() and
// modifyForInsert().

// If we were to remove removedNode from its parent, what would the boundary
// point [node, offset] become?  Returns [new node, new offset].  Must be
// called BEFORE the node is actually removed, so its parent is not null.  (If
// the parent is null, it will do nothing.)
function modifyForRemove(removedNode, point) {
  var oldParent = removedNode.parentNode;
  var oldIndex = indexOf(removedNode);
  if (!oldParent) {
    return point;
  }

  // "For each boundary point whose node is removed node or a descendant of
  // it, set the boundary point to (old parent, old index)."
  if (point[0] == removedNode || isDescendant(point[0], removedNode)) {
    return [oldParent, oldIndex];
  }

  // "For each boundary point whose node is old parent and whose offset is
  // greater than old index, subtract one from its offset."
  if (point[0] == oldParent && point[1] > oldIndex) {
    return [point[0], point[1] - 1];
  }

  return point;
}

// Update the given boundary point [node, offset] to account for the fact that
// insertedNode was just inserted into its current position.  This must be
// called AFTER insertedNode was already inserted.
function modifyForInsert(insertedNode, point) {
  // "For each boundary point whose node is the new parent of the affected
  // node and whose offset is greater than the new index of the affected
  // node, add one to the boundary point's offset."
  if (point[0] == insertedNode.parentNode && point[1] > indexOf(insertedNode)) {
    return [point[0], point[1] + 1];
  }

  return point;
}


function testInsertBefore(newParent, affectedNode, refNode, startContainer, startOffset, endContainer, endOffset) {
  var expectedStart = [startContainer, startOffset];
  var expectedEnd = [endContainer, endOffset];

  expectedStart = modifyForRemove(affectedNode, expectedStart);
  expectedEnd = modifyForRemove(affectedNode, expectedEnd);

  try {
    newParent.insertBefore(affectedNode, refNode);
  } catch (e) {
    // For our purposes, assume that DOM Core is true -- i.e., ignore
    // mutation events and similar.
    return [startContainer, startOffset, endContainer, endOffset];
  }

  expectedStart = modifyForInsert(affectedNode, expectedStart);
  expectedEnd = modifyForInsert(affectedNode, expectedEnd);

  return expectedStart.concat(expectedEnd);
}

var insertBeforeTests = [
  // Moving a node to its current position
  ["testDiv", "paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 0],
  ["testDiv", "paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 1],
  ["testDiv", "paras[0]", "paras[1]", "paras[0]", 1, "paras[0]", 1],
  ["testDiv", "paras[0]", "paras[1]", "testDiv", 0, "testDiv", 2],
  ["testDiv", "paras[0]", "paras[1]", "testDiv", 1, "testDiv", 1],
  ["testDiv", "paras[0]", "paras[1]", "testDiv", 1, "testDiv", 2],
  ["testDiv", "paras[0]", "paras[1]", "testDiv", 2, "testDiv", 2],

  // Stuff that actually moves something.  Note that paras[0] and paras[1]
  // are both children of testDiv.
  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 1],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 2],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 1],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 2],
  ["paras[0]", "paras[1]", "null", "paras[0]", 0, "paras[0]", 0],
  ["paras[0]", "paras[1]", "null", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "paras[1]", "null", "paras[0]", 1, "paras[0]", 1],
  ["paras[0]", "paras[1]", "null", "testDiv", 0, "testDiv", 1],
  ["paras[0]", "paras[1]", "null", "testDiv", 0, "testDiv", 2],
  ["paras[0]", "paras[1]", "null", "testDiv", 1, "testDiv", 1],
  ["paras[0]", "paras[1]", "null", "testDiv", 1, "testDiv", 2],
  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 0],
  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 1],
  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 2],
  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 1, "foreignDoc", 1],
  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 0],
  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 1],
  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 2],
  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 1, "foreignDoc", 1],
  ["foreignDoc", "detachedComment", "null", "foreignDoc", 0, "foreignDoc", 1],
  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],

  // Stuff that throws exceptions
  ["paras[0]", "paras[0]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "testDiv", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "document", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "foreignDoc", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "document.doctype", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
];


function testReplaceChild(newParent, newChild, oldChild, startContainer, startOffset, endContainer, endOffset) {
  var expectedStart = [startContainer, startOffset];
  var expectedEnd = [endContainer, endOffset];

  expectedStart = modifyForRemove(oldChild, expectedStart);
  expectedEnd = modifyForRemove(oldChild, expectedEnd);

  if (newChild != oldChild) {
    // Don't do this twice, if they're the same!
    expectedStart = modifyForRemove(newChild, expectedStart);
    expectedEnd = modifyForRemove(newChild, expectedEnd);
  }

  try {
    newParent.replaceChild(newChild, oldChild);
  } catch (e) {
    return [startContainer, startOffset, endContainer, endOffset];
  }

  expectedStart = modifyForInsert(newChild, expectedStart);
  expectedEnd = modifyForInsert(newChild, expectedEnd);

  return expectedStart.concat(expectedEnd);
}

var replaceChildTests = [
  // Moving a node to its current position.  Doesn't match most browsers'
  // behavior, but we probably want to keep the spec the same anyway:
  // https://bugzilla.mozilla.org/show_bug.cgi?id=647603
  ["testDiv", "paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 0],
  ["testDiv", "paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 1],
  ["testDiv", "paras[0]", "paras[0]", "paras[0]", 1, "paras[0]", 1],
  ["testDiv", "paras[0]", "paras[0]", "testDiv", 0, "testDiv", 2],
  ["testDiv", "paras[0]", "paras[0]", "testDiv", 1, "testDiv", 1],
  ["testDiv", "paras[0]", "paras[0]", "testDiv", 1, "testDiv", 2],
  ["testDiv", "paras[0]", "paras[0]", "testDiv", 2, "testDiv", 2],

  // Stuff that actually moves something.
  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 1],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 0, "testDiv", 2],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 1],
  ["paras[0]", "paras[1]", "paras[0].firstChild", "testDiv", 1, "testDiv", 2],
  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 0],
  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 1],
  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", 2],
  ["foreignDoc", "detachedComment", "foreignDoc.documentElement", "foreignDoc", 1, "foreignDoc", 1],
  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 0],
  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 1],
  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 0, "foreignDoc", 2],
  ["foreignDoc", "detachedComment", "foreignDoc.doctype", "foreignDoc", 1, "foreignDoc", 1],
  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 0],
  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "xmlTextNode", "paras[0].firstChild", "paras[0]", 1, "paras[0]", 1],

  // Stuff that throws exceptions
  ["paras[0]", "paras[0]", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "testDiv", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "document", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "foreignDoc", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "document.doctype", "paras[0].firstChild", "paras[0]", 0, "paras[0]", 1],
];


function testAppendChild(newParent, affectedNode, startContainer, startOffset, endContainer, endOffset) {
  var expectedStart = [startContainer, startOffset];
  var expectedEnd = [endContainer, endOffset];

  expectedStart = modifyForRemove(affectedNode, expectedStart);
  expectedEnd = modifyForRemove(affectedNode, expectedEnd);

  try {
    newParent.appendChild(affectedNode);
  } catch (e) {
    return [startContainer, startOffset, endContainer, endOffset];
  }

  // These two lines will actually never do anything, if you think about it,
  // but let's leave them in so correctness is more obvious.
  expectedStart = modifyForInsert(affectedNode, expectedStart);
  expectedEnd = modifyForInsert(affectedNode, expectedEnd);

  return expectedStart.concat(expectedEnd);
}

var appendChildTests = [
  // Moving a node to its current position
  ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 0, "testDiv.lastChild", 0],
  ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 0, "testDiv.lastChild", 1],
  ["testDiv", "testDiv.lastChild", "testDiv.lastChild", 1, "testDiv.lastChild", 1],
  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 2", "testDiv", "testDiv.childNodes.length"],
  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 2", "testDiv", "testDiv.childNodes.length - 1"],
  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 1", "testDiv", "testDiv.childNodes.length"],
  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length - 1", "testDiv", "testDiv.childNodes.length - 1"],
  ["testDiv", "testDiv.lastChild", "testDiv", "testDiv.childNodes.length", "testDiv", "testDiv.childNodes.length"],
  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 0, "detachedDiv.lastChild", 0],
  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 0, "detachedDiv.lastChild", 1],
  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv.lastChild", 1, "detachedDiv.lastChild", 1],
  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 2", "detachedDiv", "detachedDiv.childNodes.length"],
  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 2", "detachedDiv", "detachedDiv.childNodes.length - 1"],
  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 1", "detachedDiv", "detachedDiv.childNodes.length"],
  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length - 1", "detachedDiv", "detachedDiv.childNodes.length - 1"],
  ["detachedDiv", "detachedDiv.lastChild", "detachedDiv", "detachedDiv.childNodes.length", "detachedDiv", "detachedDiv.childNodes.length"],

  // Stuff that actually moves something
  ["paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 0],
  ["paras[0]", "paras[1]", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "paras[1]", "paras[0]", 1, "paras[0]", 1],
  ["paras[0]", "paras[1]", "testDiv", 0, "testDiv", 1],
  ["paras[0]", "paras[1]", "testDiv", 0, "testDiv", 2],
  ["paras[0]", "paras[1]", "testDiv", 1, "testDiv", 1],
  ["paras[0]", "paras[1]", "testDiv", 1, "testDiv", 2],
  ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length - 1", "foreignDoc", "foreignDoc.childNodes.length"],
  ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length - 1", "foreignDoc", "foreignDoc.childNodes.length - 1"],
  ["foreignDoc", "detachedComment", "foreignDoc", "foreignDoc.childNodes.length", "foreignDoc", "foreignDoc.childNodes.length"],
  ["foreignDoc", "detachedComment", "detachedComment", 0, "detachedComment", 5],
  ["paras[0]", "xmlTextNode", "paras[0]", 0, "paras[0]", 0],
  ["paras[0]", "xmlTextNode", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "xmlTextNode", "paras[0]", 1, "paras[0]", 1],

  // Stuff that throws exceptions
  ["paras[0]", "paras[0]", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "testDiv", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "document", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "foreignDoc", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "document.doctype", "paras[0]", 0, "paras[0]", 1],
];


function testRemoveChild(affectedNode, startContainer, startOffset, endContainer, endOffset) {
  var expectedStart = [startContainer, startOffset];
  var expectedEnd = [endContainer, endOffset];

  expectedStart = modifyForRemove(affectedNode, expectedStart);
  expectedEnd = modifyForRemove(affectedNode, expectedEnd);

  // We don't test cases where the parent is wrong, so this should never
  // throw an exception.
  affectedNode.parentNode.removeChild(affectedNode);

  return expectedStart.concat(expectedEnd);
}

var removeChildTests = [
  ["paras[0]", "paras[0]", 0, "paras[0]", 0],
  ["paras[0]", "paras[0]", 0, "paras[0]", 1],
  ["paras[0]", "paras[0]", 1, "paras[0]", 1],
  ["paras[0]", "testDiv", 0, "testDiv", 0],
  ["paras[0]", "testDiv", 0, "testDiv", 1],
  ["paras[0]", "testDiv", 1, "testDiv", 1],
  ["paras[0]", "testDiv", 0, "testDiv", 2],
  ["paras[0]", "testDiv", 1, "testDiv", 2],
  ["paras[0]", "testDiv", 2, "testDiv", 2],

  ["foreignDoc.documentElement", "foreignDoc", 0, "foreignDoc", "foreignDoc.childNodes.length"],
];
