/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et: */
/* 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/. */

var bs = PlacesUtils.bookmarks;
var hs = PlacesUtils.history;
var os = PlacesUtils.observers;

var bookmarksObserver = {
  handlePlacesEvents(events) {
    Assert.equal(events.length, 1);
    let event = events[0];
    switch (event.type) {
      case "bookmark-added": {
        bookmarksObserver._itemAddedId = event.id;
        bookmarksObserver._itemAddedParent = event.parentId;
        bookmarksObserver._itemAddedIndex = event.index;
        bookmarksObserver._itemAddedURI = event.url
          ? Services.io.newURI(event.url)
          : null;
        bookmarksObserver._itemAddedTitle = event.title;

        // Ensure that we've created a guid for this item.
        let stmt = DBConn().createStatement(
          `SELECT guid
           FROM moz_bookmarks
           WHERE id = :item_id`
        );
        stmt.params.item_id = event.id;
        Assert.ok(stmt.executeStep());
        Assert.ok(!stmt.getIsNull(0));
        do_check_valid_places_guid(stmt.row.guid);
        Assert.equal(stmt.row.guid, event.guid);
        stmt.finalize();
        break;
      }
      case "bookmark-removed":
        bookmarksObserver._itemRemovedId = event.id;
        bookmarksObserver._itemRemovedFolder = event.parentId;
        bookmarksObserver._itemRemovedIndex = event.index;
        break;
      case "bookmark-title-changed":
        bookmarksObserver._itemTitleChangedId = event.id;
        bookmarksObserver._itemTitleChangedTitle = event.title;
        break;
    }
  },
};

var root;
// Index at which items should begin.
var bmStartIndex = 0;

add_task(async function setup() {
  // Get bookmarks menu folder id.
  root = await PlacesTestUtils.promiseItemId(PlacesUtils.bookmarks.menuGuid);
});

