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

var gAccumulator = {
  get callback() {
    this.results = [];
    return result => this.results.push(result);
  },
};

add_task(async function invalid_input_throws() {
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch(),
    /Input should be a valid object/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch(null),
    /Input should be a valid object/
  );

  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ guid: "123456789012", index: 0 }),
    /The following properties were expected: parentGuid/
  );

  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({}),
    /Unexpected number of conditions provided: 0/
  );
  Assert.throws(
    () =>
      PlacesUtils.bookmarks.fetch({
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
      }),
    /Unexpected number of conditions provided: 0/
  );
  Assert.throws(
    () =>
      PlacesUtils.bookmarks.fetch({
        guid: "123456789012",
        parentGuid: "012345678901",
        index: 0,
      }),
    /Unexpected number of conditions provided: 2/
  );
  Assert.throws(
    () =>
      PlacesUtils.bookmarks.fetch({
        guid: "123456789012",
        url: "http://example.com",
      }),
    /Unexpected number of conditions provided: 2/
  );

  Assert.throws(
    () => PlacesUtils.bookmarks.fetch("test"),
    /Invalid value for property 'guid'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch(123),
    /Invalid value for property 'guid'/
  );

  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ guid: "test" }),
    /Invalid value for property 'guid'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ guid: null }),
    /Invalid value for property 'guid'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ guid: 123 }),
    /Invalid value for property 'guid'/
  );

  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ guidPrefix: "" }),
    /Invalid value for property 'guidPrefix'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ guidPrefix: null }),
    /Invalid value for property 'guidPrefix'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ guidPrefix: 123 }),
    /Invalid value for property 'guidPrefix'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ guidPrefix: "123456789012" }),
    /Invalid value for property 'guidPrefix'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ guidPrefix: "@" }),
    /Invalid value for property 'guidPrefix'/
  );

  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ parentGuid: "test", index: 0 }),
    /Invalid value for property 'parentGuid'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ parentGuid: null, index: 0 }),
    /Invalid value for property 'parentGuid'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ parentGuid: 123, index: 0 }),
    /Invalid value for property 'parentGuid'/
  );

  Assert.throws(
    () =>
      PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", index: "0" }),
    /Invalid value for property 'index'/
  );
  Assert.throws(
    () =>
      PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", index: null }),
    /Invalid value for property 'index'/
  );
  Assert.throws(
    () =>
      PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", index: -10 }),
    /Invalid value for property 'index'/
  );

  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ url: "http://te st/" }),
    /Invalid value for property 'url'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ url: null }),
    /Invalid value for property 'url'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch({ url: -10 }),
    /Invalid value for property 'url'/
  );

  Assert.throws(
    () => PlacesUtils.bookmarks.fetch("123456789012", "test"),
    /onResult callback must be a valid function/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.fetch("123456789012", {}),
    /onResult callback must be a valid function/
  );
});

add_task(async function fetch_nonexistent_guid() {
  let bm = await PlacesUtils.bookmarks.fetch(
    { guid: "123456789012" },
    gAccumulator.callback
  );
  Assert.equal(bm, null);
  Assert.equal(gAccumulator.results.length, 0);
});

add_task(async function fetch_bookmark() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
    url: "http://example.com/",
    title: "a bookmark",
  });
  checkBookmarkObject(bm1);

  let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid, gAccumulator.callback);
  checkBookmarkObject(bm2);
  Assert.equal(gAccumulator.results.length, 1);
  checkBookmarkObject(gAccumulator.results[0]);
  Assert.deepEqual(gAccumulator.results[0], bm1);

  Assert.deepEqual(bm1, bm2);
  Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
  Assert.equal(bm2.index, 0);
  Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
  Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
  Assert.equal(bm2.url.href, "http://example.com/");
  Assert.equal(bm2.title, "a bookmark");
  Assert.strictEqual(bm2.childCount, undefined);

  await PlacesUtils.bookmarks.remove(bm1.guid);
});

add_task(async function fetch_bookmar_empty_title() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
    url: "http://example.com/",
    title: "",
  });
  checkBookmarkObject(bm1);

  let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid);
  checkBookmarkObject(bm2);

  Assert.deepEqual(bm1, bm2);
  Assert.equal(bm2.index, 0);
  Assert.strictEqual(bm2.title, "");

  await PlacesUtils.bookmarks.remove(bm1.guid);
});

