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

add_task(async function invalid_input_throws() {
  Assert.throws(
    () => PlacesUtils.bookmarks.reorder(),
    /Invalid value for property 'guid'/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.reorder(null),
    /Invalid value for property 'guid'/
  );

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

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

  Assert.throws(
    () => PlacesUtils.bookmarks.reorder("123456789012"),
    /Must provide a sorted array of children GUIDs./
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.reorder("123456789012", {}),
    /Must provide a sorted array of children GUIDs./
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.reorder("123456789012", null),
    /Must provide a sorted array of children GUIDs./
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.reorder("123456789012", []),
    /Must provide a sorted array of children GUIDs./
  );

  Assert.throws(
    () => PlacesUtils.bookmarks.reorder("123456789012", [null]),
    /Invalid GUID found in the sorted children array/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.reorder("123456789012", [""]),
    /Invalid GUID found in the sorted children array/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.reorder("123456789012", [{}]),
    /Invalid GUID found in the sorted children array/
  );
  Assert.throws(
    () => PlacesUtils.bookmarks.reorder("123456789012", ["012345678901", null]),
    /Invalid GUID found in the sorted children array/
  );
});

add_task(async function reorder_nonexistent_guid() {
  await Assert.rejects(
    PlacesUtils.bookmarks.reorder("123456789012", ["012345678901"]),
    /No folder found for the provided GUID/,
    "Should throw for nonexisting guid"
  );
});

add_task(async function reorder() {
  let bookmarks = [
    {
      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,
    },
    {
      url: "http://example2.com/",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    },
    {
      url: "http://example3.com/",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    },
  ];

  let sorted = [];
  for (let bm of bookmarks) {
    sorted.push(await PlacesUtils.bookmarks.insert(bm));
  }

  // Check the initial append sorting.
  Assert.ok(
    sorted.every((bm, i) => bm.index == i),
    "Initial bookmarks sorting is correct"
  );

  // Apply random sorting and run multiple tests.
  for (let t = 0; t < 4; t++) {
    sorted.sort(() => 0.5 - Math.random());
    let sortedGuids = sorted.map(child => child.guid);
    dump("Expected order: " + sortedGuids.join() + "\n");
    // Add a nonexisting guid to the array, to ensure nothing will break.
    sortedGuids.push("123456789012");
    await PlacesUtils.bookmarks.reorder(
      PlacesUtils.bookmarks.unfiledGuid,
      sortedGuids
    );
    for (let i = 0; i < sorted.length; ++i) {
      let item = await PlacesUtils.bookmarks.fetch(sorted[i].guid);
      Assert.equal(item.index, i);
    }
  }

  info("Test partial sorting");
  {
    // Try a partial sorting by passing 2 entries in same order as they
    // currently have. No entries should change order.
    let sortedGuids = [sorted[0].guid, sorted[3].guid];
    dump("Expected order: " + sorted.map(b => b.guid).join() + "\n");
    await PlacesUtils.bookmarks.reorder(
      PlacesUtils.bookmarks.unfiledGuid,
      sortedGuids
    );
    for (let i = 0; i < sorted.length; ++i) {
      let item = await PlacesUtils.bookmarks.fetch(sorted[i].guid);
      Assert.equal(item.index, i);
    }
  }

  {
    // Try a partial sorting by passing 2 entries out of order
    // The unspecified entries should be appended and retain the original order
    sorted = [sorted[1], sorted[0]].concat(sorted.slice(2));
    let sortedGuids = [sorted[0].guid, sorted[1].guid];
    dump("Expected order: " + sorted.map(b => b.guid).join() + "\n");
    await PlacesUtils.bookmarks.reorder(
      PlacesUtils.bookmarks.unfiledGuid,
      sortedGuids
    );
    for (let i = 0; i < sorted.length; ++i) {
      let item = await PlacesUtils.bookmarks.fetch(sorted[i].guid);
      Assert.equal(item.index, i);
    }
  }
  // Use triangular numbers to detect skipped position.
  let db = await PlacesUtils.promiseDBConnection();
  let rows = await db.execute(
    `SELECT parent
     FROM moz_bookmarks
     GROUP BY parent
     HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0`
  );
  Assert.equal(
    rows.length,
    0,
    "All the bookmarks should have consistent positions"
  );
});