add_task(async function test_bookmarks() {
  os.addListener(
    ["bookmark-added", "bookmark-removed", "bookmark-title-changed"],
    bookmarksObserver.handlePlacesEvents
  );

  // test special folders
  Assert.ok(bs.tagsFolder > 0);

  // create a folder to hold all the tests
  // this makes the tests more tolerant of changes to default_places.html
  let testRoot = bs.createFolder(
    root,
    "places bookmarks xpcshell tests",
    bs.DEFAULT_INDEX
  );
  let testRootGuid = await PlacesTestUtils.promiseItemGuid(testRoot);
  Assert.equal(bookmarksObserver._itemAddedId, testRoot);
  Assert.equal(bookmarksObserver._itemAddedParent, root);
  Assert.equal(bookmarksObserver._itemAddedIndex, bmStartIndex);
  Assert.equal(bookmarksObserver._itemAddedURI, null);
  let testStartIndex = 0;

  // insert a bookmark.
  // the time before we insert, in microseconds
  let beforeInsert = Date.now() * 1000;
  Assert.ok(beforeInsert > 0);

  let newId = bs.insertBookmark(
    testRoot,
    uri("http://google.com/"),
    bs.DEFAULT_INDEX,
    ""
  );
  Assert.equal(bookmarksObserver._itemAddedId, newId);
  Assert.equal(bookmarksObserver._itemAddedParent, testRoot);
  Assert.equal(bookmarksObserver._itemAddedIndex, testStartIndex);
  Assert.ok(bookmarksObserver._itemAddedURI.equals(uri("http://google.com/")));

  // after just inserting, modified should not be set
  let lastModified = PlacesUtils.toPRTime(
    (
      await PlacesUtils.bookmarks.fetch(
        await PlacesTestUtils.promiseItemGuid(newId)
      )
    ).lastModified
  );

  // The time before we set the title, in microseconds.
  let beforeSetTitle = Date.now() * 1000;
  Assert.ok(beforeSetTitle >= beforeInsert);

  // Workaround possible VM timers issues moving lastModified and dateAdded
  // to the past.
  lastModified -= 1000;
  bs.setItemLastModified(newId, lastModified);

  // set bookmark title
  bs.setItemTitle(newId, "Google");
  Assert.equal(bookmarksObserver._itemTitleChangedId, newId);
  Assert.equal(bookmarksObserver._itemTitleChangedTitle, "Google");

  // check lastModified after we set the title
  let lastModified2 = PlacesUtils.toPRTime(
    (
      await PlacesUtils.bookmarks.fetch(
        await PlacesTestUtils.promiseItemGuid(newId)
      )
    ).lastModified
  );
  info("test setItemTitle");
  info("beforeSetTitle = " + beforeSetTitle);
  info("lastModified = " + lastModified);
  info("lastModified2 = " + lastModified2);
  Assert.ok(is_time_ordered(lastModified, lastModified2));

  // get item title
  let title = bs.getItemTitle(newId);
  Assert.equal(title, "Google");

  // get item title bad input
  try {
    bs.getItemTitle(-3);
    do_throw("getItemTitle accepted bad input");
  } catch (ex) {}

  // create a folder at a specific index
  let workFolder = bs.createFolder(testRoot, "Work", 0);
  Assert.equal(bookmarksObserver._itemAddedId, workFolder);
  Assert.equal(bookmarksObserver._itemAddedParent, testRoot);
  Assert.equal(bookmarksObserver._itemAddedIndex, 0);
  Assert.equal(bookmarksObserver._itemAddedURI, null);

  Assert.equal(bs.getItemTitle(workFolder), "Work");
  bs.setItemTitle(workFolder, "Work #");
  Assert.equal(bs.getItemTitle(workFolder), "Work #");

  // add item into subfolder, specifying index
  let newId2 = bs.insertBookmark(
    workFolder,
    uri("http://developer.mozilla.org/"),
    0,
    ""
  );
  Assert.equal(bookmarksObserver._itemAddedId, newId2);
  Assert.equal(bookmarksObserver._itemAddedParent, workFolder);
  Assert.equal(bookmarksObserver._itemAddedIndex, 0);

  // change item
  bs.setItemTitle(newId2, "DevMo");
  Assert.equal(bookmarksObserver._itemTitleChangedId, newId2);
  Assert.equal(bookmarksObserver._itemTitleChangedTitle, "DevMo");

  // insert item into subfolder
  let newId3 = bs.insertBookmark(
    workFolder,
    uri("http://msdn.microsoft.com/"),
    bs.DEFAULT_INDEX,
    ""
  );
  Assert.equal(bookmarksObserver._itemAddedId, newId3);
  Assert.equal(bookmarksObserver._itemAddedParent, workFolder);
  Assert.equal(bookmarksObserver._itemAddedIndex, 1);

  // change item
  bs.setItemTitle(newId3, "MSDN");
  Assert.equal(bookmarksObserver._itemTitleChangedId, newId3);
  Assert.equal(bookmarksObserver._itemTitleChangedTitle, "MSDN");

  // remove item
  bs.removeItem(newId2);
  Assert.equal(bookmarksObserver._itemRemovedId, newId2);
  Assert.equal(bookmarksObserver._itemRemovedFolder, workFolder);
  Assert.equal(bookmarksObserver._itemRemovedIndex, 0);

  // insert item into subfolder
  let newId4 = bs.insertBookmark(
    workFolder,
    uri("http://developer.mozilla.org/"),
    bs.DEFAULT_INDEX,
    ""
  );
  Assert.equal(bookmarksObserver._itemAddedId, newId4);
  Assert.equal(bookmarksObserver._itemAddedParent, workFolder);
  Assert.equal(bookmarksObserver._itemAddedIndex, 1);

  // create folder
  let homeFolder = bs.createFolder(testRoot, "Home", bs.DEFAULT_INDEX);
  Assert.equal(bookmarksObserver._itemAddedId, homeFolder);
  Assert.equal(bookmarksObserver._itemAddedParent, testRoot);
  Assert.equal(bookmarksObserver._itemAddedIndex, 2);

  // insert item
  let newId5 = bs.insertBookmark(
    homeFolder,
    uri("http://espn.com/"),
    bs.DEFAULT_INDEX,
    ""
  );
  Assert.equal(bookmarksObserver._itemAddedId, newId5);
  Assert.equal(bookmarksObserver._itemAddedParent, homeFolder);
  Assert.equal(bookmarksObserver._itemAddedIndex, 0);

  // change item
  bs.setItemTitle(newId5, "ESPN");
  Assert.equal(bookmarksObserver._itemTitleChangedId, newId5);
  Assert.equal(bookmarksObserver._itemTitleChangedTitle, "ESPN");

  // insert query item
  let uri6 = uri(
    "place:domain=google.com&type=" +
      Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY
  );
  let newId6 = bs.insertBookmark(testRoot, uri6, bs.DEFAULT_INDEX, "");
  Assert.equal(bookmarksObserver._itemAddedParent, testRoot);
  Assert.equal(bookmarksObserver._itemAddedIndex, 3);

  // change item
  bs.setItemTitle(newId6, "Google Sites");
  Assert.equal(bookmarksObserver._itemTitleChangedId, newId6);
  Assert.equal(bookmarksObserver._itemTitleChangedTitle, "Google Sites");

  // test bookmark id in query output
  try {
    let options = hs.getNewQueryOptions();
    let query = hs.getNewQuery();
    query.setParents([testRootGuid]);
    let result = hs.executeQuery(query, options);
    let rootNode = result.root;
    rootNode.containerOpen = true;
    let cc = rootNode.childCount;
    info("bookmark itemId test: CC = " + cc);
    Assert.ok(cc > 0);
    for (let i = 0; i < cc; ++i) {
      let node = rootNode.getChild(i);
      if (
        node.type == node.RESULT_TYPE_FOLDER ||
        node.type == node.RESULT_TYPE_URI ||
        node.type == node.RESULT_TYPE_SEPARATOR ||
        node.type == node.RESULT_TYPE_QUERY
      ) {
        Assert.ok(node.itemId > 0);
      } else {
        Assert.equal(node.itemId, -1);
      }
    }
    rootNode.containerOpen = false;
  } catch (ex) {
    do_throw("bookmarks query: " + ex);
  }

  // test that multiple bookmarks with same URI show up right in bookmark
  // folder queries, todo: also to do for complex folder queries
  try {
    // test uri
    let mURI = uri("http://multiple.uris.in.query");

    let testFolder = bs.createFolder(testRoot, "test Folder", bs.DEFAULT_INDEX);
    let testFolderGuid = await PlacesTestUtils.promiseItemGuid(testFolder);
    // add 2 bookmarks
    bs.insertBookmark(testFolder, mURI, bs.DEFAULT_INDEX, "title 1");
    bs.insertBookmark(testFolder, mURI, bs.DEFAULT_INDEX, "title 2");

    // query
    let options = hs.getNewQueryOptions();
    let query = hs.getNewQuery();
    query.setParents([testFolderGuid]);
    let result = hs.executeQuery(query, options);
    let rootNode = result.root;
    rootNode.containerOpen = true;
    let cc = rootNode.childCount;
    Assert.equal(cc, 2);
    Assert.equal(rootNode.getChild(0).title, "title 1");
    Assert.equal(rootNode.getChild(1).title, "title 2");
    rootNode.containerOpen = false;
  } catch (ex) {
    do_throw("bookmarks query: " + ex);
  }

  // test change bookmark uri
  let newId10 = bs.insertBookmark(
    testRoot,
    uri("http://foo10.com/"),
    bs.DEFAULT_INDEX,
    ""
  );

  // Workaround possible VM timers issues moving lastModified and dateAdded
  // to the past.
  lastModified -= 1000;
  bs.setItemLastModified(newId10, lastModified);

  // insert a bookmark with title ZZZXXXYYY and then search for it.
  // this test confirms that we can find bookmarks that we haven't visited
  // (which are "hidden") and that we can find by title.
  // see bug #369887 for more details
  let newId13 = bs.insertBookmark(
    testRoot,
    uri("http://foobarcheese.com/"),
    bs.DEFAULT_INDEX,
    ""
  );
  Assert.equal(bookmarksObserver._itemAddedId, newId13);
  Assert.equal(bookmarksObserver._itemAddedParent, testRoot);
  Assert.equal(bookmarksObserver._itemAddedIndex, 6);

  // set bookmark title
  bs.setItemTitle(newId13, "ZZZXXXYYY");
  Assert.equal(bookmarksObserver._itemTitleChangedId, newId13);
  Assert.equal(bookmarksObserver._itemTitleChangedTitle, "ZZZXXXYYY");

  // test search on bookmark title ZZZXXXYYY
  try {
    let options = hs.getNewQueryOptions();
    options.excludeQueries = 1;
    options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
    let query = hs.getNewQuery();
    query.searchTerms = "ZZZXXXYYY";
    let result = hs.executeQuery(query, options);
    let rootNode = result.root;
    rootNode.containerOpen = true;
    let cc = rootNode.childCount;
    Assert.equal(cc, 1);
    let node = rootNode.getChild(0);
    Assert.equal(node.title, "ZZZXXXYYY");
    Assert.ok(node.itemId > 0);
    rootNode.containerOpen = false;
  } catch (ex) {
    do_throw("bookmarks query: " + ex);
  }

  // test dateAdded and lastModified properties
  // for a search query
  try {
    let options = hs.getNewQueryOptions();
    options.excludeQueries = 1;
    options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
    let query = hs.getNewQuery();
    query.searchTerms = "ZZZXXXYYY";
    let result = hs.executeQuery(query, options);
    let rootNode = result.root;
    rootNode.containerOpen = true;
    let cc = rootNode.childCount;
    Assert.equal(cc, 1);
    let node = rootNode.getChild(0);

    Assert.equal(typeof node.dateAdded, "number");
    Assert.ok(node.dateAdded > 0);

    Assert.equal(typeof node.lastModified, "number");
    Assert.ok(node.lastModified > 0);

    rootNode.containerOpen = false;
  } catch (ex) {
    do_throw("bookmarks query: " + ex);
  }

  // test dateAdded and lastModified properties
  // for a folder query
  try {
    let options = hs.getNewQueryOptions();
    let query = hs.getNewQuery();
    query.setParents([testRootGuid]);
    let result = hs.executeQuery(query, options);
    let rootNode = result.root;
    rootNode.containerOpen = true;
    let cc = rootNode.childCount;
    Assert.ok(cc > 0);
    for (let i = 0; i < cc; i++) {
      let node = rootNode.getChild(i);

      if (node.type == node.RESULT_TYPE_URI) {
        Assert.equal(typeof node.dateAdded, "number");
        Assert.ok(node.dateAdded > 0);

        Assert.equal(typeof node.lastModified, "number");
        Assert.ok(node.lastModified > 0);
        break;
      }
    }
    rootNode.containerOpen = false;
  } catch (ex) {
    do_throw("bookmarks query: " + ex);
  }

  // check setItemLastModified()
  let newId14 = bs.insertBookmark(
    testRoot,
    uri("http://bar.tld/"),
    bs.DEFAULT_INDEX,
    ""
  );
  bs.setItemLastModified(newId14, 1234000000000000);
  let fakeLastModified = PlacesUtils.toPRTime(
    (
      await PlacesUtils.bookmarks.fetch(
        await PlacesTestUtils.promiseItemGuid(newId14)
      )
    ).lastModified
  );
  Assert.equal(fakeLastModified, 1234000000000000);

  // bug 378820
  let uri1 = uri("http://foo.tld/a");
  bs.insertBookmark(testRoot, uri1, bs.DEFAULT_INDEX, "");
  await PlacesTestUtils.addVisits(uri1);

  // bug 646993 - test bookmark titles longer than the maximum allowed length
  let title15 = Array(TITLE_LENGTH_MAX + 5).join("X");
  let title15expected = title15.substring(0, TITLE_LENGTH_MAX);
  let newId15 = bs.insertBookmark(
    testRoot,
    uri("http://evil.com/"),
    bs.DEFAULT_INDEX,
    title15
  );

  Assert.equal(bs.getItemTitle(newId15).length, title15expected.length);
  Assert.equal(bookmarksObserver._itemAddedTitle, title15expected);
  // test title length after updates
  bs.setItemTitle(newId15, title15 + " updated");
  Assert.equal(bs.getItemTitle(newId15).length, title15expected.length);
  Assert.equal(bookmarksObserver._itemTitleChangedId, newId15);
  Assert.equal(bookmarksObserver._itemTitleChangedTitle, title15expected);

  await testSimpleFolderResult();
});