add_task(async function fetch_folder() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
    title: "a folder",
  });
  checkBookmarkObject(bm1);
  Assert.deepEqual(bm1.dateAdded, bm1.lastModified);

  // Inserting a child updates both the childCount and lastModified of bm1,
  // though the bm1 object is static once fetched, thus later we'll manually
  // update it.
  await PlacesUtils.bookmarks.insert({
    parentGuid: bm1.guid,
    url: "https://www.mozilla.org/",
    title: "",
  });

  let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid);
  checkBookmarkObject(bm2);

  Assert.equal(bm2.childCount, 1);
  bm1.childCount = bm2.childCount;
  bm1.lastModified = bm2.lastModified;

  Assert.deepEqual(bm1, bm2);
  Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
  Assert.equal(bm2.index, 0);
  Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER);
  Assert.equal(bm2.title, "a folder");
  Assert.ok(!("url" in bm2));

  await PlacesUtils.bookmarks.remove(bm1.guid);
});

add_task(async function fetch_folder_empty_title() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
    title: "",
  });
  checkBookmarkObject(bm1);

  let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid);
  checkBookmarkObject(bm2);

  Assert.equal(bm2.childCount, 0);
  // Insert doesn't populate childCount (it would always be 0 anyway), so set
  // it to be able to just use deepEqual.
  bm1.childCount = bm2.childCount;

  Assert.deepEqual(bm1, bm2);
  Assert.equal(bm2.index, 0);
  Assert.strictEqual(bm2.title, "");

  await PlacesUtils.bookmarks.remove(bm1.guid);
});

add_task(async function fetch_separator() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
  });
  checkBookmarkObject(bm1);

  let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid);
  checkBookmarkObject(bm2);

  Assert.deepEqual(bm1, bm2);
  Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
  Assert.equal(bm2.index, 0);
  Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
  Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
  Assert.ok(!("url" in bm2));
  Assert.strictEqual(bm2.title, "");

  await PlacesUtils.bookmarks.remove(bm1.guid);
});

add_task(async function fetch_byguid_prefix() {
  const PREFIX = "PREFIX-";

  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    guid: PlacesUtils.generateGuidWithPrefix(PREFIX),
    url: "http://bm1.example.com/",
    title: "bookmark 1",
  });
  checkBookmarkObject(bm1);
  Assert.ok(bm1.guid.startsWith(PREFIX));

  let bm2 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    guid: PlacesUtils.generateGuidWithPrefix(PREFIX),
    url: "http://bm2.example.com/",
    title: "bookmark 2",
  });
  checkBookmarkObject(bm2);
  Assert.ok(bm2.guid.startsWith(PREFIX));

  let bm3 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
    guid: PlacesUtils.generateGuidWithPrefix(PREFIX),
    title: "a folder",
  });
  await PlacesUtils.bookmarks.insert({
    parentGuid: bm3.guid,
    url: "https://www.mozilla.org/",
    title: "",
  });
  checkBookmarkObject(bm3);
  Assert.ok(bm3.guid.startsWith(PREFIX));

  // Bookmark 4 doesn't have the same guid prefix, so it shouldn't be returned in the results.
  let bm4 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    url: "http://bm3.example.com/",
    title: "bookmark 4",
  });
  checkBookmarkObject(bm4);
  Assert.ok(!bm4.guid.startsWith(PREFIX));

  await PlacesUtils.bookmarks.fetch(
    { guidPrefix: PREFIX },
    gAccumulator.callback
  );

  Assert.equal(gAccumulator.results.length, 3);

  // The results are returned by most recent first, so the first bookmark
  // inserted is the last one in the returned array.
  Assert.deepEqual(bm1, gAccumulator.results[2]);
  Assert.deepEqual(bm2, gAccumulator.results[1]);
  Assert.equal(gAccumulator.results[0].childCount, 1);
  bm3.childCount = gAccumulator.results[0].childCount;
  bm3.lastModified = gAccumulator.results[0].lastModified;
  Assert.deepEqual(bm3, gAccumulator.results[0]);

  Assert.equal(bm3.childCount, 1);

  await PlacesUtils.bookmarks.remove(bm1);
  await PlacesUtils.bookmarks.remove(bm2);
  await PlacesUtils.bookmarks.remove(bm3);
  await PlacesUtils.bookmarks.remove(bm4);
});

