/* 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/. */

/**
 * Tests the bookmarks Properties dialog.
 */

const IS_REVAMP = Services.prefs.getBoolPref("sidebar.revamp");

// DOM ids of Places sidebar trees.
const SIDEBAR_HISTORY_TREE_ID = "historyTree";
const SIDEBAR_BOOKMARKS_TREE_ID = "bookmarks-view";

const SIDEBAR_HISTORY_ID = "viewHistorySidebar";
const SIDEBAR_BOOKMARKS_ID = "viewBookmarksSidebar";

// For history sidebar.
const SIDEBAR_HISTORY_BYLASTVISITED_VIEW = IS_REVAMP
  ? "sidebar-history-sort-by-last-visited"
  : "bylastvisited";
const SIDEBAR_HISTORY_BYMOSTVISITED_VIEW = "byvisited";
const SIDEBAR_HISTORY_BYDATE_VIEW = "byday";
const SIDEBAR_HISTORY_BYSITE_VIEW = "bysite";
const SIDEBAR_HISTORY_BYDATEANDSITE_VIEW = "bydateandsite";

// Action to execute on the current node.
const ACTION_EDIT = 0;
const ACTION_ADD = 1;

// If action is ACTION_ADD, set type to one of those, to define what do you
// want to create.
const TYPE_FOLDER = 0;
const TYPE_BOOKMARK = 1;

const TEST_URL = "https://www.example.com/";

const DIALOG_URL = "chrome://browser/content/places/bookmarkProperties.xhtml";

function add_bookmark(url) {
  return PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    url,
    title: `bookmark/${url}`,
  });
}

// Each test is an obj w/ a desc property and run method.
var gTests = [];
// ------------------------------------------------------------------------------
// Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog

gTests.push({
  desc: "Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog",
  sidebar: SIDEBAR_BOOKMARKS_ID,
  action: ACTION_EDIT,
  itemType: null,
  window: null,
  _bookmark: null,
  _cleanShutdown: false,

  async setup() {
    // Add a bookmark in unsorted bookmarks folder.
    this._bookmark = await add_bookmark(TEST_URL);
    Assert.ok(this._bookmark, "Correctly added a bookmark");

    // Add a tag to this bookmark.
    PlacesUtils.tagging.tagURI(Services.io.newURI(TEST_URL), ["testTag"]);
    var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL));
    Assert.equal(tags[0], "testTag", "Correctly added a tag");
  },

  selectNode(tree) {
    tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]);
    PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
    tree.selectItems([this._bookmark.guid]);
    Assert.equal(
      tree.selectedNode.bookmarkGuid,
      this._bookmark.guid,
      "Bookmark has been selected"
    );
  },

  async run() {
    // open tags autocomplete and press enter
    var tagsField = this.window.document.getElementById(
      "editBMPanel_tagsField"
    );
    var self = this;

    let unloadPromise = new Promise(resolve => {
      this.window.addEventListener(
        "unload",
        function () {
          tagsField.popup.removeEventListener(
            "popuphidden",
            popupListener,
            true
          );
          Assert.ok(
            self._cleanShutdown,
            "Dialog window should not be closed by pressing Enter on the autocomplete popup"
          );
          executeSoon(function () {
            resolve();
          });
        },
        { capture: true, once: true }
      );
    });

    var popupListener = {
      handleEvent(aEvent) {
        switch (aEvent.type) {
          case "popuphidden":
            // Everything worked fine, we can stop observing the window.
            self._cleanShutdown = true;
            self.window.document
              .getElementById("bookmarkpropertiesdialog")
              .cancelDialog();
            break;
          case "popupshown": {
            tagsField.popup.removeEventListener("popupshown", this, true);
            // In case this test fails the window will close, the test will fail
            // since we didn't set _cleanShutdown.
            let richlistbox = tagsField.popup.richlistbox;
            // Focus and select first result.
            Assert.equal(
              richlistbox.itemCount,
              1,
              "We have 1 autocomplete result"
            );
            tagsField.popup.selectedIndex = 0;
            Assert.equal(
              richlistbox.selectedItems.length,
              1,
              "We have selected a tag from the autocomplete popup"
            );
            info("About to focus the autocomplete results");
            richlistbox.focus();
            EventUtils.synthesizeKey("VK_RETURN", {}, self.window);
            break;
          }
          default:
            Assert.ok(false, "unknown event: " + aEvent.type);
        }
      },
    };
    tagsField.popup.addEventListener("popupshown", popupListener, true);
    tagsField.popup.addEventListener("popuphidden", popupListener, true);

    // Open tags autocomplete popup.
    info("About to focus the tagsField");
    executeSoon(() => {
      tagsField.focus();
      tagsField.value = "";
      EventUtils.synthesizeKey("t", {}, this.window);
    });
    await unloadPromise;
  },

  finish() {
    SidebarController.hide();
  },

  async cleanup() {
    // Check tags have not changed.
    var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL));
    Assert.equal(tags[0], "testTag", "Tag on node has not changed");

    // Cleanup.
    PlacesUtils.tagging.untagURI(Services.io.newURI(TEST_URL), ["testTag"]);
    await PlacesUtils.bookmarks.remove(this._bookmark);
    let bm = await PlacesUtils.bookmarks.fetch(this._bookmark.guid);
    Assert.ok(!bm, "should have been removed");
  },
});

