// This file has circular dependencies that may require other files. Rather
// than use import-globals-from, we list the globals individually here to save
// confusing ESLint.
// actions.js
/* globals testActionNames */
// attributes.js
/* globals testAttrs, testAbsentAttrs, testTextAttrs */
// relations.js
/* globals testRelation */
// role.js
/* globals isRole */
// state.js
/* globals testStates */

// //////////////////////////////////////////////////////////////////////////////
// Interfaces

const nsIAccessibilityService = Ci.nsIAccessibilityService;

const nsIAccessibleEvent = Ci.nsIAccessibleEvent;
const nsIAccessibleStateChangeEvent = Ci.nsIAccessibleStateChangeEvent;
const nsIAccessibleCaretMoveEvent = Ci.nsIAccessibleCaretMoveEvent;
const nsIAccessibleScrollingEvent = Ci.nsIAccessibleScrollingEvent;
const nsIAccessibleTextChangeEvent = Ci.nsIAccessibleTextChangeEvent;
const nsIAccessibleTextSelectionChangeEvent =
  Ci.nsIAccessibleTextSelectionChangeEvent;
const nsIAccessibleObjectAttributeChangedEvent =
  Ci.nsIAccessibleObjectAttributeChangedEvent;
const nsIAccessibleAnnouncementEvent = Ci.nsIAccessibleAnnouncementEvent;

const nsIAccessibleStates = Ci.nsIAccessibleStates;
const nsIAccessibleRole = Ci.nsIAccessibleRole;
const nsIAccessibleScrollType = Ci.nsIAccessibleScrollType;
const nsIAccessibleCoordinateType = Ci.nsIAccessibleCoordinateType;

const nsIAccessibleRelation = Ci.nsIAccessibleRelation;
const nsIAccessibleTextRange = Ci.nsIAccessibleTextRange;

const nsIAccessible = Ci.nsIAccessible;

const nsIAccessibleDocument = Ci.nsIAccessibleDocument;
const nsIAccessibleApplication = Ci.nsIAccessibleApplication;

const nsIAccessibleText = Ci.nsIAccessibleText;
const nsIAccessibleEditableText = Ci.nsIAccessibleEditableText;

const nsIAccessibleHyperLink = Ci.nsIAccessibleHyperLink;
const nsIAccessibleHyperText = Ci.nsIAccessibleHyperText;

const nsIAccessibleImage = Ci.nsIAccessibleImage;
const nsIAccessiblePivot = Ci.nsIAccessiblePivot;
const nsIAccessibleSelectable = Ci.nsIAccessibleSelectable;
const nsIAccessibleTable = Ci.nsIAccessibleTable;
const nsIAccessibleTableCell = Ci.nsIAccessibleTableCell;
const nsIAccessibleTraversalRule = Ci.nsIAccessibleTraversalRule;
const nsIAccessibleValue = Ci.nsIAccessibleValue;

const nsIObserverService = Ci.nsIObserverService;

const nsIDOMWindow = Ci.nsIDOMWindow;

const nsIPropertyElement = Ci.nsIPropertyElement;

// //////////////////////////////////////////////////////////////////////////////
// OS detect

const MAC = navigator.platform.includes("Mac");
const LINUX = navigator.platform.includes("Linux");
const SOLARIS = navigator.platform.includes("SunOS");
const WIN = navigator.platform.includes("Win");

// //////////////////////////////////////////////////////////////////////////////
// Application detect
// Firefox is assumed by default.