add_task(async function fetch_byposition_nonexisting_parentGuid() {
  let bm = await PlacesUtils.bookmarks.fetch(
    { parentGuid: "123456789012", index: 0 },
    gAccumulator.callback
  );
  Assert.equal(bm, null);
  Assert.equal(gAccumulator.results.length, 0);
});

add_task(async function fetch_byposition_nonexisting_index() {
  let bm = await PlacesUtils.bookmarks.fetch(
    { parentGuid: PlacesUtils.bookmarks.unfiledGuid, index: 100 },
    gAccumulator.callback
  );
  Assert.equal(bm, null);
  Assert.equal(gAccumulator.results.length, 0);
});

add_task(async function fetch_byposition() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
    url: "http://example.com/",
    title: "a bookmark",
  });
  checkBookmarkObject(bm1);

  let bm2 = await PlacesUtils.bookmarks.fetch(
    { parentGuid: bm1.parentGuid, index: bm1.index },
    gAccumulator.callback
  );
  checkBookmarkObject(bm2);
  Assert.equal(gAccumulator.results.length, 1);
  checkBookmarkObject(gAccumulator.results[0]);
  Assert.deepEqual(gAccumulator.results[0], bm1);

  Assert.deepEqual(bm1, bm2);
  Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
  Assert.equal(bm2.index, 0);
  Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
  Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
  Assert.equal(bm2.url.href, "http://example.com/");
  Assert.equal(bm2.title, "a bookmark");
});

add_task(async function fetch_byposition_default_index() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
    url: "http://example.com/last",
    title: "last child",
  });
  checkBookmarkObject(bm1);

  let bm2 = await PlacesUtils.bookmarks.fetch(
    { parentGuid: bm1.parentGuid, index: PlacesUtils.bookmarks.DEFAULT_INDEX },
    gAccumulator.callback
  );
  checkBookmarkObject(bm2);
  Assert.equal(gAccumulator.results.length, 1);
  checkBookmarkObject(gAccumulator.results[0]);
  Assert.deepEqual(gAccumulator.results[0], bm1);

  Assert.deepEqual(bm1, bm2);
  Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
  Assert.equal(bm2.index, 1);
  Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
  Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
  Assert.equal(bm2.url.href, "http://example.com/last");
  Assert.equal(bm2.title, "last child");

  await PlacesUtils.bookmarks.remove(bm1.guid);
});

add_task(async function fetch_byurl_nonexisting() {
  let bm = await PlacesUtils.bookmarks.fetch(
    { url: "http://nonexisting.com/" },
    gAccumulator.callback
  );
  Assert.equal(bm, null);
  Assert.equal(gAccumulator.results.length, 0);
});

add_task(async function fetch_byurl() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
    url: "http://byurl.com/",
    title: "a bookmark",
  });
  checkBookmarkObject(bm1);

  // Also ensure that fecth-by-url excludes the tags folder.
  PlacesUtils.tagging.tagURI(bm1.url.URI, ["Test Tag"]);

  let bm2 = await PlacesUtils.bookmarks.fetch(
    { url: bm1.url },
    gAccumulator.callback
  );
  checkBookmarkObject(bm2);
  Assert.equal(gAccumulator.results.length, 1);
  checkBookmarkObject(gAccumulator.results[0]);
  Assert.deepEqual(gAccumulator.results[0], bm1);

  Assert.deepEqual(bm1, bm2);
  Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
  Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
  Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
  Assert.equal(bm2.url.href, "http://byurl.com/");
  Assert.equal(bm2.title, "a bookmark");

  let bm3 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
    url: "http://byurl.com/",
    title: "a bookmark",
  });
  let bm4 = await PlacesUtils.bookmarks.fetch(
    { url: bm1.url },
    gAccumulator.callback
  );
  checkBookmarkObject(bm4);
  Assert.deepEqual(bm3, bm4);
  Assert.equal(gAccumulator.results.length, 2);
  gAccumulator.results.forEach(checkBookmarkObject);
  Assert.deepEqual(gAccumulator.results[0], bm4);

  // After an update the returned bookmark should change.
  await PlacesUtils.bookmarks.update({ guid: bm1.guid, title: "new title" });
  let bm5 = await PlacesUtils.bookmarks.fetch(
    { url: bm1.url },
    gAccumulator.callback
  );
  checkBookmarkObject(bm5);
  // Cannot use deepEqual cause lastModified changed.
  Assert.equal(bm1.guid, bm5.guid);
  Assert.ok(bm5.lastModified > bm1.lastModified);
  Assert.equal(gAccumulator.results.length, 2);
  gAccumulator.results.forEach(checkBookmarkObject);
  Assert.deepEqual(gAccumulator.results[0], bm5);

  // cleanup
  PlacesUtils.tagging.untagURI(bm1.url.URI, ["Test Tag"]);
});