// ------------------------------------------------------------------------------
// Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel

gTests.push({
  desc: "Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel",
  sidebar: SIDEBAR_BOOKMARKS_ID,
  action: ACTION_EDIT,
  itemType: null,
  window: null,
  _bookmark: null,
  _cleanShutdown: false,

  async setup() {
    // Add a bookmark in unsorted bookmarks folder.
    this._bookmark = await add_bookmark(TEST_URL);
    Assert.ok(this._bookmark, "Correctly added a bookmark");

    // Add a tag to this bookmark.
    PlacesUtils.tagging.tagURI(Services.io.newURI(TEST_URL), ["testTag"]);
    var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL));
    Assert.equal(tags[0], "testTag", "Correctly added a tag");
  },

  selectNode(tree) {
    tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]);
    PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
    tree.selectItems([this._bookmark.guid]);
    Assert.equal(
      tree.selectedNode.bookmarkGuid,
      this._bookmark.guid,
      "Bookmark has been selected"
    );
  },

  async run() {
    // open tags autocomplete and press enter
    var tagsField = this.window.document.getElementById(
      "editBMPanel_tagsField"
    );
    var self = this;

    let hiddenPromise = new Promise(resolve => {
      this.window.addEventListener(
        "unload",
        function () {
          tagsField.popup.removeEventListener(
            "popuphidden",
            popupListener,
            true
          );
          Assert.ok(
            self._cleanShutdown,
            "Dialog window should not be closed by pressing Escape on the autocomplete popup"
          );
          executeSoon(function () {
            resolve();
          });
        },
        { capture: true, once: true }
      );
    });

    var popupListener = {
      handleEvent(aEvent) {
        switch (aEvent.type) {
          case "popuphidden":
            // Everything worked fine.
            self._cleanShutdown = true;
            self.window.document
              .getElementById("bookmarkpropertiesdialog")
              .cancelDialog();
            break;
          case "popupshown": {
            tagsField.popup.removeEventListener("popupshown", this, true);
            // In case this test fails the window will close, the test will fail
            // since we didn't set _cleanShutdown.
            let richlistbox = tagsField.popup.richlistbox;
            // Focus and select first result.
            Assert.equal(
              richlistbox.itemCount,
              1,
              "We have 1 autocomplete result"
            );
            tagsField.popup.selectedIndex = 0;
            Assert.equal(
              richlistbox.selectedItems.length,
              1,
              "We have selected a tag from the autocomplete popup"
            );
            info("About to focus the autocomplete results");
            richlistbox.focus();
            EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window);
            break;
          }
          default:
            Assert.ok(false, "unknown event: " + aEvent.type);
        }
      },
    };
    tagsField.popup.addEventListener("popupshown", popupListener, true);
    tagsField.popup.addEventListener("popuphidden", popupListener, true);

    // Open tags autocomplete popup.
    info("About to focus the tagsField");
    tagsField.focus();
    tagsField.value = "";
    EventUtils.synthesizeKey("t", {}, this.window);
    await hiddenPromise;
  },

  finish() {
    SidebarController.hide();
  },

  async cleanup() {
    // Check tags have not changed.
    var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL));
    Assert.equal(tags[0], "testTag", "Tag on node has not changed");

    // Cleanup.
    PlacesUtils.tagging.untagURI(Services.io.newURI(TEST_URL), ["testTag"]);
    await PlacesUtils.bookmarks.remove(this._bookmark);
    let bm = await PlacesUtils.bookmarks.fetch(this._bookmark.guid);
    Assert.ok(!bm, "should have been removed");
  },
});