const SEAMONKEY = navigator.userAgent.match(/ SeaMonkey\//);

// //////////////////////////////////////////////////////////////////////////////
// Accessible general

const STATE_BUSY = nsIAccessibleStates.STATE_BUSY;

const SCROLL_TYPE_TOP_EDGE = nsIAccessibleScrollType.SCROLL_TYPE_TOP_EDGE;
const SCROLL_TYPE_ANYWHERE = nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE;

const COORDTYPE_SCREEN_RELATIVE =
  nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE;
const COORDTYPE_WINDOW_RELATIVE =
  nsIAccessibleCoordinateType.COORDTYPE_WINDOW_RELATIVE;
const COORDTYPE_PARENT_RELATIVE =
  nsIAccessibleCoordinateType.COORDTYPE_PARENT_RELATIVE;

const kEmbedChar = String.fromCharCode(0xfffc);

const kDiscBulletChar = String.fromCharCode(0x2022);
const kDiscBulletText = kDiscBulletChar + " ";
const kCircleBulletText = String.fromCharCode(0x25e6) + " ";
const kSquareBulletText = String.fromCharCode(0x25aa) + " ";

const MAX_TRIM_LENGTH = 100;

/**
 * Services to determine if e10s is enabled.
 */

/**
 * nsIAccessibilityService service.
 */
var gAccService = Cc["@mozilla.org/accessibilityService;1"].getService(
  nsIAccessibilityService
);

/**
 * Enable/disable logging.
 */
function enableLogging(aModules) {
  gAccService.setLogging(aModules);
}
function disableLogging() {
  gAccService.setLogging("");
}
function isLogged(aModule) {
  return gAccService.isLogged(aModule);
}

/**
 * Dumps the accessible tree into console.
 */
function dumpTree(aId, aMsg) {
  function dumpTreeIntl(acc, indent) {
    dump(indent + prettyName(acc) + "\n");

    var children = acc.children;
    for (var i = 0; i < children.length; i++) {
      var child = children.queryElementAt(i, nsIAccessible);
      dumpTreeIntl(child, indent + "  ");
    }
  }

  function dumpDOMTreeIntl(node, indent) {
    dump(indent + prettyName(node) + "\n");

    var children = node.childNodes;
    for (var i = 0; i < children.length; i++) {
      var child = children.item(i);
      dumpDOMTreeIntl(child, indent + "  ");
    }
  }

  dump(aMsg + "\n");
  var root = getAccessible(aId);
  dumpTreeIntl(root, "  ");

  dump("DOM tree:\n");
  dumpDOMTreeIntl(getNode(aId), "  ");
}

/**
 * Invokes the given function when document is loaded and focused. Preferable
 * to mochitests 'addLoadEvent' function -- additionally ensures state of the
 * document accessible is not busy.
 *
 * @param aFunc  the function to invoke
 */
function addA11yLoadEvent(aFunc, aWindow) {
  function waitForDocLoad() {
    window.setTimeout(function () {
      var targetDocument = aWindow ? aWindow.document : document;
      var accDoc = getAccessible(targetDocument);
      var state = {};
      accDoc.getState(state, {});
      if (state.value & STATE_BUSY) {
        waitForDocLoad();
        return;
      }

      window.setTimeout(aFunc, 0);
    }, 0);
  }

  if (
    aWindow &&
    aWindow.document.activeElement &&
    aWindow.document.activeElement.localName == "browser"
  ) {
    waitForDocLoad();
  } else {
    SimpleTest.waitForFocus(waitForDocLoad, aWindow);
  }
}

/**
 * Analogy of SimpleTest.is function used to compare objects.
 */
function isObject(aObj, aExpectedObj, aMsg) {
  if (aObj == aExpectedObj) {
    ok(true, aMsg);
    return;
  }

  ok(
    false,
    aMsg +
      " - got '" +
      prettyName(aObj) +
      "', expected '" +
      prettyName(aExpectedObj) +
      "'"
  );
}

/**
 * is() function checking the expected value is within the range.
 */
function isWithin(aExpected, aGot, aWithin, aMsg) {
  if (Math.abs(aGot - aExpected) <= aWithin) {
    ok(true, `${aMsg} - Got ${aGot}`);
  } else {
    ok(
      false,
      `${aMsg} - Got ${aGot}, expected ${aExpected} with error of ${aWithin}`
    );
  }
}

// //////////////////////////////////////////////////////////////////////////////
// Helpers for getting DOM node/accessible

/**
 * Return the DOM node by identifier (may be accessible, DOM node or ID).
 */
function getNode(aAccOrNodeOrID, aDocument) {
  if (!aAccOrNodeOrID) {
    return null;
  }

  if (Node.isInstance(aAccOrNodeOrID)) {
    return aAccOrNodeOrID;
  }

  if (aAccOrNodeOrID instanceof nsIAccessible) {
    return aAccOrNodeOrID.DOMNode;
  }

  var node = (aDocument || document).getElementById(aAccOrNodeOrID);
  if (!node) {
    ok(false, "Can't get DOM element for " + aAccOrNodeOrID);
    return null;
  }

  return node;
}

/**
 * Constants indicates getAccessible doesn't fail if there is no accessible.
 */
const DONOTFAIL_IF_NO_ACC = 1;

/**
 * Constants indicates getAccessible won't fail if accessible doesn't implement
 * the requested interfaces.
 */
const DONOTFAIL_IF_NO_INTERFACE = 2;

/**
 * Return accessible for the given identifier (may be ID attribute or DOM
 * element or accessible object) or null.
 *
 * @param aAccOrElmOrID      [in] identifier to get an accessible implementing
 *                           the given interfaces
 * @param aInterfaces        [in, optional] the interface or an array interfaces
 *                           to query it/them from obtained accessible
 * @param aElmObj            [out, optional] object to store DOM element which
 *                           accessible is obtained for
 * @param aDoNotFailIf       [in, optional] no error for special cases (see
 *                            constants above)
 */
function getAccessible(aAccOrElmOrID, aInterfaces, aElmObj, aDoNotFailIf) {
  if (!aAccOrElmOrID) {
    return null;
  }

  var elm = null;

  if (aAccOrElmOrID instanceof nsIAccessible) {
    try {
      elm = aAccOrElmOrID.DOMNode;
    } catch (e) {}
  } else if (Node.isInstance(aAccOrElmOrID)) {
    elm = aAccOrElmOrID;
  } else {
    elm = document.getElementById(aAccOrElmOrID);
    if (!elm) {
      ok(false, "Can't get DOM element for " + aAccOrElmOrID);
      return null;
    }
  }

  if (aElmObj && typeof aElmObj == "object") {
    aElmObj.value = elm;
  }

  var acc = aAccOrElmOrID instanceof nsIAccessible ? aAccOrElmOrID : null;
  if (!acc) {
    try {
      acc = gAccService.getAccessibleFor(elm);
    } catch (e) {}

    if (!acc) {
      if (!(aDoNotFailIf & DONOTFAIL_IF_NO_ACC)) {
        ok(false, "Can't get accessible for " + prettyName(aAccOrElmOrID));
      }

      return null;
    }
  }

  if (!aInterfaces) {
    return acc;
  }

  if (!(aInterfaces instanceof Array)) {
    aInterfaces = [aInterfaces];
  }

  for (var index = 0; index < aInterfaces.length; index++) {
    if (acc instanceof aInterfaces[index]) {
      continue;
    }
    try {
      acc.QueryInterface(aInterfaces[index]);
    } catch (e) {
      if (!(aDoNotFailIf & DONOTFAIL_IF_NO_INTERFACE)) {
        ok(
          false,
          "Can't query " + aInterfaces[index] + " for " + aAccOrElmOrID
        );
      }

      return null;
    }
  }

  return acc;
}

/**
 * Return true if the given identifier has an accessible, or exposes the wanted
 * interfaces.
 */
function isAccessible(aAccOrElmOrID, aInterfaces) {
  return !!getAccessible(
    aAccOrElmOrID,
    aInterfaces,
    null,
    DONOTFAIL_IF_NO_ACC | DONOTFAIL_IF_NO_INTERFACE
  );
}

/**
 * Return an accessible that contains the DOM node for the given identifier.
 */
function getContainerAccessible(aAccOrElmOrID) {
  var node = getNode(aAccOrElmOrID);
  if (!node) {
    return null;
  }

  // eslint-disable-next-line no-empty
  while ((node = node.parentNode) && !isAccessible(node)) {}
  return node ? getAccessible(node) : null;
}

/**
 * Return root accessible for the given identifier.
 */
function getRootAccessible(aAccOrElmOrID) {
  var acc = getAccessible(aAccOrElmOrID ? aAccOrElmOrID : document);
  return acc ? acc.rootDocument.QueryInterface(nsIAccessible) : null;
}

/**
 * Return tab document accessible the given accessible is contained by.
 */
function getTabDocAccessible(aAccOrElmOrID) {
  var acc = getAccessible(aAccOrElmOrID ? aAccOrElmOrID : document);

  var docAcc = acc.document.QueryInterface(nsIAccessible);
  var containerDocAcc = docAcc.parent.document;

  // Test is running is stand-alone mode.
  if (acc.rootDocument == containerDocAcc) {
    return docAcc;
  }

  // In the case of running all tests together.
  return containerDocAcc.QueryInterface(nsIAccessible);
}

/**
 * Return application accessible.
 */
function getApplicationAccessible() {
  return gAccService
    .getApplicationAccessible()
    .QueryInterface(nsIAccessibleApplication);
}

/**
 * A version of accessible tree testing, doesn't fail if tree is not complete.
 */
function testElm(aID, aTreeObj) {
  testAccessibleTree(aID, aTreeObj, kSkipTreeFullCheck);
}

/**
 * Flags used for testAccessibleTree
 */
const kSkipTreeFullCheck = 1;

/**
 * Compare expected and actual accessibles trees.
 *
 * @param  aAccOrElmOrID  [in] accessible identifier
 * @param  aAccTree       [in] JS object, each field corresponds to property of
 *                         accessible object. Additionally special properties
 *                         are presented:
 *                          children - an array of JS objects representing
 *                                      children of accessible
 *                          states   - an object having states and extraStates
 *                                      fields
 * @param aFlags          [in, optional] flags, see constants above
 */
// eslint-disable-next-line complexity
function testAccessibleTree(aAccOrElmOrID, aAccTree, aFlags) {
  var acc = getAccessible(aAccOrElmOrID);
  if (!acc) {
    return;
  }

  var accTree = aAccTree;

  // Support of simplified accessible tree object.
  accTree = normalizeAccTreeObj(accTree);

  // Test accessible properties.
  for (var prop in accTree) {
    var msg =
      "Wrong value of property '" + prop + "' for " + prettyName(acc) + ".";

    switch (prop) {
      case "actions": {
        testActionNames(acc, accTree.actions);
        break;
      }

      case "attributes":
        testAttrs(acc, accTree[prop], true);
        break;

      case "absentAttributes":
        testAbsentAttrs(acc, accTree[prop]);
        break;

      case "interfaces": {
        var ifaces =
          accTree[prop] instanceof Array ? accTree[prop] : [accTree[prop]];
        for (let i = 0; i < ifaces.length; i++) {
          ok(
            acc instanceof ifaces[i],
            "No " + ifaces[i] + " interface on " + prettyName(acc)
          );
        }
        break;
      }

      case "relations": {
        for (var rel in accTree[prop]) {
          testRelation(acc, window[rel], accTree[prop][rel]);
        }
        break;
      }

      case "role":
        isRole(acc, accTree[prop], msg);
        break;

      case "states":
      case "extraStates":
      case "absentStates":
      case "absentExtraStates": {
        testStates(
          acc,
          accTree.states,
          accTree.extraStates,
          accTree.absentStates,
          accTree.absentExtraStates
        );
        break;
      }

      case "tagName":
        is(accTree[prop], acc.DOMNode.tagName, msg);
        break;

      case "textAttrs": {
        var prevOffset = -1;
        for (var offset in accTree[prop]) {
          if (prevOffset != -1) {
            let attrs = accTree[prop][prevOffset];
            testTextAttrs(
              acc,
              prevOffset,
              attrs,
              {},
              prevOffset,
              +offset,
              true
            );
          }
          prevOffset = +offset;
        }

        if (prevOffset != -1) {
          var charCount = getAccessible(acc, [
            nsIAccessibleText,
          ]).characterCount;
          let attrs = accTree[prop][prevOffset];
          testTextAttrs(
            acc,
            prevOffset,
            attrs,
            {},
            prevOffset,
            charCount,
            true
          );
        }

        break;
      }

      default:
        if (prop.indexOf("todo_") == 0) {
          todo(false, msg);
        } else if (prop != "children") {
          is(acc[prop], accTree[prop], msg);
        }
    }
  }

  // Test children.
  if ("children" in accTree && accTree.children instanceof Array) {
    var children = acc.children;
    var childCount = children.length;

    if (accTree.children.length != childCount) {
      for (let i = 0; i < Math.max(accTree.children.length, childCount); i++) {
        var accChild = null,
          testChild = null;
        try {
          testChild = accTree.children[i];
          accChild = children.queryElementAt(i, nsIAccessible);

          if (!testChild) {
            ok(
              false,
              prettyName(acc) +
                " has an extra child at index " +
                i +
                " : " +
                prettyName(accChild)
            );
            continue;
          }

          testChild = normalizeAccTreeObj(testChild);
          if (accChild.role !== testChild.role) {
            ok(
              false,
              prettyName(accTree) +
                " and " +
                prettyName(acc) +
                " have different children at index " +
                i +
                " : " +
                prettyName(testChild) +
                ", " +
                prettyName(accChild)
            );
          }
          info(
            "Matching " +
              prettyName(accTree) +
              " and " +
              prettyName(acc) +
              " child at index " +
              i +
              " : " +
              prettyName(accChild)
          );
        } catch (e) {
          ok(
            false,
            prettyName(accTree) +
              " is expected to have a child at index " +
              i +
              " : " +
              prettyName(testChild) +
              ", original tested: " +
              prettyName(aAccOrElmOrID) +
              ", " +
              e
          );
        }
      }
    } else {
      if (aFlags & kSkipTreeFullCheck) {
        for (let i = 0; i < childCount; i++) {
          let child = children.queryElementAt(i, nsIAccessible);
          testAccessibleTree(child, accTree.children[i], aFlags);
        }
        return;
      }

      // nsIAccessible::firstChild
      var expectedFirstChild =
        childCount > 0 ? children.queryElementAt(0, nsIAccessible) : null;
      var firstChild = null;
      try {
        firstChild = acc.firstChild;
      } catch (e) {}
      is(
        firstChild,
        expectedFirstChild,
        "Wrong first child of " + prettyName(acc)
      );

      // nsIAccessible::lastChild
      var expectedLastChild =
        childCount > 0
          ? children.queryElementAt(childCount - 1, nsIAccessible)
          : null;
      var lastChild = null;
      try {
        lastChild = acc.lastChild;
      } catch (e) {}
      is(
        lastChild,
        expectedLastChild,
        "Wrong last child of " + prettyName(acc)
      );

      for (var i = 0; i < childCount; i++) {
        let child = children.queryElementAt(i, nsIAccessible);

        // nsIAccessible::parent
        var parent = null;
        try {
          parent = child.parent;
        } catch (e) {}
        is(parent, acc, "Wrong parent of " + prettyName(child));

        // nsIAccessible::indexInParent
        var indexInParent = -1;
        try {
          indexInParent = child.indexInParent;
        } catch (e) {}
        is(indexInParent, i, "Wrong index in parent of " + prettyName(child));

        // nsIAccessible::nextSibling
        var expectedNextSibling =
          i < childCount - 1
            ? children.queryElementAt(i + 1, nsIAccessible)
            : null;
        var nextSibling = null;
        try {
          nextSibling = child.nextSibling;
        } catch (e) {}
        is(
          nextSibling,
          expectedNextSibling,
          "Wrong next sibling of " + prettyName(child)
        );

        // nsIAccessible::previousSibling
        var expectedPrevSibling =
          i > 0 ? children.queryElementAt(i - 1, nsIAccessible) : null;
        var prevSibling = null;
        try {
          prevSibling = child.previousSibling;
        } catch (e) {}
        is(
          prevSibling,
          expectedPrevSibling,
          "Wrong previous sibling of " + prettyName(child)
        );

        // Go down through subtree
        testAccessibleTree(child, accTree.children[i], aFlags);
      }
    }
  }
}

/**
 * Return true if accessible for the given node is in cache.
 */
function isAccessibleInCache(aNodeOrId) {
  var node = getNode(aNodeOrId);
  return !!gAccService.getAccessibleFromCache(node);
}

/**
 * Test accessible tree for defunct accessible.
 *
 * @param  aAcc       [in] the defunct accessible
 * @param  aNodeOrId  [in] the DOM node identifier for the defunct accessible
 */
function testDefunctAccessible(aAcc, aNodeOrId) {
  if (aNodeOrId) {
    ok(
      !isAccessible(aNodeOrId),
      "Accessible for " + aNodeOrId + " wasn't properly shut down!"
    );
  }

  var msg =
    " doesn't fail for shut down accessible " + prettyName(aNodeOrId) + "!";

  // firstChild
  var success = false;
  try {
    aAcc.firstChild;
  } catch (e) {
    success = e.result == Cr.NS_ERROR_FAILURE;
  }
  ok(success, "firstChild" + msg);

  // lastChild
  success = false;
  try {
    aAcc.lastChild;
  } catch (e) {
    success = e.result == Cr.NS_ERROR_FAILURE;
  }
  ok(success, "lastChild" + msg);

  // childCount
  success = false;
  try {
    aAcc.childCount;
  } catch (e) {
    success = e.result == Cr.NS_ERROR_FAILURE;
  }
  ok(success, "childCount" + msg);

  // children
  success = false;
  try {
    aAcc.children;
  } catch (e) {
    success = e.result == Cr.NS_ERROR_FAILURE;
  }
  ok(success, "children" + msg);

  // nextSibling
  success = false;
  try {
    aAcc.nextSibling;
  } catch (e) {
    success = e.result == Cr.NS_ERROR_FAILURE;
  }
  ok(success, "nextSibling" + msg);

  // previousSibling
  success = false;
  try {
    aAcc.previousSibling;
  } catch (e) {
    success = e.result == Cr.NS_ERROR_FAILURE;
  }
  ok(success, "previousSibling" + msg);

  // parent
  success = false;
  try {
    aAcc.parent;
  } catch (e) {
    success = e.result == Cr.NS_ERROR_FAILURE;
  }
  ok(success, "parent" + msg);
}

/**
 * Convert role to human readable string.
 */
function roleToString(aRole) {
  return gAccService.getStringRole(aRole);
}

/**
 * Convert states to human readable string.
 */
function statesToString(aStates, aExtraStates) {
  var list = gAccService.getStringStates(aStates, aExtraStates);

  var str = "";
  for (var index = 0; index < list.length - 1; index++) {
    str += list.item(index) + ", ";
  }

  if (list.length) {
    str += list.item(index);
  }

  return str;
}

/**
 * Convert event type to human readable string.
 */
function eventTypeToString(aEventType) {
  return gAccService.getStringEventType(aEventType);
}

/**
 * Convert relation type to human readable string.
 */
function relationTypeToString(aRelationType) {
  return gAccService.getStringRelationType(aRelationType);
}

function getLoadContext() {
  return window.docShell.QueryInterface(Ci.nsILoadContext);
}

/**
 * Return text from clipboard.
 */
function getTextFromClipboard() {
  var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
    Ci.nsITransferable
  );
  trans.init(getLoadContext());
  if (!trans) {
    return "";
  }

  trans.addDataFlavor("text/plain");
  Services.clipboard.getData(
    trans,
    Services.clipboard.kGlobalClipboard,
    SpecialPowers.wrap(window).browsingContext.currentWindowContext
  );

  var str = {};
  trans.getTransferData("text/plain", str);

  if (str) {
    str = str.value.QueryInterface(Ci.nsISupportsString);
  }
  if (str) {
    return str.data;
  }

  return "";
}

