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

"use strict";

function ensurePosition(info, parentGuid, index) {
 print(`Checking ${info.guid}`);
 checkBookmarkObject(info);
 Assert.equal(info.parentGuid, parentGuid, "Should be in the correct parent folder");
 Assert.equal(info.index, index, "Should have the correct index");
}

function insertChildren(folder, items) {
  if (!items.length) {
    return [];
  }

  let children = [];
  for (let i = 0; i < items.length; i++) {
    if (items[i].type === TYPE_BOOKMARK) {
      children.push({
        title: `${i}`,
        url: "http://example.com",
      });
    } else {
      throw new Error(`Type ${items[i].type} is not supported.`);
    }
  }
  return PlacesUtils.bookmarks.insertTree({
    guid: folder.guid,
    children,
  });
}

async function dumpFolderChildren(folder, details, folderA, folderB,
                                  originalAChildren, originalBChildren) {
  info(`${folder} Details:`);
  info(`Input: ${JSON.stringify(details.initial[folder])}`);
  info(`Expected: ${JSON.stringify(details.expected[folder])}`);
  info("Index\tOriginal\tExpected\tResult");

  let originalChildren;
  let folderGuid;
  if (folder == "folderA") {
    originalChildren = originalAChildren;
    folderGuid = folderA.guid;
  } else {
    originalChildren = originalBChildren;
    folderGuid = folderB.guid;
  }

  let tree = await PlacesUtils.promiseBookmarksTree(folderGuid);
  let childrenCount = tree.children ? tree.children.length : 0;
  for (let i = 0; i < originalChildren.length || i < childrenCount; i++) {
    let originalGuid = i < originalChildren.length ? originalChildren[i].guid : "            ";
    let resultGuid = i < childrenCount ? tree.children[i].guid : "            ";
    let expectedGuid = "            ";
    if (i < details.expected[folder].length) {
      let expected = details.expected[folder][i];
      expectedGuid = expected.folder == "a" ?
        originalAChildren[expected.originalIndex].guid :
        originalBChildren[expected.originalIndex].guid;
    }
    info(`${i}\t${originalGuid}\t${expectedGuid}\t${resultGuid}\n`);
  }
}

async function checkExpectedResults(details, folder, folderGuid, lastModified,
                                    movedItems, folderAChildren, folderBChildren) {
  let expectedResults = details.expected[folder];
  for (let i = 0; i < expectedResults.length; i++) {
    let expectedDetails = expectedResults[i];
    let originalItem = expectedDetails.folder == "a" ?
      folderAChildren[expectedDetails.originalIndex] :
      folderBChildren[expectedDetails.originalIndex];

    // Check the item got updated correctly in the database.
    let updatedItem = await PlacesUtils.bookmarks.fetch(originalItem.guid);

    ensurePosition(updatedItem, folderGuid, i);
    Assert.greaterOrEqual(updatedItem.lastModified.getTime(), lastModified.getTime(),
              "Last modified should be later or equal to before");
  }

  if (details.expected.skipResultIndexChecks) {
    return;
  }

  // Check the items returned from the actual move() call are correct.
  let index = 0;
  for (let item of details.initial[folder]) {
    if (!("targetFolder" in item)) {
      // We weren't moving this item, so skip it and continue.
      continue;
    }

    let movedItem = movedItems[index];
    let updatedItem = await PlacesUtils.bookmarks.fetch(movedItem.guid);

    ensurePosition(movedItem, updatedItem.parentGuid, updatedItem.index);

    index++;
  }
}

async function checkLastModifiedForFolders(details, folder, movedItems) {
  // For the tests, the moves always come from folderA.
  if (details.initial.folderA.some(item => "targetFolder" in item && item.targetFolder == folder.title)) {
    let updatedFolder = await PlacesUtils.bookmarks.fetch(folder.guid);

    Assert.greaterOrEqual(updatedFolder.lastModified.getTime(),
                          folder.lastModified.getTime(),
                          "Should have updated the folder's last modified time.");
    print(JSON.stringify(movedItems[0]));
    Assert.deepEqual(updatedFolder.lastModified, movedItems[0].lastModified,
                     "Should have the same last modified as the moved items.");
  }
}