// ------------------------------------------------------------------------------
//  Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog

gTests.push({
  desc: `Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog`,
  sidebar: SIDEBAR_HISTORY_ID,
  dialogUrl: DIALOG_URL,
  action: ACTION_ADD,
  historyView: SIDEBAR_HISTORY_BYLASTVISITED_VIEW,
  window: null,

  async setup() {
    // Add a visit.
    await PlacesTestUtils.addVisits(TEST_URL);
  },

  selectNode(tree) {
    let selectedNode;
    if (IS_REVAMP) {
      // its actually a sidebar-tab-list not a xul:tree
      const itemList = tree;
      itemList.focusIndex(0);
      selectedNode = itemList.rowEls[0];
    } else {
      let visitNode = tree.view.nodeForTreeIndex(0);
      tree.selectNode(visitNode);
      selectedNode = tree.selectedNode;
    }
    Assert.equal(
      selectedNode.uri,
      TEST_URL,
      "The correct visit has been selected"
    );
    if (!IS_REVAMP) {
      Assert.equal(
        selectedNode.itemId,
        -1,
        "The selected node is not bookmarked"
      );
    }
    return selectedNode;
  },

  async run() {
    // Open folder selector.
    var foldersExpander = this.window.document.getElementById(
      "editBMPanel_foldersExpander"
    );
    var folderTree = this.window.gEditItemOverlay._folderTree;
    var self = this;

    let unloadPromise = new Promise(resolve => {
      this.window.addEventListener(
        "unload",
        () => {
          Assert.ok(
            self._cleanShutdown,
            "Dialog window should not be closed by pressing ESC in folder name textbox"
          );
          executeSoon(() => {
            resolve();
          });
        },
        { capture: true, once: true }
      );
    });

    const observer = new this.window.MutationObserver(
      (aMutationList, aObserver) => {
        for (const mutation of aMutationList) {
          if (
            mutation.type != "attributes" ||
            mutation.attributeName != "place"
          ) {
            continue;
          }
          aObserver.disconnect();
          executeSoon(async function () {
            // Create a new folder.
            var newFolderButton = self.window.document.getElementById(
              "editBMPanel_newFolderButton"
            );
            newFolderButton.doCommand();

            // Wait for the folder to be created and for editing to start.
            await TestUtils.waitForCondition(
              () => folderTree.hasAttribute("editing"),
              "We are editing new folder name in folder tree"
            );

            // Press Escape to discard editing new folder name.
            EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window);
            Assert.ok(
              !folderTree.hasAttribute("editing"),
              "We have finished editing folder name in folder tree"
            );

            self._cleanShutdown = true;

            self.window.document
              .getElementById("bookmarkpropertiesdialog")
              .cancelDialog();
          });
          break;
        }
      }
    );
    observer.observe(folderTree, { attributes: true });
    foldersExpander.doCommand();
    await unloadPromise;
  },

  finish() {
    SidebarController.hide();
  },

  async cleanup() {
    await PlacesTestUtils.promiseAsyncUpdates();

    await PlacesUtils.history.clear();
  },
});

// ------------------------------------------------------------------------------

add_setup(async function test_setup() {
  // This test can take some time, if we timeout too early it could run
  // in the middle of other tests, or hang them.
  requestLongerTimeout(2);
});

add_task(async function test_run() {
  for (let test of gTests) {
    info(`Start of test: ${test.desc}`);
    await test.setup();

    await execute_test_in_sidebar(test);
    await test.run();

    await test.cleanup();
    await test.finish();

    info(`End of test: ${test.desc}`);
  }
});

/**
 * Global functions to run a test in Properties dialog context.
 */

function execute_test_in_sidebar(test) {
  return new Promise(resolve => {
    var sidebar = document.getElementById("sidebar");
    sidebar.addEventListener(
      "load",
      function () {
        // Need to executeSoon since the tree is initialized on sidebar load.
        executeSoon(async () => {
          if (IS_REVAMP && test.sidebar == SIDEBAR_HISTORY_ID) {
            await open_properties_dialog_from_revamp_list(test);
          } else {
            await open_properties_dialog_from_tree(test);
          }
          resolve();
        });
      },
      { capture: true, once: true }
    );
    SidebarController.show(test.sidebar);
  });
}