add_task(async function move_and_reorder() {
  // Start clean.
  await PlacesUtils.bookmarks.eraseEverything();

  let bm1 = await PlacesUtils.bookmarks.insert({
    url: "http://example1.com/",
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
  });
  let f1 = await PlacesUtils.bookmarks.insert({
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
  });
  let bm2 = await PlacesUtils.bookmarks.insert({
    url: "http://example2.com/",
    parentGuid: f1.guid,
  });
  let f2 = await PlacesUtils.bookmarks.insert({
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
  });
  let bm3 = await PlacesUtils.bookmarks.insert({
    url: "http://example3.com/",
    parentGuid: f2.guid,
  });
  let bm4 = await PlacesUtils.bookmarks.insert({
    url: "http://example4.com/",
    parentGuid: f2.guid,
  });
  let bm5 = await PlacesUtils.bookmarks.insert({
    url: "http://example5.com/",
    parentGuid: f2.guid,
  });

  // Invert f2 children.
  // This is critical to reproduce the bug, cause it inverts the position
  // compared to the natural insertion order.
  await PlacesUtils.bookmarks.reorder(f2.guid, [bm5.guid, bm4.guid, bm3.guid]);

  bm1.parentGuid = f1.guid;
  bm1.index = 0;
  await PlacesUtils.bookmarks.update(bm1);

  bm1 = await PlacesUtils.bookmarks.fetch(bm1.guid);
  Assert.equal(bm1.index, 0);
  bm2 = await PlacesUtils.bookmarks.fetch(bm2.guid);
  Assert.equal(bm2.index, 1);
  bm3 = await PlacesUtils.bookmarks.fetch(bm3.guid);
  Assert.equal(bm3.index, 2);
  bm4 = await PlacesUtils.bookmarks.fetch(bm4.guid);
  Assert.equal(bm4.index, 1);
  bm5 = await PlacesUtils.bookmarks.fetch(bm5.guid);
  Assert.equal(bm5.index, 0);

  // No-op reorder on f1 children.
  // Nothing should change. Though, due to bug 1293365 this was causing children
  // of other folders to get messed up.
  await PlacesUtils.bookmarks.reorder(f1.guid, [bm1.guid, bm2.guid]);

  bm1 = await PlacesUtils.bookmarks.fetch(bm1.guid);
  Assert.equal(bm1.index, 0);
  bm2 = await PlacesUtils.bookmarks.fetch(bm2.guid);
  Assert.equal(bm2.index, 1);
  bm3 = await PlacesUtils.bookmarks.fetch(bm3.guid);
  Assert.equal(bm3.index, 2);
  bm4 = await PlacesUtils.bookmarks.fetch(bm4.guid);
  Assert.equal(bm4.index, 1);
  bm5 = await PlacesUtils.bookmarks.fetch(bm5.guid);
  Assert.equal(bm5.index, 0);
});

add_task(async function reorder_empty_folder_invalid_children() {
  // Start clean.
  await PlacesUtils.bookmarks.eraseEverything();

  let f1 = await PlacesUtils.bookmarks.insert({
    type: PlacesUtils.bookmarks.TYPE_FOLDER,
    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
  });
  // Specifying a child that doesn't exist should cause that to be ignored.
  // However, before bug 1333304, doing this on an empty folder threw.
  await PlacesUtils.bookmarks.reorder(f1.guid, ["123456789012"]);
});

add_task(async function reorder_lastModified() {
  // Start clean.
  await PlacesUtils.bookmarks.eraseEverything();

  let lastModified = new Date(Date.now() - 1000);
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.menuGuid,
    children: [
      {
        guid: "bookmarkAAAA",
        url: "http://example.com/a",
        title: "A",
        dateAdded: lastModified,
        lastModified,
      },
      {
        guid: "bookmarkBBBB",
        url: "http://example.com/b",
        title: "B",
        dateAdded: lastModified,
        lastModified,
      },
    ],
  });
  await PlacesUtils.bookmarks.update({
    guid: PlacesUtils.bookmarks.menuGuid,
    lastModified,
  });

  info("Reorder and set explicit last modified time");
  let newLastModified = new Date(lastModified.getTime() + 500);
  await PlacesUtils.bookmarks.reorder(
    PlacesUtils.bookmarks.menuGuid,
    ["bookmarkBBBB", "bookmarkAAAA"],
    { lastModified: newLastModified }
  );
  for (let guid of [
    PlacesUtils.bookmarks.menuGuid,
    "bookmarkAAAA",
    "bookmarkBBBB",
  ]) {
    let info = await PlacesUtils.bookmarks.fetch(guid);
    Assert.equal(info.lastModified.getTime(), newLastModified.getTime());
  }

  info("Reorder and set default last modified time");
  await PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.menuGuid, [
    "bookmarkAAAA",
    "bookmarkBBBB",
  ]);
  for (let guid of [
    PlacesUtils.bookmarks.menuGuid,
    "bookmarkAAAA",
    "bookmarkBBBB",
  ]) {
    let info = await PlacesUtils.bookmarks.fetch(guid);
    Assert.greater(info.lastModified.getTime(), newLastModified.getTime());
  }
});