async function testMoveToFolder(details) {
  await PlacesUtils.bookmarks.eraseEverything();

  // Always create two folders by default.
  let [folderA, folderB] =
    await PlacesUtils.bookmarks.insertTree({
      guid: PlacesUtils.bookmarks.unfiledGuid,
      children: [{
        title: "a",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
      }, {
        title: "b",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
      }],
    });

  checkBookmarkObject(folderA);
  let folderAChildren = await insertChildren(folderA, details.initial.folderA);
  checkBookmarkObject(folderB);
  let folderBChildren = await insertChildren(folderB, details.initial.folderB);

  const originalAChildren = folderAChildren.map(child => { return { ...child }; });
  const originalBChildren = folderBChildren.map(child => { return { ...child }; });

  let lastModified;
  if (folderAChildren.length) {
    lastModified = folderAChildren[0].lastModified;
  } else if (folderBChildren.length) {
    lastModified = folderBChildren[0].lastModified;
  } else {
    throw new Error("No children added, can't determine lastModified");
  }

  // Work out which children to move and to where.
  let childrenToUpdate = [];
  for (let i = 0; i < details.initial.folderA.length; i++) {
    if ("move" in details.initial.folderA[i]) {
      childrenToUpdate.push(folderAChildren[i].guid);
    }
  }

  let observer;
  if (details.notifications) {
    observer = expectNotifications(false, true);
  }

  let movedItems = await PlacesUtils.bookmarks.moveToFolder(childrenToUpdate,
    details.targetFolder == "a" ? folderA.guid : folderB.guid,
    details.targetIndex
  );

  await dumpFolderChildren("folderA", details, folderA, folderB,
    originalAChildren, originalBChildren);
  await dumpFolderChildren("folderB", details, folderA, folderB,
    originalAChildren, originalBChildren);

  Assert.equal(movedItems.length, childrenToUpdate.length);
  await checkExpectedResults(details, "folderA", folderA.guid, lastModified,
    movedItems, folderAChildren, folderBChildren);
  await checkExpectedResults(details, "folderB", folderB.guid, lastModified,
    movedItems, folderAChildren, folderBChildren);

  if (details.notifications) {
    let expectedNotifications = [];

    for (let notification of details.notifications) {
      let origItem = notification.originalFolder == "folderA" ?
        originalAChildren[notification.originalIndex] :
        originalBChildren[notification.originalIndex];
      let newFolder = notification.newFolder == "folderA" ? folderA : folderB;

      expectedNotifications.push({
        name: "onItemMoved",
        arguments: [
          await PlacesUtils.promiseItemId(origItem.guid),
          await PlacesUtils.promiseItemId(origItem.parentGuid),
          notification.originalIndex,
          await PlacesUtils.promiseItemId(newFolder.guid),
          notification.newIndex,
          PlacesUtils.bookmarks.TYPE_BOOKMARK,
          origItem.guid,
          origItem.parentGuid,
          newFolder.guid,
          PlacesUtils.bookmarks.SOURCES.DEFAULT,
          origItem.url,
        ],
      });
    }
    observer.check(expectedNotifications);
  }

  await checkLastModifiedForFolders(details, folderA, movedItems);
  await checkLastModifiedForFolders(details, folderB, movedItems);
}

const TYPE_BOOKMARK = PlacesUtils.bookmarks.TYPE_BOOKMARK;

add_task(async function invalid_input_throws() {
  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder(),
                /guids should be an array/);
  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder({}),
                /guids should be an array/);
  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder([]),
                /guids should be an array of at least one item/);

  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder([ "test" ]),
                /Expected only valid GUIDs to be passed/);
  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder([ null ]),
                /Expected only valid GUIDs to be passed/);
  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder([ 123 ]),
                /Expected only valid GUIDs to be passed/);

  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder([ "123456789012" ], 123),
                /Error: parentGuid should be a valid GUID/);

  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder([ "123456789012" ],
                                                         PlacesUtils.bookmarks.rootGuid),
                /Cannot move bookmarks into root/);

  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder([ "123456789012" ],
                                                         "123456789012",
                                                         -2),
                /index should be a number greater than/);
  Assert.throws(() => PlacesUtils.bookmarks.moveToFolder([ "123456789012" ],
                                                         "123456789012",
                                                         "sdffd"),
                /index should be a number greater than/);
});

add_task(async function test_move_nonexisting_bookmark_rejects() {
  await Assert.rejects(PlacesUtils.bookmarks.moveToFolder([
    "123456789012",
  ], "123456789012", -1), /No bookmarks found for the provided GUID/, "Should reject when moving a non-existing bookmark");
});