async function testSimpleFolderResult() {
  // the time before we create a folder, in microseconds
  // Workaround possible VM timers issues subtracting 1us.
  let beforeCreate = Date.now() * 1000 - 1;
  Assert.ok(beforeCreate > 0);

  // create a folder
  let parent = bs.createFolder(root, "test", bs.DEFAULT_INDEX);
  let parentGuid = await PlacesTestUtils.promiseItemGuid(parent);

  // the time before we insert, in microseconds
  // Workaround possible VM timers issues subtracting 1ms.
  let beforeInsert = Date.now() * 1000 - 1;
  Assert.ok(beforeInsert > 0);

  // re-set item title separately so can test nodes' last modified
  let item = bs.insertBookmark(
    parent,
    uri("about:blank"),
    bs.DEFAULT_INDEX,
    ""
  );
  bs.setItemTitle(item, "test bookmark");

  // see above
  let folder = bs.createFolder(parent, "test folder", bs.DEFAULT_INDEX);
  bs.setItemTitle(folder, "test folder");

  let longName = Array(TITLE_LENGTH_MAX + 5).join("A");
  let folderLongName = bs.createFolder(parent, longName, bs.DEFAULT_INDEX);
  Assert.equal(
    bookmarksObserver._itemAddedTitle,
    longName.substring(0, TITLE_LENGTH_MAX)
  );

  let options = hs.getNewQueryOptions();
  let query = hs.getNewQuery();
  query.setParents([parentGuid]);
  let result = hs.executeQuery(query, options);
  let rootNode = result.root;
  rootNode.containerOpen = true;
  Assert.equal(rootNode.childCount, 3);

  let node = rootNode.getChild(0);
  Assert.equal(node.itemId, item);
  Assert.ok(node.dateAdded > 0);
  Assert.ok(node.lastModified > 0);
  Assert.equal(node.title, "test bookmark");
  node = rootNode.getChild(1);
  Assert.equal(node.itemId, folder);
  Assert.equal(node.title, "test folder");
  Assert.ok(node.dateAdded > 0);
  Assert.ok(node.lastModified > 0);
  node = rootNode.getChild(2);
  Assert.equal(node.itemId, folderLongName);
  Assert.equal(node.title, longName.substring(0, TITLE_LENGTH_MAX));
  Assert.ok(node.dateAdded > 0);
  Assert.ok(node.lastModified > 0);

  // update with another long title
  bs.setItemTitle(folderLongName, longName + " updated");
  Assert.equal(bookmarksObserver._itemTitleChangedId, folderLongName);
  Assert.equal(
    bookmarksObserver._itemTitleChangedTitle,
    longName.substring(0, TITLE_LENGTH_MAX)
  );

  node = rootNode.getChild(2);
  Assert.equal(node.title, longName.substring(0, TITLE_LENGTH_MAX));

  rootNode.containerOpen = false;
}
