/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/* import-globals-from inspector-helpers.js */
Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/server/tests/browser/inspector-helpers.js", this);

function loadSelector(walker, selector) {
  return walker.querySelectorAll(walker.rootNode, selector).then(nodeList => {
    return nodeList.items();
  });
}

function loadSelectors(walker, selectors) {
  return Promise.all(Array.from(selectors, (sel) => loadSelector(walker, sel)));
}

function doMoves(movesArg) {
  return ContentTask.spawn(gBrowser.selectedBrowser, movesArg, function(moves) {
    function setParent(nodeSelector, newParentSelector) {
      const node = content.document.querySelector(nodeSelector);
      if (newParentSelector) {
        const newParent = content.document.querySelector(newParentSelector);
        newParent.appendChild(node);
      } else {
        node.remove();
      }
    }
    for (const move of moves) {
      setParent(move[0], move[1]);
    }
  });
}

/**
 * Test a set of tree rearrangements and make sure they cause the expected changes.
 */

var gDummySerial = 0;

function mutationTest(testSpec) {
  return async function() {
    const { walker } =
      await initInspectorFront(MAIN_DOMAIN + "inspector-traversal-data.html");
    await loadSelectors(walker, testSpec.load || ["html"]);
    walker.autoCleanup = !!testSpec.autoCleanup;
    if (testSpec.preCheck) {
      testSpec.preCheck();
    }
    const onMutations = walker.once("mutations");

    await doMoves(testSpec.moves || []);

    // Some of these moves will trigger no mutation events,
    // so do a dummy change to the root node to trigger
    // a mutation event anyway.
    await ContentTask.spawn(gBrowser.selectedBrowser, [gDummySerial++],
      function(serial) {
        content.document.documentElement.setAttribute("data-dummy", serial);
      });

    let mutations = await onMutations;

    // Filter out our dummy mutation.
    mutations = mutations.filter(change => {
      if (change.type == "attributes" &&
          change.attributeName == "data-dummy") {
        return false;
      }
      return true;
    });
    await assertOwnershipTrees(walker);
    if (testSpec.postCheck) {
      testSpec.postCheck(walker, mutations);
    }
  };
}

// Verify that our dummy mutation works.
add_task(mutationTest({
  autoCleanup: false,
  postCheck: function(walker, mutations) {
    is(mutations.length, 0, "Dummy mutation is filtered out.");
  },
}));

// Test a simple move to a different location in the sibling list for the same
// parent.
add_task(mutationTest({
  autoCleanup: false,
  load: ["#longlist div"],
  moves: [
    ["#a", "#longlist"],
  ],
  postCheck: function(walker, mutations) {
    const remove = mutations[0];
    is(remove.type, "childList", "First mutation should be a childList.");
    ok(remove.removed.length > 0, "First mutation should be a removal.");
    const add = mutations[1];
    is(add.type, "childList", "Second mutation should be a childList removal.");
    ok(add.added.length > 0, "Second mutation should be an addition.");
    const a = add.added[0];
    is(a.id, "a", "Added node should be #a");
    is(a.parentNode(), remove.target, "Should still be a child of longlist.");
    is(remove.target, add.target,
       "First and second mutations should be against the same node.");
  },
}));

// Test a move to another location that is within our ownership tree.
add_task(mutationTest({
  autoCleanup: false,
  load: ["#longlist div", "#longlist-sibling"],
  moves: [
    ["#a", "#longlist-sibling"],
  ],
  postCheck: function(walker, mutations) {
    const remove = mutations[0];
    is(remove.type, "childList", "First mutation should be a childList.");
    ok(remove.removed.length > 0, "First mutation should be a removal.");
    const add = mutations[1];
    is(add.type, "childList", "Second mutation should be a childList removal.");
    ok(add.added.length > 0, "Second mutation should be an addition.");
    const a = add.added[0];
    is(a.id, "a", "Added node should be #a");
    is(a.parentNode(), add.target, "Should still be a child of longlist.");
    is(add.target.id, "longlist-sibling", "long-sibling should be the target.");
  },
}));