/**
 * Extract DOMNode id from an accessible. If e10s is enabled, DOMNode is not
 * present in parent process but, if available, DOMNode id is attached to an
 * accessible object.
 * @param  {nsIAccessible} accessible  accessible
 * @return {String?}                   DOMNode id if available
 */
function getAccessibleDOMNodeID(accessible) {
  if (accessible instanceof nsIAccessibleDocument) {
    // If accessible is a document, trying to find its document body id.
    try {
      return accessible.DOMNode.body.id;
    } catch (e) {
      /* This only works if accessible is not a proxy. */
    }
  }
  try {
    return accessible.DOMNode.id;
  } catch (e) {
    /* This will fail if DOMNode is in different process. */
  }
  try {
    // When e10s is enabled, accessible will have an "id" property if its
    // corresponding DOMNode has an id. If accessible is a document, its "id"
    // property corresponds to the "id" of its body element.
    return accessible.id;
  } catch (e) {
    /* This will fail if accessible is not a proxy. */
  }
  return null;
}

/**
 * Return pretty name for identifier, it may be ID, DOM node or accessible.
 */
function prettyName(aIdentifier) {
  if (aIdentifier instanceof Array) {
    let msg = "";
    for (var idx = 0; idx < aIdentifier.length; idx++) {
      if (msg != "") {
        msg += ", ";
      }

      msg += prettyName(aIdentifier[idx]);
    }
    return msg;
  }

  if (aIdentifier instanceof nsIAccessible) {
    var acc = getAccessible(aIdentifier);
    var domID = getAccessibleDOMNodeID(acc);
    let msg = "[";
    try {
      if (Services.appinfo.browserTabsRemoteAutostart) {
        if (domID) {
          msg += `DOM node id: ${domID}, `;
        }
      } else {
        msg += `${getNodePrettyName(acc.DOMNode)}, `;
      }
      msg += "role: " + roleToString(acc.role);
      if (acc.name) {
        msg += ", name: '" + shortenString(acc.name) + "'";
      }
    } catch (e) {
      msg += "defunct";
    }

    if (acc) {
      msg += ", address: " + getObjAddress(acc);
    }
    msg += "]";

    return msg;
  }

  if (Node.isInstance(aIdentifier)) {
    return "[ " + getNodePrettyName(aIdentifier) + " ]";
  }

  if (aIdentifier && typeof aIdentifier === "object") {
    var treeObj = normalizeAccTreeObj(aIdentifier);
    if ("role" in treeObj) {
      function stringifyTree(aObj) {
        var text = roleToString(aObj.role) + ": [ ";
        if ("children" in aObj) {
          for (var i = 0; i < aObj.children.length; i++) {
            var c = normalizeAccTreeObj(aObj.children[i]);
            text += stringifyTree(c);
            if (i < aObj.children.length - 1) {
              text += ", ";
            }
          }
        }
        return text + "] ";
      }
      return `{ ${stringifyTree(treeObj)} }`;
    }
    return JSON.stringify(aIdentifier);
  }

  return " '" + aIdentifier + "' ";
}

