/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

async function check_has_child(aParentGuid, aChildGuid) {
  let parentTree = await PlacesUtils.promiseBookmarksTree(aParentGuid);
  do_check_true("children" in parentTree);
  do_check_true(parentTree.children.find( e => e.guid == aChildGuid ) != null);
}

async function compareToNode(aItem, aNode, aIsRootItem, aExcludedGuids = []) {
  // itemId==-1 indicates a non-bookmark node, which is unexpected.
  do_check_neq(aNode.itemId, -1);

  function check_unset(...aProps) {
    aProps.forEach( p => { do_check_false(p in aItem); } );
  }
  function strict_eq_check(v1, v2) {
    dump("v1: " + v1 + " v2: " + v2 + "\n");
    do_check_eq(typeof v1, typeof v2);
    do_check_eq(v1, v2);
  }
  function compare_prop(aItemProp, aNodeProp = aItemProp, aOptional = false) {
    if (aOptional && aNode[aNodeProp] === null)
      check_unset(aItemProp);
    else
      strict_eq_check(aItem[aItemProp], aNode[aNodeProp]);
  }
  function compare_prop_to_value(aItemProp, aValue, aOptional = true) {
    if (aOptional && aValue === null)
      check_unset(aItemProp);
    else
      strict_eq_check(aItem[aItemProp], aValue);
  }

  // Bug 1013053 - bookmarkIndex is unavailable for the query's root
  if (aNode.bookmarkIndex == -1) {
    compare_prop_to_value("index",
                          PlacesUtils.bookmarks.getItemIndex(aNode.itemId),
                          false);
  } else {
    compare_prop("index", "bookmarkIndex");
  }

  compare_prop("dateAdded");
  compare_prop("lastModified");

  if (aIsRootItem && aNode.itemId != PlacesUtils.placesRootId) {
    do_check_true("parentGuid" in aItem);
    await check_has_child(aItem.parentGuid, aItem.guid)
  } else {
    check_unset("parentGuid");
  }

  let expectedAnnos = PlacesUtils.getAnnotationsForItem(aItem.id);
  if (expectedAnnos.length > 0) {
    let annosToString = annos => {
      return annos.map(a => a.name + ":" + a.value).sort().join(",");
    };
    do_check_true(Array.isArray(aItem.annos))
    do_check_eq(annosToString(aItem.annos), annosToString(expectedAnnos));
  } else {
    check_unset("annos");
  }
  const BOOKMARK_ONLY_PROPS = ["uri", "iconuri", "tags", "charset", "keyword"];
  const FOLDER_ONLY_PROPS = ["children", "root"];

  let nodesCount = 1;

  switch (aNode.type) {
    case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
      do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
      compare_prop("title", "title", true);
      check_unset(...BOOKMARK_ONLY_PROPS);

      let expectedChildrenNodes = [];

      PlacesUtils.asContainer(aNode);
      if (!aNode.containerOpen)
        aNode.containerOpen = true;

      for (let i = 0; i < aNode.childCount; i++) {
        let childNode = aNode.getChild(i);
        if (childNode.itemId == PlacesUtils.tagsFolderId ||
            aExcludedGuids.includes(childNode.bookmarkGuid)) {
          continue;
        }
        expectedChildrenNodes.push(childNode);
      }

      if (expectedChildrenNodes.length > 0) {
        do_check_true(Array.isArray(aItem.children));
        do_check_eq(aItem.children.length, expectedChildrenNodes.length);
        for (let i = 0; i < aItem.children.length; i++) {
          nodesCount +=
            await compareToNode(aItem.children[i], expectedChildrenNodes[i],
                                false, aExcludedGuids);
        }
      } else {
        check_unset("children");
      }

      switch (aItem.id) {
      case PlacesUtils.placesRootId:
        compare_prop_to_value("root", "placesRoot");
        break;
      case PlacesUtils.bookmarksMenuFolderId:
        compare_prop_to_value("root", "bookmarksMenuFolder");
        break;
      case PlacesUtils.toolbarFolderId:
        compare_prop_to_value("root", "toolbarFolder");
        break;
      case PlacesUtils.unfiledBookmarksFolderId:
        compare_prop_to_value("root", "unfiledBookmarksFolder");
        break;
      case PlacesUtils.mobileFolderId:
        compare_prop_to_value("root", "mobileFolder");
        break;
      default:
        check_unset("root");
      }
      break;
    case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
      do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
      check_unset(...BOOKMARK_ONLY_PROPS, ...FOLDER_ONLY_PROPS);
      break;
    default:
      do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE);
      compare_prop("uri");
      // node.tags's format is "a, b" whilst promiseBoookmarksTree is "a,b"
      if (aNode.tags === null)
        check_unset("tags");
      else
        compare_prop_to_value("tags", aNode.tags.replace(/, /g, ","), false);

      if (aNode.icon) {
        try {
          await compareFavicons(aNode.icon, aItem.iconuri);
        } catch (ex) {
          do_print(ex);
          todo_check_true(false);
        }
      } else {
        check_unset(aItem.iconuri);
      }

      check_unset(...FOLDER_ONLY_PROPS);

      let itemURI = uri(aNode.uri);
      compare_prop_to_value("charset",
                            await PlacesUtils.getCharsetForURI(itemURI));

      let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });
      compare_prop_to_value("keyword", entry ? entry.keyword : null);

      if ("title" in aItem)
        compare_prop("title");
      else
        do_check_null(aNode.title);
  }

  if (aIsRootItem)
    do_check_eq(aItem.itemsCount, nodesCount);

  return nodesCount;
}