async function promise_properties_window(dialogUrl = DIALOG_URL) {
  let win = await BrowserTestUtils.promiseAlertDialogOpen(null, dialogUrl, {
    isSubDialog: true,
  });
  await SimpleTest.promiseFocus(win);
  await win.document.mozSubdialogReady;
  return win;
}

async function open_properties_dialog_from_tree(test) {
  var sidebar = document.getElementById("sidebar");

  // If this is history sidebar, set the required view.
  if (test.sidebar == SIDEBAR_HISTORY_ID) {
    sidebar.contentDocument.getElementById(test.historyView).doCommand();
  }

  // Get sidebar's Places tree.
  var sidebarTreeID =
    test.sidebar == SIDEBAR_BOOKMARKS_ID
      ? SIDEBAR_BOOKMARKS_TREE_ID
      : SIDEBAR_HISTORY_TREE_ID;
  var tree = sidebar.contentDocument.getElementById(sidebarTreeID);
  // The sidebar may take a moment to open from the doCommand, therefore wait
  // until it has opened before continuing.
  await TestUtils.waitForCondition(() => tree, "Sidebar tree has been loaded");

  // Ask current test to select the node to edit.
  test.selectNode(tree);
  Assert.ok(
    tree.selectedNode,
    "We have a places node selected: " + tree.selectedNode.title
  );

  return new Promise(resolve => {
    var command = null;
    switch (test.action) {
      case ACTION_EDIT:
        command = "placesCmd_show:info";
        break;
      case ACTION_ADD:
        if (test.sidebar == SIDEBAR_BOOKMARKS_ID) {
          if (test.itemType == TYPE_FOLDER) {
            command = "placesCmd_new:folder";
          } else if (test.itemType == TYPE_BOOKMARK) {
            command = "placesCmd_new:bookmark";
          } else {
            Assert.ok(
              false,
              "You didn't set a valid itemType for adding an item"
            );
          }
        } else {
          command = "placesCmd_createBookmark";
        }
        break;
      default:
        Assert.ok(false, "You didn't set a valid action for this test");
    }
    // Ensure command is enabled for this node.
    Assert.ok(
      tree.controller.isCommandEnabled(command),
      " command '" + command + "' on current selected node is enabled"
    );

    promise_properties_window(test.dialogUrl).then(win => {
      test.window = win;
      resolve();
    });
    // This will open the dialog. For some reason this needs to be executed
    // later, as otherwise opening the dialog throws an exception.
    executeSoon(() => {
      tree.controller.doCommand(command);
    });
  });
}

async function open_properties_dialog_from_revamp_list(test) {
  const { contentDocument } = document.getElementById("sidebar");
  const sidebarComponent = contentDocument.querySelector("sidebar-history");

  // If this is history sidebar, set the required view.
  if (test.sidebar == SIDEBAR_HISTORY_ID) {
    info(`selecting correct history view: ${test.historyView}`);
    let menuItem = document.getElementById(test.historyView);
    menuItem.doCommand();
    await sidebarComponent.updateComplete;
  }

  await TestUtils.waitForCondition(
    () => !!sidebarComponent.cards[0].firstElementChild.rowEls.length
  );

  const list = sidebarComponent.cards[0].firstElementChild;
  Assert.ok(
    list && BrowserTestUtils.isVisible(list),
    "Sidebar list has been loaded"
  );
  // Ask current test to select the node to edit.
  const selectedNode = test.selectNode(list);
  Assert.ok(selectedNode, "We have a places node selected: " + selectedNode);

  return new Promise(resolve => {
    promise_properties_window(test.dialogUrl).then(win => {
      test.window = win;
      resolve();
    });
    // This will open the dialog. For some reason this needs to be executed
    // later, as otherwise opening the dialog throws an exception.
    executeSoon(() => {
      // Its the interactions in the bookmarks dialog which is under test,
      // so we'll just call the same function handling the context menu item would call
      window.PlacesCommandHook.bookmarkLink(
        selectedNode.url,
        selectedNode.title
      );
    });
  });
}
