/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

add_task(function* insert_separator_notification() {
  let observer = expectNotifications();
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid});
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
  observer.check([ { name: "onItemAdded",
                     arguments: [ itemId, parentId, bm.index, bm.type,
                                  null, null, bm.dateAdded,
                                  bm.guid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* insert_folder_notification() {
  let observer = expectNotifications();
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                title: "a folder" });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
  observer.check([ { name: "onItemAdded",
                     arguments: [ itemId, parentId, bm.index, bm.type,
                                  null, bm.title, bm.dateAdded,
                                  bm.guid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* insert_folder_notitle_notification() {
  let observer = expectNotifications();
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
  observer.check([ { name: "onItemAdded",
                     arguments: [ itemId, parentId, bm.index, bm.type,
                                  null, null, bm.dateAdded,
                                  bm.guid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* insert_bookmark_notification() {
  let observer = expectNotifications();
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://example.com/"),
                                                title: "a bookmark" });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
  observer.check([ { name: "onItemAdded",
                     arguments: [ itemId, parentId, bm.index, bm.type,
                                  bm.url, bm.title, bm.dateAdded,
                                  bm.guid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* insert_bookmark_notitle_notification() {
  let observer = expectNotifications();
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://example.com/") });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
  observer.check([ { name: "onItemAdded",
                     arguments: [ itemId, parentId, bm.index, bm.type,
                                  bm.url, null, bm.dateAdded,
                                  bm.guid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* insert_bookmark_tag_notification() {
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://tag.example.com/") });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);

  let tagFolder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                       parentGuid: PlacesUtils.bookmarks.tagsGuid,
                                                       title: "tag" });
  let observer = expectNotifications();
  let tag = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: tagFolder.guid,
                                                 url: new URL("http://tag.example.com/") });
  let tagId = yield PlacesUtils.promiseItemId(tag.guid);
  let tagParentId = yield PlacesUtils.promiseItemId(tag.parentGuid);

  observer.check([ { name: "onItemAdded",
                     arguments: [ tagId, tagParentId, tag.index, tag.type,
                                  tag.url, null, tag.dateAdded,
                                  tag.guid, tag.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                   { name: "onItemChanged",
                     arguments: [ itemId, "tags", false, "",
                                  bm.lastModified, bm.type, parentId,
                                  bm.guid, bm.parentGuid, "",
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* update_bookmark_lastModified() {
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://lastmod.example.com/") });
  let observer = expectNotifications();
  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                            lastModified: new Date() });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);

  observer.check([ { name: "onItemChanged",
                     arguments: [ itemId, "lastModified", false,
                                  `${bm.lastModified * 1000}`, bm.lastModified,
                                  bm.type, parentId, bm.guid, bm.parentGuid, "",
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* update_bookmark_title() {
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://title.example.com/") });
  let observer = expectNotifications();
  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                            title: "new title" });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);

  observer.check([ { name: "onItemChanged",
                     arguments: [ itemId, "title", false, bm.title,
                                  bm.lastModified, bm.type, parentId, bm.guid,
                                  bm.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* update_bookmark_uri() {
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://url.example.com/") });
  let observer = expectNotifications();
  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                            url: "http://mozilla.org/" });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);

  observer.check([ { name: "onItemChanged",
                     arguments: [ itemId, "uri", false, bm.url.href,
                                  bm.lastModified, bm.type, parentId, bm.guid,
                                  bm.parentGuid, "http://url.example.com/",
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* update_move_same_folder() {
  // Ensure there are at least two items in place (others test do so for us,
  // but we don't have to depend on that).
  let sep = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid });
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://move.example.com/") });
  let bmItemId = yield PlacesUtils.promiseItemId(bm.guid);
  let bmParentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
  let bmOldIndex = bm.index;

  let observer = expectNotifications();
  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                            parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                            index: 0 });
  Assert.equal(bm.index, 0);
  observer.check([ { name: "onItemMoved",
                     arguments: [ bmItemId, bmParentId, bmOldIndex, bmParentId, bm.index,
                                  bm.type, bm.guid, bm.parentGuid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);

  // Test that we get the right index for DEFAULT_INDEX input.
  bmOldIndex = 0;
  observer = expectNotifications();
  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                            parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                            index: PlacesUtils.bookmarks.DEFAULT_INDEX });
  Assert.ok(bm.index > 0);
  observer.check([ { name: "onItemMoved",
                     arguments: [ bmItemId, bmParentId, bmOldIndex, bmParentId, bm.index,
                                  bm.type, bm.guid, bm.parentGuid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* update_move_different_folder() {
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://move.example.com/") });
  let folder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                    parentGuid: PlacesUtils.bookmarks.unfiledGuid });
  let bmItemId = yield PlacesUtils.promiseItemId(bm.guid);
  let bmOldParentId = PlacesUtils.unfiledBookmarksFolderId;
  let bmOldIndex = bm.index;

  let observer = expectNotifications();
  bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
                                            parentGuid: folder.guid,
                                            index: PlacesUtils.bookmarks.DEFAULT_INDEX });
  Assert.equal(bm.index, 0);
  let bmNewParentId = yield PlacesUtils.promiseItemId(folder.guid);
  observer.check([ { name: "onItemMoved",
                     arguments: [ bmItemId, bmOldParentId, bmOldIndex, bmNewParentId,
                                  bm.index, bm.type, bm.guid,
                                  PlacesUtils.bookmarks.unfiledGuid,
                                  bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* remove_bookmark() {
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://remove.example.com/") });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);

  let observer = expectNotifications();
  bm = yield PlacesUtils.bookmarks.remove(bm.guid);
  // TODO (Bug 653910): onItemAnnotationRemoved notified even if there were no
  // annotations.
  observer.check([ { name: "onItemRemoved",
                     arguments: [ itemId, parentId, bm.index, bm.type, bm.url,
                                  bm.guid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* remove_folder() {
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);

  let observer = expectNotifications();
  bm = yield PlacesUtils.bookmarks.remove(bm.guid);
  observer.check([ { name: "onItemRemoved",
                     arguments: [ itemId, parentId, bm.index, bm.type, null,
                                  bm.guid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* remove_bookmark_tag_notification() {
  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                url: new URL("http://untag.example.com/") });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);

  let tagFolder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                       parentGuid: PlacesUtils.bookmarks.tagsGuid,
                                                       title: "tag" });
  let tag = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: tagFolder.guid,
                                                 url: new URL("http://untag.example.com/") });
  let tagId = yield PlacesUtils.promiseItemId(tag.guid);
  let tagParentId = yield PlacesUtils.promiseItemId(tag.parentGuid);

  let observer = expectNotifications();
  let removed = yield PlacesUtils.bookmarks.remove(tag.guid);

  observer.check([ { name: "onItemRemoved",
                     arguments: [ tagId, tagParentId, tag.index, tag.type,
                                  tag.url, tag.guid, tag.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                   { name: "onItemChanged",
                     arguments: [ itemId, "tags", false, "",
                                  bm.lastModified, bm.type, parentId,
                                  bm.guid, bm.parentGuid, "",
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* remove_folder_notification() {
  let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                     parentGuid: PlacesUtils.bookmarks.unfiledGuid });
  let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid);
  let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid);

  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: folder1.guid,
                                                url: new URL("http://example.com/") });
  let bmItemId = yield PlacesUtils.promiseItemId(bm.guid);

  let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                     parentGuid: folder1.guid });
  let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid);

  let bm2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: folder2.guid,
                                                 url: new URL("http://example.com/") });
  let bm2ItemId = yield PlacesUtils.promiseItemId(bm2.guid);

  let observer = expectNotifications();
  yield PlacesUtils.bookmarks.remove(folder1.guid);

  observer.check([ { name: "onItemRemoved",
                     arguments: [ bm2ItemId, folder2Id, bm2.index, bm2.type,
                                  bm2.url, bm2.guid, bm2.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                   { name: "onItemRemoved",
                     arguments: [ folder2Id, folder1Id, folder2.index,
                                  folder2.type, null, folder2.guid,
                                  folder2.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                   { name: "onItemRemoved",
                     arguments: [ bmItemId, folder1Id, bm.index, bm.type,
                                  bm.url, bm.guid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                   { name: "onItemRemoved",
                     arguments: [ folder1Id, folder1ParentId, folder1.index,
                                  folder1.type, null, folder1.guid,
                                  folder1.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* eraseEverything_notification() {
  // Let's start from a clean situation.
  yield PlacesUtils.bookmarks.eraseEverything();

  let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                     parentGuid: PlacesUtils.bookmarks.unfiledGuid });
  let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid);
  let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid);

  let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                parentGuid: folder1.guid,
                                                url: new URL("http://example.com/") });
  let itemId = yield PlacesUtils.promiseItemId(bm.guid);
  let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);

  let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                     parentGuid: PlacesUtils.bookmarks.unfiledGuid });
  let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid);
  let folder2ParentId = yield PlacesUtils.promiseItemId(folder2.parentGuid);

  let toolbarBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                       parentGuid: PlacesUtils.bookmarks.toolbarGuid,
                                                       url: new URL("http://example.com/") });
  let toolbarBmId = yield PlacesUtils.promiseItemId(toolbarBm.guid);
  let toolbarBmParentId = yield PlacesUtils.promiseItemId(toolbarBm.parentGuid);

  let menuBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                    parentGuid: PlacesUtils.bookmarks.menuGuid,
                                                    url: new URL("http://example.com/") });
  let menuBmId = yield PlacesUtils.promiseItemId(menuBm.guid);
  let menuBmParentId = yield PlacesUtils.promiseItemId(menuBm.parentGuid);

  let observer = expectNotifications();
  yield PlacesUtils.bookmarks.eraseEverything();

  observer.check([ { name: "onItemRemoved",
                     arguments: [ folder2Id, folder2ParentId, folder2.index,
                                  folder2.type, null, folder2.guid,
                                  folder2.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                   { name: "onItemRemoved",
                     arguments: [ itemId, parentId, bm.index, bm.type,
                                  bm.url, bm.guid, bm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                   { name: "onItemRemoved",
                     arguments: [ folder1Id, folder1ParentId, folder1.index,
                                  folder1.type, null, folder1.guid,
                                  folder1.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                   { name: "onItemRemoved",
                     arguments: [ menuBmId, menuBmParentId,
                                  menuBm.index, menuBm.type,
                                  menuBm.url, menuBm.guid,
                                  menuBm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                    { name: "onItemRemoved",
                     arguments: [ toolbarBmId, toolbarBmParentId,
                                  toolbarBm.index, toolbarBm.type,
                                  toolbarBm.url, toolbarBm.guid,
                                  toolbarBm.parentGuid,
                                  Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                 ]);
});

add_task(function* reorder_notification() {
  let bookmarks = [
    { type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
      url: "http://example1.com/",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid
    },
    { type: PlacesUtils.bookmarks.TYPE_FOLDER,
      parentGuid: PlacesUtils.bookmarks.unfiledGuid
    },
    { type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
      parentGuid: PlacesUtils.bookmarks.unfiledGuid
    },
    { type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
      url: "http://example2.com/",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid
    },
    { type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
      url: "http://example3.com/",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid
    },
  ];
  let sorted = [];
  for (let bm of bookmarks) {
    sorted.push(yield PlacesUtils.bookmarks.insert(bm));
  }

  // Randomly reorder the array.
  sorted.sort(() => 0.5 - Math.random());

  let observer = expectNotifications();
  yield PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.unfiledGuid,
                                      sorted.map(bm => bm.guid));

  let expectedNotifications = [];
  for (let i = 0; i < sorted.length; ++i) {
    let child = sorted[i];
    let childId = yield PlacesUtils.promiseItemId(child.guid);
    expectedNotifications.push({ name: "onItemMoved",
                                 arguments: [ childId,
                                              PlacesUtils.unfiledBookmarksFolderId,
                                              child.index,
                                              PlacesUtils.unfiledBookmarksFolderId,
                                              i,
                                              child.type,
                                              child.guid,
                                              child.parentGuid,
                                              child.parentGuid,
                                              Ci.nsINavBookmarksService.SOURCE_DEFAULT
                                            ] });
  }
  observer.check(expectedNotifications);
});

function expectNotifications() {
  let notifications = [];
  let observer = new Proxy(NavBookmarkObserver, {
    get(target, name) {
      if (name == "check") {
        PlacesUtils.bookmarks.removeObserver(observer);
        return expectedNotifications =>
          Assert.deepEqual(notifications, expectedNotifications);
      }

      if (name.startsWith("onItem")) {
        return (...origArgs) => {
          let args = Array.from(origArgs, arg => {
            if (arg && arg instanceof Ci.nsIURI)
              return new URL(arg.spec);
            if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000)
              return new Date(parseInt(arg/1000));
            return arg;
          });
          notifications.push({ name: name, arguments: args });
        }
      }

      if (name in target)
        return target[name];
      return undefined;
    }
  });
  PlacesUtils.bookmarks.addObserver(observer, false);
  return observer;
}

function run_test() {
  run_next_test();
}