var itemsCount = 0;
async function new_bookmark(aInfo) {
  ++itemsCount;
  if (!("url" in aInfo))
    aInfo.url = uri("http://test.item." + itemsCount);

  if (!("title" in aInfo))
    aInfo.title = "Test Item (bookmark) " + itemsCount;

  await PlacesTransactions.NewBookmark(aInfo).transact();
}

async function new_folder(aInfo) {
  if (!("title" in aInfo))
    aInfo.title = "Test Item (folder) " + itemsCount;
  return await PlacesTransactions.NewFolder(aInfo).transact();
}

// Walks a result nodes tree and test promiseBookmarksTree for each node.
// DO NOT COPY THIS LOGIC:  It is done here to accomplish a more comprehensive
// test of the API (the entire hierarchy data is available in the very test).
async function test_promiseBookmarksTreeForEachNode(aNode, aOptions, aExcludedGuids) {
  do_check_true(aNode.bookmarkGuid && aNode.bookmarkGuid.length > 0);
  let item = await PlacesUtils.promiseBookmarksTree(aNode.bookmarkGuid, aOptions);
  await compareToNode(item, aNode, true, aExcludedGuids);

  for (let i = 0; i < aNode.childCount; i++) {
    let child = aNode.getChild(i);
    if (child.itemId != PlacesUtils.tagsFolderId)
      await test_promiseBookmarksTreeForEachNode(child,
                                                 { includeItemIds: true },
                                                 aExcludedGuids);
  }
  return item;
}

async function test_promiseBookmarksTreeAgainstResult(aItemGuid = "",
                                                 aOptions = { includeItemIds: true },
                                                 aExcludedGuids) {
  let itemId = aItemGuid ?
    await PlacesUtils.promiseItemId(aItemGuid) : PlacesUtils.placesRootId;
  let node = PlacesUtils.getFolderContents(itemId).root;
  return await test_promiseBookmarksTreeForEachNode(node, aOptions, aExcludedGuids);
}

add_task(async function() {
  // Add some bookmarks to cover various use cases.
  await new_bookmark({ parentGuid: PlacesUtils.bookmarks.toolbarGuid });
  await new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid,
                     annotations: [{ name: "TestAnnoA", value: "TestVal" },
                                   { name: "TestAnnoB", value: 0 }]});
  let sepInfo = { parentGuid: PlacesUtils.bookmarks.menuGuid };
  await PlacesTransactions.NewSeparator(sepInfo).transact();
  let folderGuid = await new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid });
  await new_bookmark({ title: null,
                       parentGuid: folderGuid,
                       keyword: "test_keyword",
                       tags: ["TestTagA", "TestTagB"],
                       annotations: [{ name: "TestAnnoA", value: "TestVal2"}]});
  let urlWithCharsetAndFavicon = uri("http://charset.and.favicon");
  await new_bookmark({ parentGuid: folderGuid, url: urlWithCharsetAndFavicon });
  await PlacesUtils.setCharsetForURI(urlWithCharsetAndFavicon, "UTF-8");
  await setFaviconForPage(urlWithCharsetAndFavicon, SMALLPNG_DATA_URI);
  // Test the default places root without specifying it.
  await test_promiseBookmarksTreeAgainstResult();

  // Do specify it
  await test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid);

  // Exclude the bookmarks menu.
  // The calllback should be four times - once for the toolbar, once for
  // the bookmark we inserted under, and once for the menu (and not
  // at all for any of its descendants) and once for the unsorted bookmarks
  // folder.  However, promiseBookmarksTree is called multiple times, so
  // rather than counting the calls, we count the number of unique items
  // passed in.
  let guidsPassedToExcludeCallback = new Set();
  let placesRootWithoutTheMenu =
  await test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid, {
    excludeItemsCallback: aItem => {
      guidsPassedToExcludeCallback.add(aItem.guid);
      return aItem.root == "bookmarksMenuFolder";
    },
    includeItemIds: true
  }, [PlacesUtils.bookmarks.menuGuid]);
  do_check_eq(guidsPassedToExcludeCallback.size, 5);
  do_check_eq(placesRootWithoutTheMenu.children.length, 3);
});