add_task(async function test_move_folder_into_descendant_rejects() {
  let parent = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
  });
  let descendant = await PlacesUtils.bookmarks.insert({
    parentGuid: parent.guid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
  });

  await Assert.rejects(PlacesUtils.bookmarks.moveToFolder([parent.guid], parent.guid, 0),
    /Cannot insert a folder into itself or one of its descendants/,
    "Should reject when moving a folder into itself");

  await Assert.rejects(PlacesUtils.bookmarks.moveToFolder([parent.guid], descendant.guid, 0),
    /Cannot insert a folder into itself or one of its descendants/,
    "Should reject when moving a folder into a descendant");
});

add_task(async function test_move_from_differnt_with_no_target_rejects() {
  let bm1 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER });
  let bm2 = await PlacesUtils.bookmarks.insert({
    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
  });

  await Assert.rejects(PlacesUtils.bookmarks.moveToFolder([bm1.guid, bm2.guid], null, -1),
    /All bookmarks should be in the same folder if no parent is specified/,
    "Should reject when moving bookmarks from different folders with no target folder");
});

add_task(async function test_move_append_same_folder() {
  await testMoveToFolder({
    initial: {
      folderA: [
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK},
      ],
      folderB: [],
    },
    targetFolder: "a",
    targetIndex: -1,
    expected: {
      folderA: [
        {folder: "a", originalIndex: 0},
        {folder: "a", originalIndex: 2},
        {folder: "a", originalIndex: 1},
      ],
      folderB: [],
    },
    notifications: [{
      originalFolder: "folderA", originalIndex: 1,
      newFolder: "folderA", newIndex: 2,
    }],
  });
});

add_task(async function test_move_append_multiple_same_folder() {
  await testMoveToFolder({
    initial: {
      folderA: [
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK },
      ],
      folderB: [],
    },
    targetFolder: "a",
    targetIndex: -1,
    expected: {
      folderA: [
        {folder: "a", originalIndex: 3},
        {folder: "a", originalIndex: 0},
        {folder: "a", originalIndex: 1},
        {folder: "a", originalIndex: 2},
      ],
      folderB: [],
    },
    // These are all inserted at position 3 as that's what the views require
    // to be notified, to ensure the new items are displayed in their correct
    // positions.
    notifications: [{
      originalFolder: "folderA", originalIndex: 0,
      newFolder: "folderA", newIndex: 3,
    }, {
      originalFolder: "folderA", originalIndex: 1,
      newFolder: "folderA", newIndex: 3,
    }, {
      originalFolder: "folderA", originalIndex: 2,
      newFolder: "folderA", newIndex: 3,
    }],
  });
});


add_task(async function test_move_append_multiple_new_folder() {
  await testMoveToFolder({
    initial: {
      folderA: [
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK, move: true},
      ],
      folderB: [],
    },
    targetFolder: "b",
    targetIndex: -1,
    expected: {
      folderA: [],
      folderB: [
        {folder: "a", originalIndex: 0},
        {folder: "a", originalIndex: 1},
        {folder: "a", originalIndex: 2},
      ],
    },
    notifications: [{
      originalFolder: "folderA", originalIndex: 0,
      newFolder: "folderB", newIndex: 0,
    }, {
      originalFolder: "folderA", originalIndex: 1,
      newFolder: "folderB", newIndex: 1,
    }, {
      originalFolder: "folderA", originalIndex: 2,
      newFolder: "folderB", newIndex: 2,
    }],
  });
});

add_task(async function test_move_append_multiple_new_folder_with_existing() {
  await testMoveToFolder({
    initial: {
      folderA: [
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK, move: true},
      ],
      folderB: [
        {type: TYPE_BOOKMARK },
        {type: TYPE_BOOKMARK },
      ],
    },
    targetFolder: "b",
    targetIndex: -1,
    expected: {
      folderA: [],
      folderB: [
        {folder: "b", originalIndex: 0},
        {folder: "b", originalIndex: 1},
        {folder: "a", originalIndex: 0},
        {folder: "a", originalIndex: 1},
        {folder: "a", originalIndex: 2},
      ],
    },
    notifications: [{
      originalFolder: "folderA", originalIndex: 0,
      newFolder: "folderB", newIndex: 2,
    }, {
      originalFolder: "folderA", originalIndex: 1,
      newFolder: "folderB", newIndex: 3,
    }, {
      originalFolder: "folderA", originalIndex: 2,
      newFolder: "folderB", newIndex: 4,
    }],
  });
});