/**
 * Shorten a long string if it exceeds MAX_TRIM_LENGTH.
 * @param aString the string to shorten.
 * @returns the shortened string.
 */
function shortenString(aString) {
  if (aString.length <= MAX_TRIM_LENGTH) {
    return aString;
  }

  // Trim the string if its length is > MAX_TRIM_LENGTH characters.
  var trimOffset = MAX_TRIM_LENGTH / 2;
  return (
    aString.substring(0, trimOffset - 1) +
    "..." +
    aString.substring(aString.length - trimOffset, aString.length)
  );
}

// //////////////////////////////////////////////////////////////////////////////
// General Utils
// //////////////////////////////////////////////////////////////////////////////
/**
 * Return main chrome window (crosses chrome boundary)
 */
function getMainChromeWindow(aWindow) {
  return aWindow.browsingContext.topChromeWindow;
}

// //////////////////////////////////////////////////////////////////////////////
// Private
// //////////////////////////////////////////////////////////////////////////////

// //////////////////////////////////////////////////////////////////////////////
// Accessible general

function getNodePrettyName(aNode) {
  try {
    var tag = "";
    if (aNode.nodeType == Node.DOCUMENT_NODE) {
      tag = "document";
    } else {
      tag = aNode.localName;
      if (aNode.nodeType == Node.ELEMENT_NODE && aNode.hasAttribute("id")) {
        tag += '@id="' + aNode.getAttribute("id") + '"';
      }
    }

    return "'" + tag + " node', address: " + getObjAddress(aNode);
  } catch (e) {
    return "' no node info '";
  }
}

function getObjAddress(aObj) {
  var exp = /native\s*@\s*(0x[a-f0-9]+)/g;
  var match = exp.exec(aObj.toString());
  if (match) {
    return match[1];
  }

  return aObj.toString();
}

function normalizeAccTreeObj(aObj) {
  var key = Object.keys(aObj)[0];
  var roleName = "ROLE_" + key;
  if (roleName in nsIAccessibleRole) {
    return {
      role: nsIAccessibleRole[roleName],
      children: aObj[key],
    };
  }
  return aObj;
}