// Move an unseen node with a seen parent into our ownership tree - should generate a
// childList pair with no adds or removes.
add_task(mutationTest({
  autoCleanup: false,
  load: ["#longlist"],
  moves: [
    ["#longlist-sibling", "#longlist"],
  ],
  postCheck: function(walker, mutations) {
    is(mutations.length, 2, "Should generate two mutations");
    is(mutations[0].type, "childList", "Should be childList mutations.");
    is(mutations[0].added.length, 0, "Should have no adds.");
    is(mutations[0].removed.length, 0, "Should have no removes.");
    is(mutations[1].type, "childList", "Should be childList mutations.");
    is(mutations[1].added.length, 0, "Should have no adds.");
    is(mutations[1].removed.length, 0, "Should have no removes.");
  },
}));

// Move an unseen node with an unseen parent into our ownership tree.  Should only
// generate one childList mutation with no adds or removes.
add_task(mutationTest({
  autoCleanup: false,
  load: ["#longlist div"],
  moves: [
    ["#longlist-sibling-firstchild", "#longlist"],
  ],
  postCheck: function(walker, mutations) {
    is(mutations.length, 1, "Should generate two mutations");
    is(mutations[0].type, "childList", "Should be childList mutations.");
    is(mutations[0].added.length, 0, "Should have no adds.");
    is(mutations[0].removed.length, 0, "Should have no removes.");
  },
}));

// Move a node between unseen nodes, should generate no mutations.
add_task(mutationTest({
  autoCleanup: false,
  load: ["html"],
  moves: [
    ["#longlist-sibling", "#longlist"],
  ],
  postCheck: function(walker, mutations) {
    is(mutations.length, 0, "Should generate no mutations.");
  },
}));

// Orphan a node and don't clean it up
add_task(mutationTest({
  autoCleanup: false,
  load: ["#longlist div"],
  moves: [
    ["#longlist", null],
  ],
  postCheck: function(walker, mutations) {
    is(mutations.length, 1, "Should generate one mutation.");
    const change = mutations[0];
    is(change.type, "childList", "Should be a childList.");
    is(change.removed.length, 1, "Should have removed a child.");
    const ownership = clientOwnershipTree(walker);
    is(ownership.orphaned.length, 1, "Should have one orphaned subtree.");
    is(ownershipTreeSize(ownership.orphaned[0]), 1 + 26 + 26,
       "Should have orphaned longlist, and 26 children, and 26 singleTextChilds");
  },
}));

// Orphan a node, and do clean it up.
add_task(mutationTest({
  autoCleanup: true,
  load: ["#longlist div"],
  moves: [
    ["#longlist", null],
  ],
  postCheck: function(walker, mutations) {
    is(mutations.length, 1, "Should generate one mutation.");
    const change = mutations[0];
    is(change.type, "childList", "Should be a childList.");
    is(change.removed.length, 1, "Should have removed a child.");
    const ownership = clientOwnershipTree(walker);
    is(ownership.orphaned.length, 0, "Should have no orphaned subtrees.");
  },
}));

// Orphan a node by moving it into the tree but out of our visible subtree.
add_task(mutationTest({
  autoCleanup: false,
  load: ["#longlist div"],
  moves: [
    ["#longlist", "#longlist-sibling"],
  ],
  postCheck: function(walker, mutations) {
    is(mutations.length, 1, "Should generate one mutation.");
    const change = mutations[0];
    is(change.type, "childList", "Should be a childList.");
    is(change.removed.length, 1, "Should have removed a child.");
    const ownership = clientOwnershipTree(walker);
    is(ownership.orphaned.length, 1, "Should have one orphaned subtree.");
    is(ownershipTreeSize(ownership.orphaned[0]), 1 + 26 + 26,
       "Should have orphaned longlist, 26 children, and 26 singleTextChilds.");
  },
}));

// Orphan a node by moving it into the tree but out of our visible subtree,
// and clean it up.
add_task(mutationTest({
  autoCleanup: true,
  load: ["#longlist div"],
  moves: [
    ["#longlist", "#longlist-sibling"],
  ],
  postCheck: function(walker, mutations) {
    is(mutations.length, 1, "Should generate one mutation.");
    const change = mutations[0];
    is(change.type, "childList", "Should be a childList.");
    is(change.removed.length, 1, "Should have removed a child.");
    const ownership = clientOwnershipTree(walker);
    is(ownership.orphaned.length, 0, "Should have no orphaned subtrees.");
  },
}));