add_task(async function fetch_concurrent() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    url: "http://concurrent.url.com/",
  });
  checkBookmarkObject(bm1);

  let bm2 = await PlacesUtils.bookmarks.fetch(
    { url: bm1.url },
    gAccumulator.callback,
    { concurrent: true }
  );
  checkBookmarkObject(bm2);
  Assert.equal(gAccumulator.results.length, 1);
  Assert.deepEqual(gAccumulator.results[0], bm1);
  Assert.deepEqual(bm1, bm2);
  let bm3 = await PlacesUtils.bookmarks.fetch(
    { url: bm1.url },
    gAccumulator.callback,
    { concurrent: false }
  );
  checkBookmarkObject(bm3);
  Assert.equal(gAccumulator.results.length, 1);
  Assert.deepEqual(gAccumulator.results[0], bm1);
  Assert.deepEqual(bm1, bm3);
  let bm4 = await PlacesUtils.bookmarks.fetch(
    { url: bm1.url },
    gAccumulator.callback,
    {}
  );
  checkBookmarkObject(bm4);
  Assert.equal(gAccumulator.results.length, 1);
  Assert.deepEqual(gAccumulator.results[0], bm1);
  Assert.deepEqual(bm1, bm4);
});

add_task(async function fetch_by_parent() {
  let folder1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
    title: "a folder",
  });
  checkBookmarkObject(folder1);

  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: folder1.guid,
    url: "http://bm1.example.com/",
    title: "bookmark 1",
  });
  checkBookmarkObject(bm1);

  let bm2 = await PlacesUtils.bookmarks.insert({
    parentGuid: folder1.guid,
    url: "http://bm2.example.com/",
    title: "bookmark 2",
  });
  checkBookmarkObject(bm2);

  let bm3 = await PlacesUtils.bookmarks.insert({
    parentGuid: folder1.guid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
    title: "sub folder",
    index: 0,
  });
  checkBookmarkObject(bm2);
  await PlacesUtils.bookmarks.insert({
    parentGuid: bm3.guid,
    url: "http://mozilla.org/",
    title: "sub bookmark",
  });

  await PlacesUtils.bookmarks.fetch(
    { parentGuid: folder1.guid },
    gAccumulator.callback
  );

  Assert.equal(gAccumulator.results.length, 3);

  Assert.equal(gAccumulator.results[0].childCount, 1);
  bm3.childCount = gAccumulator.results[0].childCount;
  bm3.lastModified = gAccumulator.results[0].lastModified;
  Assert.deepEqual(bm3, gAccumulator.results[0]);
  Assert.equal(bm1.url.href, gAccumulator.results[1].url.href);
  Assert.equal(bm2.url.href, gAccumulator.results[2].url.href);

  await PlacesUtils.bookmarks.remove(folder1);
});

add_task(async function fetch_with_bookmark_path() {
  let folder1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
    title: "Parent",
  });
  checkBookmarkObject(folder1);

  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: folder1.guid,
    url: "http://bookmarkpath.example.com/",
    title: "Child Bookmark",
  });
  checkBookmarkObject(bm1);

  let bm2 = await PlacesUtils.bookmarks.fetch(
    { guid: bm1.guid },
    gAccumulator.callback,
    { includePath: true }
  );
  checkBookmarkObject(bm2);
  Assert.equal(gAccumulator.results.length, 1);
  Assert.equal(bm2.path.length, 2);
  Assert.equal(bm2.path[0].guid, PlacesUtils.bookmarks.unfiledGuid);
  Assert.equal(bm2.path[0].title, "unfiled");
  Assert.equal(bm2.path[1].guid, folder1.guid);
  Assert.equal(bm2.path[1].title, folder1.title);

  await PlacesUtils.bookmarks.remove(folder1);
});