/* 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";

/* exported assertOwnershipTrees, checkMissing, waitForMutation,
   isSrcChange, isUnretained, isNewRoot,
   assertSrcChange, assertUnload, assertFrameLoad, assertChildList,
*/

function serverOwnershipTree(walkerArg) {
  return ContentTask.spawn(gBrowser.selectedBrowser, [walkerArg.actorID],
    function(actorID) {
      const { require } =
        ChromeUtils.import("resource://devtools/shared/Loader.jsm");
      const { DebuggerServer } = require("devtools/server/main");
      const { DocumentWalker } =
        require("devtools/server/actors/inspector/document-walker");

      // Convert actorID to current compartment string otherwise
      // searchAllConnectionsForActor is confused and won't find the actor.
      actorID = String(actorID);
      const serverWalker = DebuggerServer.searchAllConnectionsForActor(actorID);

      function sortOwnershipChildrenContentScript(children) {
        return children.sort((a, b) => a.name.localeCompare(b.name));
      }

      function serverOwnershipSubtree(walker, node) {
        const actor = walker._refMap.get(node);
        if (!actor) {
          return undefined;
        }

        const children = [];
        const docwalker = new DocumentWalker(node, content);
        let child = docwalker.firstChild();
        while (child) {
          const item = serverOwnershipSubtree(walker, child);
          if (item) {
            children.push(item);
          }
          child = docwalker.nextSibling();
        }
        return {
          name: actor.actorID,
          children: sortOwnershipChildrenContentScript(children),
        };
      }
      return {
        root: serverOwnershipSubtree(serverWalker, serverWalker.rootDoc),
        orphaned: [...serverWalker._orphaned]
                  .map(o => serverOwnershipSubtree(serverWalker, o.rawNode)),
        retained: [...serverWalker._retainedOrphans]
                  .map(o => serverOwnershipSubtree(serverWalker, o.rawNode)),
      };
    });
}

function sortOwnershipChildren(children) {
  return children.sort((a, b) => a.name.localeCompare(b.name));
}

function clientOwnershipSubtree(node) {
  return {
    name: node.actorID,
    children: sortOwnershipChildren(node.treeChildren()
              .map(child => clientOwnershipSubtree(child))),
  };
}

function clientOwnershipTree(walker) {
  return {
    root: clientOwnershipSubtree(walker.rootNode),
    orphaned: [...walker._orphaned].map(o => clientOwnershipSubtree(o)),
    retained: [...walker._retainedOrphans].map(o => clientOwnershipSubtree(o)),
  };
}

function ownershipTreeSize(tree) {
  let size = 1;
  for (const child of tree.children) {
    size += ownershipTreeSize(child);
  }
  return size;
}

async function assertOwnershipTrees(walker) {
  const serverTree = await serverOwnershipTree(walker);
  const clientTree = clientOwnershipTree(walker);
  is(JSON.stringify(clientTree, null, " "), JSON.stringify(serverTree, null, " "),
     "Server and client ownership trees should match.");

  return ownershipTreeSize(clientTree.root);
}

// Verify that an actorID is inaccessible both from the client library and the server.
function checkMissing({client}, actorID) {
  return new Promise(resolve => {
    const front = client.getActor(actorID);
    ok(!front, "Front shouldn't be accessible from the client for actorID: " + actorID);

    client.request({
      to: actorID,
      type: "request",
    }, response => {
      is(response.error, "noSuchActor",
        "node list actor should no longer be contactable.");
      resolve(undefined);
    });
  });
}

// Load mutations aren't predictable, so keep accumulating mutations until
// the one we're looking for shows up.
function waitForMutation(walker, test, mutations = []) {
  return new Promise(resolve => {
    for (const change of mutations) {
      if (test(change)) {
        resolve(mutations);
      }
    }

    walker.once("mutations", newMutations => {
      waitForMutation(walker, test, mutations.concat(newMutations))
        .then(finalMutations => {
          resolve(finalMutations);
        });
    });
  });
}

function assertAndStrip(mutations, message, test) {
  const size = mutations.length;
  mutations = mutations.filter(test);
  ok((mutations.size != size), message);
  return mutations;
}

function isSrcChange(change) {
  return change.type === "attributes" && change.attributeName === "src";
}

function isUnload(change) {
  return change.type === "documentUnload";
}

function isFrameLoad(change) {
  return change.type === "frameLoad";
}

function isUnretained(change) {
  return change.type === "unretained";
}

function isChildList(change) {
  return change.type === "childList";
}

function isNewRoot(change) {
  return change.type === "newRoot";
}

// Make sure an iframe's src attribute changed and then
// strip that mutation out of the list.
function assertSrcChange(mutations) {
  return assertAndStrip(mutations, "Should have had an iframe source change.",
                        isSrcChange);
}

// Make sure there's an unload in the mutation list and strip
// that mutation out of the list
function assertUnload(mutations) {
  return assertAndStrip(mutations, "Should have had a document unload change.", isUnload);
}

// Make sure there's a frame load in the mutation list and strip
// that mutation out of the list
function assertFrameLoad(mutations) {
  return assertAndStrip(mutations, "Should have had a frame load change.", isFrameLoad);
}

// Make sure there's a childList change in the mutation list and strip
// that mutation out of the list
function assertChildList(mutations) {
  return assertAndStrip(mutations, "Should have had a frame load change.", isChildList);
}