add_task(async function test_move_insert_same_folder_up() {
  await testMoveToFolder({
    initial: {
      folderA: [
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK, move: true},
      ],
      folderB: [],
    },
    targetFolder: "a",
    targetIndex: 0,
    expected: {
      folderA: [
        {folder: "a", originalIndex: 2},
        {folder: "a", originalIndex: 0},
        {folder: "a", originalIndex: 1},
      ],
      folderB: [],
    },
    notifications: [{
      originalFolder: "folderA", originalIndex: 2,
      newFolder: "folderA", newIndex: 0,
    }],
  });
});

add_task(async function test_move_insert_same_folder_down() {
  await testMoveToFolder({
    initial: {
      folderA: [
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK},
      ],
      folderB: [],
    },
    targetFolder: "a",
    targetIndex: 2,
    expected: {
      folderA: [
        {folder: "a", originalIndex: 1},
        {folder: "a", originalIndex: 0},
        {folder: "a", originalIndex: 2},
      ],
      folderB: [],
    },
    notifications: [{
      originalFolder: "folderA", originalIndex: 0,
      newFolder: "folderA", newIndex: 1,
    }],
  });
});

add_task(async function test_move_insert_multiple_same_folder_split_locations() {
  await testMoveToFolder({
    initial: {
      folderA: [
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK, move: true},
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK},
        {type: TYPE_BOOKMARK, move: true},
      ],
      folderB: [],
    },
    targetFolder: "a",
    targetIndex: 2,
    expected: {
      folderA: [
        {folder: "a", originalIndex: 1},
        {folder: "a", originalIndex: 0},
        {folder: "a", originalIndex: 3},
        {folder: "a", originalIndex: 6},
        {folder: "a", originalIndex: 9},
        {folder: "a", originalIndex: 2},
        {folder: "a", originalIndex: 4},
        {folder: "a", originalIndex: 5},
        {folder: "a", originalIndex: 7},
        {folder: "a", originalIndex: 8},
      ],
      folderB: [],
    },
    notifications: [{
      originalFolder: "folderA", originalIndex: 0,
      newFolder: "folderA", newIndex: 1,
    }, {
      originalFolder: "folderA", originalIndex: 3,
      newFolder: "folderA", newIndex: 2,
    }, {
      originalFolder: "folderA", originalIndex: 6,
      newFolder: "folderA", newIndex: 3,
    }, {
      originalFolder: "folderA", originalIndex: 9,
      newFolder: "folderA", newIndex: 4,
    }],
  });
});

add_task(async function test_move_folder_with_descendant() {
  let parent = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                    type: PlacesUtils.bookmarks.TYPE_FOLDER }) ;
  let bm = await PlacesUtils.bookmarks.insert({ parentGuid: parent.guid,
                                                url: "http://example.com/",
                                                type: PlacesUtils.bookmarks.TYPE_BOOKMARK }) ;
  let descendant = await PlacesUtils.bookmarks.insert({ parentGuid: parent.guid,
                                                        type: PlacesUtils.bookmarks.TYPE_FOLDER });
  Assert.equal(descendant.index, 1);
  let lastModified = bm.lastModified;

  // This is moving to a nonexisting index by purpose, it will be appended.
  [bm] = await PlacesUtils.bookmarks.moveToFolder([bm.guid], descendant.guid, 1);
  checkBookmarkObject(bm);
  Assert.equal(bm.parentGuid, descendant.guid);
  Assert.equal(bm.index, 0);
  Assert.ok(bm.lastModified >= lastModified);

  parent = await PlacesUtils.bookmarks.fetch(parent.guid);
  descendant = await PlacesUtils.bookmarks.fetch(descendant.guid);
  Assert.deepEqual(parent.lastModified, bm.lastModified);
  Assert.deepEqual(descendant.lastModified, bm.lastModified);
  Assert.equal(descendant.index, 0);

  bm = await PlacesUtils.bookmarks.fetch(bm.guid);
  Assert.equal(bm.parentGuid, descendant.guid);
  Assert.equal(bm.index, 0);

  [bm] = await PlacesUtils.bookmarks.moveToFolder([bm.guid], parent.guid, 0);
  Assert.equal(bm.parentGuid, parent.guid);
  Assert.equal(bm.index, 0);

  descendant = await PlacesUtils.bookmarks.fetch(descendant.guid);
  Assert.equal(descendant.index, 1);
});
