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

add_task(async function test_value_structure_conflict() {
  let buf = await openMirror("value_structure_conflict");

  info("Set up mirror");
  let dateAdded = new Date();
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.menuGuid,
    children: [
      {
        guid: "folderAAAAAA",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "A",
        children: [
          {
            guid: "bookmarkBBBB",
            url: "http://example.com/b",
            title: "B",
            dateAdded,
          },
          {
            guid: "bookmarkCCCC",
            url: "http://example.com/c",
            title: "C",
            dateAdded,
          },
        ],
      },
      {
        guid: "folderDDDDDD",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "D",
        children: [
          {
            guid: "bookmarkEEEE",
            url: "http://example.com/e",
            title: "E",
            dateAdded,
          },
        ],
      },
    ],
  });
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["folderAAAAAA", "folderDDDDDD"],
        modified: Date.now() / 1000 - 60,
      },
      {
        id: "folderAAAAAA",
        parentid: "menu",
        type: "folder",
        title: "A",
        children: ["bookmarkBBBB", "bookmarkCCCC"],
        modified: Date.now() / 1000 - 60,
      },
      {
        id: "bookmarkBBBB",
        parentid: "folderAAAAAA",
        type: "bookmark",
        title: "B",
        bmkUri: "http://example.com/b",
        modified: Date.now() / 1000 - 60,
      },
      {
        id: "bookmarkCCCC",
        parentid: "folderAAAAAA",
        type: "bookmark",
        title: "C",
        bmkUri: "http://example.com/c",
        modified: Date.now() / 1000 - 60,
      },
      {
        id: "folderDDDDDD",
        parentid: "menu",
        type: "folder",
        title: "D",
        children: ["bookmarkEEEE"],
        modified: Date.now() / 1000 - 60,
      },
      {
        id: "bookmarkEEEE",
        parentid: "folderDDDDDD",
        type: "bookmark",
        title: "E",
        bmkUri: "http://example.com/e",
        modified: Date.now() / 1000 - 60,
      },
    ]),
    { needsMerge: false }
  );
  await PlacesTestUtils.markBookmarksAsSynced();

  info("Make local value change");
  await PlacesUtils.bookmarks.update({
    guid: "folderAAAAAA",
    title: "A (local)",
  });

  info("Make local structure change");
  await PlacesUtils.bookmarks.update({
    guid: "bookmarkBBBB",
    parentGuid: "folderDDDDDD",
    index: 0,
  });

  info("Make remote value change");
  await storeRecords(buf, [
    {
      id: "folderDDDDDD",
      parentid: "menu",
      type: "folder",
      title: "D (remote)",
      children: ["bookmarkEEEE"],
      modified: Date.now() / 1000 + 60,
    },
  ]);

  info("Apply remote");
  let observer = expectBookmarkChangeNotifications();
  let changesToUpload = await buf.apply({
    remoteTimeSeconds: Date.now() / 1000,
    notifyInStableOrder: true,
  });
  deepEqual(
    await buf.fetchUnmergedGuids(),
    ["folderDDDDDD"],
    "Should leave D with new remote structure unmerged"
  );

  let idsToUpload = inspectChangeRecords(changesToUpload);
  deepEqual(
    idsToUpload,
    {
      updated: ["bookmarkBBBB", "folderAAAAAA", "folderDDDDDD"],
      deleted: [],
    },
    "Should upload records for merged and new local items"
  );

  let localItemIds = await PlacesTestUtils.promiseManyItemIds([
    "folderAAAAAA",
    "bookmarkEEEE",
    "bookmarkBBBB",
    "folderDDDDDD",
  ]);
  observer.check([
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("bookmarkEEEE"),
        oldIndex: 1,
        newIndex: 0,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        guid: "bookmarkEEEE",
        oldParentGuid: "folderDDDDDD",
        newParentGuid: "folderDDDDDD",
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "http://example.com/e",
        isTagging: false,
        title: "E",
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
        dateAdded: dateAdded.getTime(),
        lastVisitDate: null,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("bookmarkBBBB"),
        oldIndex: 0,
        newIndex: 1,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        guid: "bookmarkBBBB",
        oldParentGuid: "folderDDDDDD",
        newParentGuid: "folderDDDDDD",
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "http://example.com/b",
        isTagging: false,
        title: "B",
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
        dateAdded: dateAdded.getTime(),
        lastVisitDate: null,
      },
    },
    {
      name: "bookmark-title-changed",
      params: {
        itemId: localItemIds.get("folderDDDDDD"),
        title: "D (remote)",
        guid: "folderDDDDDD",
        parentGuid: PlacesUtils.bookmarks.menuGuid,
      },
    },
  ]);

  await assertLocalTree(
    PlacesUtils.bookmarks.menuGuid,
    {
      guid: PlacesUtils.bookmarks.menuGuid,
      type: PlacesUtils.bookmarks.TYPE_FOLDER,
      index: 0,
      title: BookmarksMenuTitle,
      children: [
        {
          guid: "folderAAAAAA",
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 0,
          title: "A (local)",
          children: [
            {
              guid: "bookmarkCCCC",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              title: "C",
              url: "http://example.com/c",
            },
          ],
        },
        {
          guid: "folderDDDDDD",
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 1,
          title: "D (remote)",
          children: [
            {
              guid: "bookmarkEEEE",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              title: "E",
              url: "http://example.com/e",
            },
            {
              guid: "bookmarkBBBB",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 1,
              title: "B",
              url: "http://example.com/b",
            },
          ],
        },
      ],
    },
    "Should reconcile structure and value changes"
  );

  await storeChangesInMirror(buf, changesToUpload);
  deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_move() {
  let buf = await openMirror("move");

  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.menuGuid,
    children: [
      {
        guid: "devFolder___",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "Dev",
        children: [
          {
            guid: "mdnBmk______",
            title: "MDN",
            url: "https://developer.mozilla.org",
          },
          {
            type: PlacesUtils.bookmarks.TYPE_FOLDER,
            guid: "mozFolder___",
            title: "Mozilla",
            children: [
              {
                guid: "fxBmk_______",
                title: "Get Firefox!",
                url: "http://getfirefox.com/",
              },
              {
                guid: "nightlyBmk__",
                title: "Nightly",
                url: "https://nightly.mozilla.org",
              },
            ],
          },
          {
            guid: "wmBmk_______",
            title: "Webmaker",
            url: "https://webmaker.org",
          },
        ],
      },
      {
        guid: "bzBmk_______",
        title: "Bugzilla",
        url: "https://bugzilla.mozilla.org",
      },
    ],
  });
  await PlacesTestUtils.markBookmarksAsSynced();

  await storeRecords(
    buf,
    shuffle([
      {
        id: "unfiled",
        parentid: "places",
        type: "folder",
        children: ["mozFolder___"],
      },
      {
        id: "toolbar",
        parentid: "places",
        type: "folder",
        children: ["devFolder___"],
      },
      {
        // Moving to toolbar.
        id: "devFolder___",
        parentid: "toolbar",
        type: "folder",
        title: "Dev",
        children: ["bzBmk_______", "wmBmk_______"],
      },
      {
        // Moving to "Mozilla".
        id: "mdnBmk______",
        parentid: "mozFolder___",
        type: "bookmark",
        title: "MDN",
        bmkUri: "https://developer.mozilla.org",
      },
      {
        // Rearranging children and moving to unfiled.
        id: "mozFolder___",
        parentid: "unfiled",
        type: "folder",
        title: "Mozilla",
        children: ["nightlyBmk__", "mdnBmk______", "fxBmk_______"],
      },
      {
        id: "fxBmk_______",
        parentid: "mozFolder___",
        type: "bookmark",
        title: "Get Firefox!",
        bmkUri: "http://getfirefox.com/",
      },
      {
        id: "nightlyBmk__",
        parentid: "mozFolder___",
        type: "bookmark",
        title: "Nightly",
        bmkUri: "https://nightly.mozilla.org",
      },
      {
        id: "wmBmk_______",
        parentid: "devFolder___",
        type: "bookmark",
        title: "Webmaker",
        bmkUri: "https://webmaker.org",
      },
      {
        id: "bzBmk_______",
        parentid: "devFolder___",
        type: "bookmark",
        title: "Bugzilla",
        bmkUri: "https://bugzilla.mozilla.org",
      },
    ])
  );

  info("Apply remote");
  let observer = expectBookmarkChangeNotifications();
  let changesToUpload = await buf.apply({
    notifyInStableOrder: true,
  });
  deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");

  let idsToUpload = inspectChangeRecords(changesToUpload);
  deepEqual(
    idsToUpload,
    {
      updated: [],
      deleted: [],
    },
    "Should not upload records for remotely moved items"
  );

  let localItemIds = await PlacesTestUtils.promiseManyItemIds([
    "devFolder___",
    "mozFolder___",
    "bzBmk_______",
    "wmBmk_______",
    "nightlyBmk__",
    "mdnBmk______",
    "fxBmk_______",
  ]);
  observer.check([
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("devFolder___"),
        oldIndex: 0,
        newIndex: 0,
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        guid: "devFolder___",
        oldParentGuid: PlacesUtils.bookmarks.menuGuid,
        newParentGuid: PlacesUtils.bookmarks.toolbarGuid,
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "",
        isTagging: false,
        title: "Dev",
        tags: "",
        frecency: 0,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("mozFolder___"),
        oldIndex: 1,
        newIndex: 0,
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        guid: "mozFolder___",
        oldParentGuid: "devFolder___",
        newParentGuid: PlacesUtils.bookmarks.unfiledGuid,
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "",
        isTagging: false,
        title: "Mozilla",
        tags: "",
        frecency: 0,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("bzBmk_______"),
        oldIndex: 1,
        newIndex: 0,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        guid: "bzBmk_______",
        oldParentGuid: PlacesUtils.bookmarks.menuGuid,
        newParentGuid: "devFolder___",
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "https://bugzilla.mozilla.org/",
        isTagging: false,
        title: "Bugzilla",
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("wmBmk_______"),
        oldIndex: 2,
        newIndex: 1,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        guid: "wmBmk_______",
        oldParentGuid: "devFolder___",
        newParentGuid: "devFolder___",
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "https://webmaker.org/",
        isTagging: false,
        title: "Webmaker",
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("nightlyBmk__"),
        oldIndex: 1,
        newIndex: 0,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        guid: "nightlyBmk__",
        oldParentGuid: "mozFolder___",
        newParentGuid: "mozFolder___",
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "https://nightly.mozilla.org/",
        isTagging: false,
        title: "Nightly",
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("mdnBmk______"),
        oldIndex: 0,
        newIndex: 1,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        guid: "mdnBmk______",
        oldParentGuid: "devFolder___",
        newParentGuid: "mozFolder___",
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "https://developer.mozilla.org/",
        isTagging: false,
        title: "MDN",
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("fxBmk_______"),
        oldIndex: 0,
        newIndex: 2,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        guid: "fxBmk_______",
        oldParentGuid: "mozFolder___",
        newParentGuid: "mozFolder___",
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "http://getfirefox.com/",
        isTagging: false,
        title: "Get Firefox!",
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
  ]);

  await assertLocalTree(
    PlacesUtils.bookmarks.rootGuid,
    {
      guid: PlacesUtils.bookmarks.rootGuid,
      type: PlacesUtils.bookmarks.TYPE_FOLDER,
      index: 0,
      title: "",
      children: [
        {
          guid: PlacesUtils.bookmarks.menuGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 0,
          title: BookmarksMenuTitle,
        },
        {
          guid: PlacesUtils.bookmarks.toolbarGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 1,
          title: BookmarksToolbarTitle,
          children: [
            {
              guid: "devFolder___",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 0,
              title: "Dev",
              children: [
                {
                  guid: "bzBmk_______",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 0,
                  title: "Bugzilla",
                  url: "https://bugzilla.mozilla.org/",
                },
                {
                  guid: "wmBmk_______",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 1,
                  title: "Webmaker",
                  url: "https://webmaker.org/",
                },
              ],
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.unfiledGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 3,
          title: UnfiledBookmarksTitle,
          children: [
            {
              guid: "mozFolder___",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 0,
              title: "Mozilla",
              children: [
                {
                  guid: "nightlyBmk__",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 0,
                  title: "Nightly",
                  url: "https://nightly.mozilla.org/",
                },
                {
                  guid: "mdnBmk______",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 1,
                  title: "MDN",
                  url: "https://developer.mozilla.org/",
                },
                {
                  guid: "fxBmk_______",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 2,
                  title: "Get Firefox!",
                  url: "http://getfirefox.com/",
                },
              ],
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.mobileGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 4,
          title: MobileBookmarksTitle,
        },
      ],
    },
    "Should move and reorder bookmarks to match remote"
  );

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_move_into_parent_sibling() {
  // This test moves a bookmark that exists locally into a new folder that only
  // exists remotely, and is a later sibling of the local parent. This ensures
  // we set up the local structure before applying structure changes.
  let buf = await openMirror("move_into_parent_sibling");

  info("Set up mirror: Menu > A > B");
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.menuGuid,
    children: [
      {
        guid: "folderAAAAAA",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "A",
        children: [
          {
            guid: "bookmarkBBBB",
            url: "http://example.com/b",
            title: "B",
          },
        ],
      },
    ],
  });
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["folderAAAAAA"],
      },
      {
        id: "folderAAAAAA",
        parentid: "menu",
        type: "folder",
        title: "A",
        children: ["bookmarkBBBB"],
      },
      {
        id: "bookmarkBBBB",
        parentid: "folderAAAAAA",
        type: "bookmark",
        title: "B",
        bmkUri: "http://example.com/b",
      },
    ]),
    { needsMerge: false }
  );
  await PlacesTestUtils.markBookmarksAsSynced();

  info("Make remote changes: Menu > (A (B > C))");
  await storeRecords(buf, [
    {
      id: "menu",
      parentid: "places",
      type: "folder",
      children: ["folderAAAAAA", "folderCCCCCC"],
    },
    {
      id: "folderAAAAAA",
      parentid: "menu",
      type: "folder",
      title: "A",
    },
    {
      id: "folderCCCCCC",
      parentid: "menu",
      type: "folder",
      title: "C",
      children: ["bookmarkBBBB"],
    },
    {
      id: "bookmarkBBBB",
      parentid: "folderCCCCCC",
      type: "bookmark",
      title: "B",
      bmkUri: "http://example.com/b",
    },
  ]);

  info("Apply remote");
  let observer = expectBookmarkChangeNotifications();
  let changesToUpload = await buf.apply({
    notifyInStableOrder: true,
  });
  deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");

  let idsToUpload = inspectChangeRecords(changesToUpload);
  deepEqual(
    idsToUpload,
    {
      updated: [],
      deleted: [],
    },
    "Should not upload records for remote-only structure changes"
  );

  let localItemIds = await PlacesTestUtils.promiseManyItemIds([
    "folderCCCCCC",
    "bookmarkBBBB",
    "folderAAAAAA",
    PlacesUtils.bookmarks.menuGuid,
  ]);
  observer.check([
    {
      name: "bookmark-added",
      params: {
        itemId: localItemIds.get("folderCCCCCC"),
        parentId: localItemIds.get(PlacesUtils.bookmarks.menuGuid),
        index: 1,
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        urlHref: "",
        title: "C",
        guid: "folderCCCCCC",
        parentGuid: PlacesUtils.bookmarks.menuGuid,
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        tags: "",
        frecency: 0,
        hidden: false,
        visitCount: 0,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("bookmarkBBBB"),
        oldIndex: 0,
        newIndex: 0,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        guid: "bookmarkBBBB",
        oldParentGuid: "folderAAAAAA",
        newParentGuid: "folderCCCCCC",
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "http://example.com/b",
        isTagging: false,
        title: "B",
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
  ]);

  await assertLocalTree(
    PlacesUtils.bookmarks.menuGuid,
    {
      guid: PlacesUtils.bookmarks.menuGuid,
      type: PlacesUtils.bookmarks.TYPE_FOLDER,
      index: 0,
      title: BookmarksMenuTitle,
      children: [
        {
          guid: "folderAAAAAA",
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 0,
          title: "A",
        },
        {
          guid: "folderCCCCCC",
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 1,
          title: "C",
          children: [
            {
              guid: "bookmarkBBBB",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              title: "B",
              url: "http://example.com/b",
            },
          ],
        },
      ],
    },
    "Should set up local structure correctly"
  );

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_complex_move_with_additions() {
  let mergeTelemetryCounts;
  let buf = await openMirror("complex_move_with_additions", {
    recordStepTelemetry(name, took, counts) {
      if (name == "merge") {
        mergeTelemetryCounts = counts;
      }
    },
  });

  info("Set up mirror: Menu > A > (B C)");
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.menuGuid,
    children: [
      {
        guid: "folderAAAAAA",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "A",
        children: [
          {
            guid: "bookmarkBBBB",
            url: "http://example.com/b",
            title: "B",
          },
          {
            guid: "bookmarkCCCC",
            url: "http://example.com/c",
            title: "C",
          },
        ],
      },
    ],
  });
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["folderAAAAAA"],
      },
      {
        id: "folderAAAAAA",
        parentid: "menu",
        type: "folder",
        title: "A",
        children: ["bookmarkBBBB", "bookmarkCCCC"],
      },
      {
        id: "bookmarkBBBB",
        parentid: "folderAAAAAA",
        type: "bookmark",
        title: "B",
        bmkUri: "http://example.com/b",
      },
      {
        id: "bookmarkCCCC",
        parentid: "folderAAAAAA",
        type: "bookmark",
        title: "C",
        bmkUri: "http://example.com/c",
      },
    ]),
    { needsMerge: false }
  );
  await PlacesTestUtils.markBookmarksAsSynced();

  info("Make local change: Menu > A > (B C D)");
  await PlacesUtils.bookmarks.insert({
    guid: "bookmarkDDDD",
    parentGuid: "folderAAAAAA",
    title: "D (local)",
    url: "http://example.com/d-local",
  });

  info("Make remote change: ((Menu > C) (Toolbar > A > (B E)))");
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["bookmarkCCCC"],
      },
      {
        id: "toolbar",
        parentid: "places",
        type: "folder",
        children: ["folderAAAAAA"],
      },
      {
        id: "folderAAAAAA",
        parentid: "toolbar",
        type: "folder",
        title: "A",
        children: ["bookmarkBBBB", "bookmarkEEEE"],
      },
      {
        id: "bookmarkCCCC",
        parentid: "menu",
        type: "bookmark",
        title: "C",
        bmkUri: "http://example.com/c",
      },
      {
        id: "bookmarkEEEE",
        parentid: "folderAAAAAA",
        type: "bookmark",
        title: "E",
        bmkUri: "http://example.com/e",
      },
    ])
  );

  info("Apply remote");
  let observer = expectBookmarkChangeNotifications();
  let changesToUpload = await buf.apply({
    notifyInStableOrder: true,
  });
  deepEqual(
    await buf.fetchUnmergedGuids(),
    ["folderAAAAAA"],
    "Should leave A with new remote structure unmerged"
  );
  deepEqual(
    mergeTelemetryCounts,
    [{ name: "items", count: 10 }],
    "Should record telemetry with structure change counts"
  );

  let idsToUpload = inspectChangeRecords(changesToUpload);
  deepEqual(
    idsToUpload,
    {
      updated: ["bookmarkDDDD", "folderAAAAAA"],
      deleted: [],
    },
    "Should upload new records for (A D)"
  );

  let localItemIds = await PlacesTestUtils.promiseManyItemIds([
    "bookmarkEEEE",
    "folderAAAAAA",
    "bookmarkCCCC",
  ]);
  observer.check([
    {
      name: "bookmark-added",
      params: {
        itemId: localItemIds.get("bookmarkEEEE"),
        parentId: localItemIds.get("folderAAAAAA"),
        index: 1,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        urlHref: "http://example.com/e",
        title: "E",
        guid: "bookmarkEEEE",
        parentGuid: "folderAAAAAA",
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("bookmarkCCCC"),
        oldIndex: 1,
        newIndex: 0,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        guid: "bookmarkCCCC",
        oldParentGuid: "folderAAAAAA",
        newParentGuid: PlacesUtils.bookmarks.menuGuid,
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "http://example.com/c",
        isTagging: false,
        title: "C",
        tags: "",
        frecency: 1,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
    {
      name: "bookmark-moved",
      params: {
        itemId: localItemIds.get("folderAAAAAA"),
        oldIndex: 0,
        newIndex: 0,
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        guid: "folderAAAAAA",
        oldParentGuid: PlacesUtils.bookmarks.menuGuid,
        newParentGuid: PlacesUtils.bookmarks.toolbarGuid,
        source: PlacesUtils.bookmarks.SOURCES.SYNC,
        urlHref: "",
        isTagging: false,
        title: "A",
        tags: "",
        frecency: 0,
        hidden: false,
        visitCount: 0,
        dateAdded: 0,
        lastVisitDate: null,
      },
    },
  ]);

  await assertLocalTree(
    PlacesUtils.bookmarks.rootGuid,
    {
      guid: PlacesUtils.bookmarks.rootGuid,
      type: PlacesUtils.bookmarks.TYPE_FOLDER,
      index: 0,
      title: "",
      children: [
        {
          guid: PlacesUtils.bookmarks.menuGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 0,
          title: BookmarksMenuTitle,
          children: [
            {
              guid: "bookmarkCCCC",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              title: "C",
              url: "http://example.com/c",
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.toolbarGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 1,
          title: BookmarksToolbarTitle,
          children: [
            {
              // We can guarantee child order (B E D), since we always walk remote
              // children first, and the remote folder A record is newer than the
              // local folder. If the local folder were newer, the order would be
              // (D B E).
              guid: "folderAAAAAA",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 0,
              title: "A",
              children: [
                {
                  guid: "bookmarkBBBB",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 0,
                  title: "B",
                  url: "http://example.com/b",
                },
                {
                  guid: "bookmarkEEEE",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 1,
                  title: "E",
                  url: "http://example.com/e",
                },
                {
                  guid: "bookmarkDDDD",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 2,
                  title: "D (local)",
                  url: "http://example.com/d-local",
                },
              ],
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.unfiledGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 3,
          title: UnfiledBookmarksTitle,
        },
        {
          guid: PlacesUtils.bookmarks.mobileGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 4,
          title: MobileBookmarksTitle,
        },
      ],
    },
    "Should take remote order and preserve local children"
  );

  await storeChangesInMirror(buf, changesToUpload);
  deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_reorder_and_insert() {
  let buf = await openMirror("reorder_and_insert");

  info("Set up mirror");
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.menuGuid,
    children: [
      {
        guid: "bookmarkAAAA",
        url: "http://example.com/a",
        title: "A",
      },
      {
        guid: "bookmarkBBBB",
        url: "http://example.com/b",
        title: "B",
      },
      {
        guid: "bookmarkCCCC",
        url: "http://example.com/c",
        title: "C",
      },
    ],
  });
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.toolbarGuid,
    children: [
      {
        guid: "bookmarkDDDD",
        url: "http://example.com/d",
        title: "D",
      },
      {
        guid: "bookmarkEEEE",
        url: "http://example.com/e",
        title: "E",
      },
      {
        guid: "bookmarkFFFF",
        url: "http://example.com/f",
        title: "F",
      },
    ],
  });
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["bookmarkAAAA", "bookmarkBBBB", "bookmarkCCCC"],
      },
      {
        id: "bookmarkAAAA",
        parentid: "menu",
        type: "bookmark",
        title: "A",
        bmkUri: "http://example.com/a",
      },
      {
        id: "bookmarkBBBB",
        parentid: "menu",
        type: "bookmark",
        title: "B",
        bmkUri: "http://example.com/b",
      },
      {
        id: "bookmarkCCCC",
        parentid: "menu",
        type: "bookmark",
        title: "C",
        bmkUri: "http://example.com/c",
      },
      {
        id: "toolbar",
        parentid: "places",
        type: "folder",
        children: ["bookmarkDDDD", "bookmarkEEEE", "bookmarkFFFF"],
      },
      {
        id: "bookmarkDDDD",
        parentid: "toolbar",
        type: "bookmark",
        title: "D",
        bmkUri: "http://example.com/d",
      },
      {
        id: "bookmarkEEEE",
        parentid: "toolbar",
        type: "bookmark",
        title: "E",
        bmkUri: "http://example.com/e",
      },
      {
        id: "bookmarkFFFF",
        parentid: "toolbar",
        type: "bookmark",
        title: "F",
        bmkUri: "http://example.com/f",
      },
    ]),
    { needsMerge: false }
  );
  await PlacesTestUtils.markBookmarksAsSynced();

  let now = Date.now();

  info("Make local changes: Reorder Menu, Toolbar > (G H)");
  await PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.menuGuid, [
    "bookmarkCCCC",
    "bookmarkAAAA",
    "bookmarkBBBB",
  ]);
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.toolbarGuid,
    children: [
      {
        guid: "bookmarkGGGG",
        url: "http://example.com/g",
        title: "G",
        dateAdded: new Date(now),
        lastModified: new Date(now),
      },
      {
        guid: "bookmarkHHHH",
        url: "http://example.com/h",
        title: "H",
        dateAdded: new Date(now),
        lastModified: new Date(now),
      },
    ],
  });

  info("Make remote changes: Reorder Toolbar, Menu > (I J)");
  await storeRecords(
    buf,
    shuffle([
      {
        // The server has a newer toolbar, so we should use the remote order (F D E)
        // as the base, then append (G H).
        id: "toolbar",
        parentid: "places",
        type: "folder",
        children: ["bookmarkFFFF", "bookmarkDDDD", "bookmarkEEEE"],
        modified: now / 1000 + 5,
      },
      {
        // The server has an older menu, so we should use the local order (C A B)
        // as the base, then append (I J).
        id: "menu",
        parentid: "places",
        type: "folder",
        children: [
          "bookmarkAAAA",
          "bookmarkBBBB",
          "bookmarkCCCC",
          "bookmarkIIII",
          "bookmarkJJJJ",
        ],
        modified: now / 1000 - 5,
      },
      {
        id: "bookmarkIIII",
        parentid: "menu",
        type: "bookmark",
        title: "I",
        bmkUri: "http://example.com/i",
      },
      {
        id: "bookmarkJJJJ",
        parentid: "menu",
        type: "bookmark",
        title: "J",
        bmkUri: "http://example.com/j",
      },
    ])
  );

  info("Apply remote");
  let changesToUpload = await buf.apply({
    remoteTimeSeconds: now / 1000,
    localTimeSeconds: now / 1000,
  });
  deepEqual(
    await buf.fetchUnmergedGuids(),
    [PlacesUtils.bookmarks.menuGuid, PlacesUtils.bookmarks.toolbarGuid],
    "Should leave roots with new remote structure unmerged"
  );

  let idsToUpload = inspectChangeRecords(changesToUpload);
  deepEqual(
    idsToUpload,
    {
      updated: ["bookmarkGGGG", "bookmarkHHHH", "menu", "toolbar"],
      deleted: [],
    },
    "Should upload records for merged and new local items"
  );

  await assertLocalTree(
    PlacesUtils.bookmarks.rootGuid,
    {
      guid: PlacesUtils.bookmarks.rootGuid,
      type: PlacesUtils.bookmarks.TYPE_FOLDER,
      index: 0,
      title: "",
      children: [
        {
          guid: PlacesUtils.bookmarks.menuGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 0,
          title: BookmarksMenuTitle,
          children: [
            {
              guid: "bookmarkCCCC",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              url: "http://example.com/c",
              title: "C",
            },
            {
              guid: "bookmarkAAAA",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 1,
              url: "http://example.com/a",
              title: "A",
            },
            {
              guid: "bookmarkBBBB",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 2,
              url: "http://example.com/b",
              title: "B",
            },
            {
              guid: "bookmarkIIII",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 3,
              url: "http://example.com/i",
              title: "I",
            },
            {
              guid: "bookmarkJJJJ",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 4,
              url: "http://example.com/j",
              title: "J",
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.toolbarGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 1,
          title: BookmarksToolbarTitle,
          children: [
            {
              guid: "bookmarkFFFF",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              url: "http://example.com/f",
              title: "F",
            },
            {
              guid: "bookmarkDDDD",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 1,
              url: "http://example.com/d",
              title: "D",
            },
            {
              guid: "bookmarkEEEE",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 2,
              url: "http://example.com/e",
              title: "E",
            },
            {
              guid: "bookmarkGGGG",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 3,
              url: "http://example.com/g",
              title: "G",
            },
            {
              guid: "bookmarkHHHH",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 4,
              url: "http://example.com/h",
              title: "H",
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.unfiledGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 3,
          title: UnfiledBookmarksTitle,
        },
        {
          guid: PlacesUtils.bookmarks.mobileGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 4,
          title: MobileBookmarksTitle,
        },
      ],
    },
    "Should use timestamps to decide base folder order"
  );

  await storeChangesInMirror(buf, changesToUpload);
  deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_newer_remote_moves() {
  let now = Date.now();
  let buf = await openMirror("newer_remote_moves");

  info("Set up mirror");
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.menuGuid,
    children: [
      {
        guid: "bookmarkAAAA",
        url: "http://example.com/a",
        title: "A",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
      },
      {
        guid: "folderBBBBBB",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "B",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
        children: [
          {
            guid: "bookmarkCCCC",
            url: "http://example.com/c",
            title: "C",
            dateAdded: new Date(now - 5000),
            lastModified: new Date(now - 5000),
          },
        ],
      },
      {
        guid: "folderDDDDDD",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "D",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
      },
    ],
  });
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.toolbarGuid,
    children: [
      {
        guid: "bookmarkEEEE",
        url: "http://example.com/e",
        title: "E",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
      },
      {
        guid: "folderFFFFFF",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "F",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
        children: [
          {
            guid: "bookmarkGGGG",
            url: "http://example.com/g",
            title: "G",
            dateAdded: new Date(now - 5000),
            lastModified: new Date(now - 5000),
          },
        ],
      },
      {
        guid: "folderHHHHHH",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "H",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
      },
    ],
  });
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["bookmarkAAAA", "folderBBBBBB", "folderDDDDDD"],
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "bookmarkAAAA",
        parentid: "menu",
        type: "bookmark",
        title: "A",
        bmkUri: "http://example.com/a",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "folderBBBBBB",
        parentid: "menu",
        type: "folder",
        title: "B",
        children: ["bookmarkCCCC"],
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "bookmarkCCCC",
        parentid: "folderBBBBBB",
        type: "bookmark",
        title: "C",
        bmkUri: "http://example.com/c",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "folderDDDDDD",
        parentid: "menu",
        type: "folder",
        title: "D",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "toolbar",
        parentid: "places",
        type: "folder",
        children: ["bookmarkEEEE", "folderFFFFFF", "folderHHHHHH"],
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "bookmarkEEEE",
        parentid: "toolbar",
        type: "bookmark",
        title: "E",
        bmkUri: "http://example.com/e",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "folderFFFFFF",
        parentid: "toolbar",
        type: "folder",
        title: "F",
        children: ["bookmarkGGGG"],
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "bookmarkGGGG",
        parentid: "folderFFFFFF",
        type: "bookmark",
        title: "G",
        bmkUri: "http://example.com/g",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "folderHHHHHH",
        parentid: "toolbar",
        type: "folder",
        title: "H",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
    ]),
    { needsMerge: false }
  );
  await PlacesTestUtils.markBookmarksAsSynced();

  info(
    "Make local changes: Unfiled > A, Mobile > B; Toolbar > (H F E); D > C; H > G"
  );
  let localMoves = [
    {
      guid: "bookmarkAAAA",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    },
    {
      guid: "folderBBBBBB",
      parentGuid: PlacesUtils.bookmarks.mobileGuid,
    },
    {
      guid: "bookmarkCCCC",
      parentGuid: "folderDDDDDD",
    },
    {
      guid: "bookmarkGGGG",
      parentGuid: "folderHHHHHH",
    },
  ];
  for (let { guid, parentGuid } of localMoves) {
    await PlacesUtils.bookmarks.update({
      guid,
      parentGuid,
      index: 0,
      lastModified: new Date(now - 2500),
    });
  }
  await PlacesUtils.bookmarks.reorder(
    PlacesUtils.bookmarks.toolbarGuid,
    ["folderHHHHHH", "folderFFFFFF", "bookmarkEEEE"],
    { lastModified: new Date(now - 2500) }
  );

  info(
    "Make remote changes: Mobile > A, Unfiled > B; Toolbar > (F E H); D > G; H > C"
  );
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["folderDDDDDD"],
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        id: "mobile",
        parentid: "places",
        type: "folder",
        children: ["bookmarkAAAA"],
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        // This is similar to H > C, explained below, except we'll always reupload
        // the mobile root, because we always prefer the local state for roots.
        id: "bookmarkAAAA",
        parentid: "mobile",
        type: "bookmark",
        title: "A",
        bmkUri: "http://example.com/a",
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        id: "unfiled",
        parentid: "places",
        type: "folder",
        children: ["folderBBBBBB"],
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        id: "folderBBBBBB",
        parentid: "unfiled",
        type: "folder",
        title: "B",
        children: [],
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        id: "toolbar",
        parentid: "places",
        type: "folder",
        children: ["folderFFFFFF", "bookmarkEEEE", "folderHHHHHH"],
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        id: "folderHHHHHH",
        parentid: "toolbar",
        type: "folder",
        title: "H",
        children: ["bookmarkCCCC"],
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        // Reparenting an item uploads records for the item and its parent.
        // The merger would still work if we only marked H as unmerged; we'd
        // then use the remote state for H, and local state for C. Since C was
        // changed locally, we'll reupload it, even though it didn't actually
        // change.
        id: "bookmarkCCCC",
        parentid: "folderHHHHHH",
        type: "bookmark",
        title: "C",
        bmkUri: "http://example.com/c",
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        id: "folderDDDDDD",
        parentid: "menu",
        type: "folder",
        title: "D",
        children: ["bookmarkGGGG"],
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        id: "folderDDDDDD",
        parentid: "menu",
        type: "folder",
        title: "D",
        dateAdded: now - 5000,
        modified: now / 1000,
        children: ["bookmarkGGGG"],
      },
      {
        id: "folderFFFFFF",
        parentid: "toolbar",
        type: "folder",
        title: "F",
        children: [],
        dateAdded: now - 5000,
        modified: now / 1000,
      },
      {
        // Same as C above.
        id: "bookmarkGGGG",
        parentid: "folderDDDDDD",
        type: "bookmark",
        title: "G",
        bmkUri: "http://example.com/g",
        dateAdded: now - 5000,
        modified: now / 1000,
      },
    ])
  );

  info("Apply remote");
  let changesToUpload = await buf.apply({
    localTimeSeconds: now / 1000,
    remoteTimeSeconds: now / 1000,
  });
  deepEqual(
    await buf.fetchUnmergedGuids(),
    [
      PlacesUtils.bookmarks.menuGuid,
      PlacesUtils.bookmarks.mobileGuid,
      PlacesUtils.bookmarks.toolbarGuid,
      PlacesUtils.bookmarks.unfiledGuid,
    ],
    "Should leave roots with new remote structure unmerged"
  );
  let datesAdded = await promiseManyDatesAdded([
    PlacesUtils.bookmarks.menuGuid,
    PlacesUtils.bookmarks.mobileGuid,
    PlacesUtils.bookmarks.unfiledGuid,
    PlacesUtils.bookmarks.toolbarGuid,
  ]);
  deepEqual(
    changesToUpload,
    {
      // We took the remote structure for the roots, but they're still flagged as
      // changed locally. Since we always use the local state for roots
      // (bug 1472241), and can't distinguish between value and structure changes
      // in Places (see the comment for F below), we'll reupload them.
      menu: {
        tombstone: false,
        counter: 2,
        synced: false,
        cleartext: {
          id: "menu",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
          children: ["folderDDDDDD"],
          title: BookmarksMenuTitle,
        },
      },
      mobile: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "mobile",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: datesAdded.get(PlacesUtils.bookmarks.mobileGuid),
          children: ["bookmarkAAAA"],
          title: MobileBookmarksTitle,
        },
      },
      unfiled: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "unfiled",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: datesAdded.get(PlacesUtils.bookmarks.unfiledGuid),
          children: ["folderBBBBBB"],
          title: UnfiledBookmarksTitle,
        },
      },
      toolbar: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "toolbar",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: datesAdded.get(PlacesUtils.bookmarks.toolbarGuid),
          children: ["folderFFFFFF", "bookmarkEEEE", "folderHHHHHH"],
          title: BookmarksToolbarTitle,
        },
      },
    },
    "Should only reupload local roots"
  );

  await assertLocalTree(
    PlacesUtils.bookmarks.rootGuid,
    {
      guid: PlacesUtils.bookmarks.rootGuid,
      type: PlacesUtils.bookmarks.TYPE_FOLDER,
      index: 0,
      title: "",
      children: [
        {
          guid: PlacesUtils.bookmarks.menuGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 0,
          title: BookmarksMenuTitle,
          children: [
            {
              guid: "folderDDDDDD",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 0,
              title: "D",
              children: [
                {
                  guid: "bookmarkGGGG",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 0,
                  title: "G",
                  url: "http://example.com/g",
                },
              ],
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.toolbarGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 1,
          title: BookmarksToolbarTitle,
          children: [
            {
              guid: "folderFFFFFF",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 0,
              title: "F",
            },
            {
              guid: "bookmarkEEEE",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 1,
              title: "E",
              url: "http://example.com/e",
            },
            {
              guid: "folderHHHHHH",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 2,
              title: "H",
              children: [
                {
                  guid: "bookmarkCCCC",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 0,
                  title: "C",
                  url: "http://example.com/c",
                },
              ],
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.unfiledGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 3,
          title: UnfiledBookmarksTitle,
          children: [
            {
              guid: "folderBBBBBB",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 0,
              title: "B",
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.mobileGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 4,
          title: MobileBookmarksTitle,
          children: [
            {
              guid: "bookmarkAAAA",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              title: "A",
              url: "http://example.com/a",
            },
          ],
        },
      ],
    },
    "Should use newer remote parents and order"
  );

  await storeChangesInMirror(buf, changesToUpload);
  deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_newer_local_moves() {
  let now = Date.now();
  let buf = await openMirror("newer_local_moves");

  info("Set up mirror");
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.menuGuid,
    children: [
      {
        guid: "bookmarkAAAA",
        url: "http://example.com/a",
        title: "A",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
      },
      {
        guid: "folderBBBBBB",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "B",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
        children: [
          {
            guid: "bookmarkCCCC",
            url: "http://example.com/c",
            title: "C",
            dateAdded: new Date(now - 5000),
            lastModified: new Date(now - 5000),
          },
        ],
      },
      {
        guid: "folderDDDDDD",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "D",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
      },
    ],
  });
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.toolbarGuid,
    children: [
      {
        guid: "bookmarkEEEE",
        url: "http://example.com/e",
        title: "E",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
      },
      {
        guid: "folderFFFFFF",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "F",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
        children: [
          {
            guid: "bookmarkGGGG",
            url: "http://example.com/g",
            title: "G",
            dateAdded: new Date(now - 5000),
            lastModified: new Date(now - 5000),
          },
        ],
      },
      {
        guid: "folderHHHHHH",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "H",
        dateAdded: new Date(now - 5000),
        lastModified: new Date(now - 5000),
      },
    ],
  });
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["bookmarkAAAA", "folderBBBBBB", "folderDDDDDD"],
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "bookmarkAAAA",
        parentid: "menu",
        type: "bookmark",
        title: "A",
        bmkUri: "http://example.com/a",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "folderBBBBBB",
        parentid: "menu",
        type: "folder",
        title: "B",
        children: ["bookmarkCCCC"],
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "bookmarkCCCC",
        parentid: "folderBBBBBB",
        type: "bookmark",
        title: "C",
        bmkUri: "http://example.com/c",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "folderDDDDDD",
        parentid: "menu",
        type: "folder",
        title: "D",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "toolbar",
        parentid: "places",
        type: "folder",
        children: ["bookmarkEEEE", "folderFFFFFF", "folderHHHHHH"],
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "bookmarkEEEE",
        parentid: "toolbar",
        type: "bookmark",
        title: "E",
        bmkUri: "http://example.com/e",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "folderFFFFFF",
        parentid: "toolbar",
        type: "folder",
        title: "F",
        children: ["bookmarkGGGG"],
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "bookmarkGGGG",
        parentid: "folderFFFFFF",
        type: "bookmark",
        title: "G",
        bmkUri: "http://example.com/g",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
      {
        id: "folderHHHHHH",
        parentid: "toolbar",
        type: "folder",
        title: "H",
        dateAdded: now - 5000,
        modified: now / 1000 - 5,
      },
    ]),
    { needsMerge: false }
  );
  await PlacesTestUtils.markBookmarksAsSynced();

  info(
    "Make local changes: Unfiled > A, Mobile > B; Toolbar > (H F E); D > C; H > G"
  );
  let localMoves = [
    {
      guid: "bookmarkAAAA",
      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
    },
    {
      guid: "folderBBBBBB",
      parentGuid: PlacesUtils.bookmarks.mobileGuid,
    },
    {
      guid: "bookmarkCCCC",
      parentGuid: "folderDDDDDD",
    },
    {
      guid: "bookmarkGGGG",
      parentGuid: "folderHHHHHH",
    },
  ];
  for (let { guid, parentGuid } of localMoves) {
    await PlacesUtils.bookmarks.update({
      guid,
      parentGuid,
      index: 0,
      lastModified: new Date(now),
    });
  }
  await PlacesUtils.bookmarks.reorder(
    PlacesUtils.bookmarks.toolbarGuid,
    ["folderHHHHHH", "folderFFFFFF", "bookmarkEEEE"],
    { lastModified: new Date(now) }
  );

  info(
    "Make remote changes: Mobile > A, Unfiled > B; Toolbar > (F E H); D > G; H > C"
  );
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["folderDDDDDD"],
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "mobile",
        parentid: "places",
        type: "folder",
        children: ["bookmarkAAAA"],
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "bookmarkAAAA",
        parentid: "mobile",
        type: "bookmark",
        title: "A",
        bmkUri: "http://example.com/a",
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "unfiled",
        parentid: "places",
        type: "folder",
        children: ["folderBBBBBB"],
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "folderBBBBBB",
        parentid: "unfiled",
        type: "folder",
        title: "B",
        children: [],
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "toolbar",
        parentid: "places",
        type: "folder",
        children: ["folderFFFFFF", "bookmarkEEEE", "folderHHHHHH"],
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "folderHHHHHH",
        parentid: "toolbar",
        type: "folder",
        title: "H",
        children: ["bookmarkCCCC"],
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "bookmarkCCCC",
        parentid: "folderHHHHHH",
        type: "bookmark",
        title: "C",
        bmkUri: "http://example.com/c",
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "folderDDDDDD",
        parentid: "menu",
        type: "folder",
        title: "D",
        children: ["bookmarkGGGG"],
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "folderFFFFFF",
        parentid: "toolbar",
        type: "folder",
        title: "F",
        children: [],
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
      {
        id: "bookmarkGGGG",
        parentid: "folderDDDDDD",
        type: "bookmark",
        title: "G",
        bmkUri: "http://example.com/g",
        dateAdded: now - 5000,
        modified: now / 1000 - 2.5,
      },
    ])
  );

  info("Apply remote");
  let changesToUpload = await buf.apply({
    localTimeSeconds: now / 1000,
    remoteTimeSeconds: now / 1000,
  });
  deepEqual(
    await buf.fetchUnmergedGuids(),
    [
      "bookmarkAAAA",
      "bookmarkCCCC",
      "bookmarkGGGG",
      "folderBBBBBB",
      "folderDDDDDD",
      "folderFFFFFF",
      "folderHHHHHH",
      PlacesUtils.bookmarks.menuGuid,
      PlacesUtils.bookmarks.mobileGuid,
      PlacesUtils.bookmarks.toolbarGuid,
      PlacesUtils.bookmarks.unfiledGuid,
    ],
    "Should leave items with new remote structure unmerged"
  );
  let datesAdded = await promiseManyDatesAdded([
    PlacesUtils.bookmarks.menuGuid,
    PlacesUtils.bookmarks.mobileGuid,
    PlacesUtils.bookmarks.unfiledGuid,
    PlacesUtils.bookmarks.toolbarGuid,
  ]);
  deepEqual(
    changesToUpload,
    {
      // Reupload roots with new children.
      menu: {
        tombstone: false,
        counter: 2,
        synced: false,
        cleartext: {
          id: "menu",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
          children: ["folderDDDDDD"],
          title: BookmarksMenuTitle,
        },
      },
      mobile: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "mobile",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: datesAdded.get(PlacesUtils.bookmarks.mobileGuid),
          children: ["folderBBBBBB"],
          title: MobileBookmarksTitle,
        },
      },
      unfiled: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "unfiled",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: datesAdded.get(PlacesUtils.bookmarks.unfiledGuid),
          children: ["bookmarkAAAA"],
          title: UnfiledBookmarksTitle,
        },
      },
      toolbar: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "toolbar",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: datesAdded.get(PlacesUtils.bookmarks.toolbarGuid),
          children: ["folderHHHHHH", "folderFFFFFF", "bookmarkEEEE"],
          title: BookmarksToolbarTitle,
        },
      },
      // G moved to H from F, so F and H have new children, and we need
      // to upload G for the new `parentid`.
      folderFFFFFF: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "folderFFFFFF",
          type: "folder",
          parentid: "toolbar",
          hasDupe: true,
          parentName: BookmarksToolbarTitle,
          dateAdded: now - 5000,
          children: [],
          title: "F",
        },
      },
      folderHHHHHH: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "folderHHHHHH",
          type: "folder",
          parentid: "toolbar",
          hasDupe: true,
          parentName: BookmarksToolbarTitle,
          dateAdded: now - 5000,
          children: ["bookmarkGGGG"],
          title: "H",
        },
      },
      bookmarkGGGG: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "bookmarkGGGG",
          type: "bookmark",
          parentid: "folderHHHHHH",
          hasDupe: true,
          parentName: "H",
          dateAdded: now - 5000,
          bmkUri: "http://example.com/g",
          title: "G",
        },
      },
      // C moved to D, so we need to reupload D (for `children`) and C
      // (for `parentid`).
      folderDDDDDD: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "folderDDDDDD",
          type: "folder",
          parentid: "menu",
          hasDupe: true,
          parentName: BookmarksMenuTitle,
          dateAdded: now - 5000,
          children: ["bookmarkCCCC"],
          title: "D",
        },
      },
      bookmarkCCCC: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "bookmarkCCCC",
          type: "bookmark",
          parentid: "folderDDDDDD",
          hasDupe: true,
          parentName: "D",
          dateAdded: now - 5000,
          bmkUri: "http://example.com/c",
          title: "C",
        },
      },
      // Reupload A with the new `parentid`. B moved to mobile *and* has
      // new children` so we should upload it, anyway.
      bookmarkAAAA: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "bookmarkAAAA",
          type: "bookmark",
          parentid: "unfiled",
          hasDupe: true,
          parentName: UnfiledBookmarksTitle,
          dateAdded: now - 5000,
          bmkUri: "http://example.com/a",
          title: "A",
        },
      },
      folderBBBBBB: {
        tombstone: false,
        counter: 2,
        synced: false,
        cleartext: {
          id: "folderBBBBBB",
          type: "folder",
          parentid: "mobile",
          hasDupe: true,
          parentName: MobileBookmarksTitle,
          dateAdded: now - 5000,
          children: [],
          title: "B",
        },
      },
    },
    "Should reupload new local structure"
  );

  await assertLocalTree(
    PlacesUtils.bookmarks.rootGuid,
    {
      guid: PlacesUtils.bookmarks.rootGuid,
      type: PlacesUtils.bookmarks.TYPE_FOLDER,
      index: 0,
      title: "",
      children: [
        {
          guid: PlacesUtils.bookmarks.menuGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 0,
          title: BookmarksMenuTitle,
          children: [
            {
              guid: "folderDDDDDD",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 0,
              title: "D",
              children: [
                {
                  guid: "bookmarkCCCC",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 0,
                  title: "C",
                  url: "http://example.com/c",
                },
              ],
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.toolbarGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 1,
          title: BookmarksToolbarTitle,
          children: [
            {
              guid: "folderHHHHHH",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 0,
              title: "H",
              children: [
                {
                  guid: "bookmarkGGGG",
                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                  index: 0,
                  title: "G",
                  url: "http://example.com/g",
                },
              ],
            },
            {
              guid: "folderFFFFFF",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 1,
              title: "F",
            },
            {
              guid: "bookmarkEEEE",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 2,
              title: "E",
              url: "http://example.com/e",
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.unfiledGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 3,
          title: UnfiledBookmarksTitle,
          children: [
            {
              guid: "bookmarkAAAA",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              title: "A",
              url: "http://example.com/a",
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.mobileGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 4,
          title: MobileBookmarksTitle,
          children: [
            {
              guid: "folderBBBBBB",
              type: PlacesUtils.bookmarks.TYPE_FOLDER,
              index: 0,
              title: "B",
            },
          ],
        },
      ],
    },
    "Should use newer local parents and order"
  );

  await storeChangesInMirror(buf, changesToUpload);
  deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});

add_task(async function test_unchanged_newer_changed_older() {
  let buf = await openMirror("unchanged_newer_changed_older");
  let modified = new Date(Date.now() - 5000);

  info("Set up mirror");
  await PlacesUtils.bookmarks.update({
    guid: PlacesUtils.bookmarks.menuGuid,
    dateAdded: new Date(modified.getTime() - 5000),
  });
  await PlacesUtils.bookmarks.update({
    guid: PlacesUtils.bookmarks.toolbarGuid,
    dateAdded: new Date(modified.getTime() - 5000),
  });
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.menuGuid,
    children: [
      {
        guid: "folderAAAAAA",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "A",
        dateAdded: new Date(modified.getTime() - 5000),
        lastModified: modified,
      },
      {
        guid: "bookmarkBBBB",
        url: "http://example.com/b",
        title: "B",
        dateAdded: new Date(modified.getTime() - 5000),
        lastModified: modified,
      },
    ],
  });
  await PlacesUtils.bookmarks.insertTree({
    guid: PlacesUtils.bookmarks.toolbarGuid,
    children: [
      {
        guid: "folderCCCCCC",
        type: PlacesUtils.bookmarks.TYPE_FOLDER,
        title: "C",
        dateAdded: new Date(modified.getTime() - 5000),
        lastModified: modified,
      },
      {
        guid: "bookmarkDDDD",
        url: "http://example.com/d",
        title: "D",
        dateAdded: new Date(modified.getTime() - 5000),
        lastModified: modified,
      },
    ],
  });
  await storeRecords(
    buf,
    shuffle([
      {
        id: "menu",
        parentid: "places",
        type: "folder",
        children: ["folderAAAAAA", "bookmarkBBBB"],
        dateAdded: modified.getTime() - 5000,
        modified: modified.getTime() / 1000,
      },
      {
        id: "folderAAAAAA",
        parentid: "menu",
        type: "folder",
        title: "A",
        dateAdded: modified.getTime() - 5000,
        modified: modified.getTime() / 1000,
      },
      {
        id: "bookmarkBBBB",
        parentid: "menu",
        type: "bookmark",
        title: "B",
        bmkUri: "http://example.com/b",
        dateAdded: modified.getTime() - 5000,
        modified: modified.getTime() / 1000,
      },
      {
        id: "toolbar",
        parentid: "places",
        type: "folder",
        children: ["folderCCCCCC", "bookmarkDDDD"],
        dateAdded: modified.getTime() - 5000,
        modified: modified.getTime() / 1000,
      },
      {
        id: "folderCCCCCC",
        parentid: "toolbar",
        type: "folder",
        title: "C",
        dateAdded: modified.getTime() - 5000,
        modified: modified.getTime() / 1000,
      },
      {
        id: "bookmarkDDDD",
        parentid: "toolbar",
        type: "bookmark",
        title: "D",
        bmkUri: "http://example.com/d",
        dateAdded: modified.getTime() - 5000,
        modified: modified.getTime() / 1000,
      },
    ]),
    { needsMerge: false }
  );
  await PlacesTestUtils.markBookmarksAsSynced();

  // Even though the local menu is newer (local = 5s, remote = 9s; adding E
  // updated the modified times of A and the menu), it's not *changed* locally,
  // so we should merge remote children first.
  info("Add A > E locally with newer time; delete A remotely with older time");
  await PlacesUtils.bookmarks.insert({
    guid: "bookmarkEEEE",
    parentGuid: "folderAAAAAA",
    url: "http://example.com/e",
    title: "E",
    index: 0,
    dateAdded: new Date(modified.getTime() + 5000),
    lastModified: new Date(modified.getTime() + 5000),
  });
  await storeRecords(buf, [
    {
      id: "menu",
      parentid: "places",
      type: "folder",
      children: ["bookmarkBBBB"],
      dateAdded: modified.getTime() - 5000,
      modified: modified.getTime() / 1000 + 1,
    },
    {
      id: "folderAAAAAA",
      deleted: true,
    },
  ]);

  // Even though the remote toolbar is newer (local = 15s, remote = 10s), it's
  // not changed remotely, so we should merge local children first.
  info("Add C > F remotely with newer time; delete C locally with older time");
  await storeRecords(
    buf,
    shuffle([
      {
        id: "folderCCCCCC",
        parentid: "toolbar",
        type: "folder",
        title: "C",
        children: ["bookmarkFFFF"],
        dateAdded: modified.getTime() - 5000,
        modified: modified.getTime() / 1000 + 5,
      },
      {
        id: "bookmarkFFFF",
        parentid: "folderCCCCCC",
        type: "bookmark",
        title: "F",
        bmkUri: "http://example.com/f",
        dateAdded: modified.getTime() - 5000,
        modified: modified.getTime() / 1000 + 5,
      },
    ])
  );
  await PlacesUtils.bookmarks.remove("folderCCCCCC");
  await PlacesUtils.bookmarks.update({
    guid: PlacesUtils.bookmarks.toolbarGuid,
    lastModified: new Date(modified.getTime() - 5000),
    // Use `SOURCES.SYNC` to avoid bumping the change counter and flagging the
    // local toolbar as modified.
    source: PlacesUtils.bookmarks.SOURCES.SYNC,
  });

  info("Apply remote");
  let changesToUpload = await buf.apply({
    localTimeSeconds: modified.getTime() / 1000 + 10,
    remoteTimeSeconds: modified.getTime() / 1000 + 10,
  });
  deepEqual(
    await buf.fetchUnmergedGuids(),
    ["bookmarkFFFF", "folderCCCCCC", PlacesUtils.bookmarks.menuGuid],
    "Should leave deleted C; F and menu with new remote structure unmerged"
  );

  deepEqual(
    changesToUpload,
    {
      menu: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "menu",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: modified.getTime() - 5000,
          children: ["bookmarkBBBB", "bookmarkEEEE"],
          title: BookmarksMenuTitle,
        },
      },
      toolbar: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "toolbar",
          type: "folder",
          parentid: "places",
          hasDupe: true,
          parentName: "",
          dateAdded: modified.getTime() - 5000,
          children: ["bookmarkDDDD", "bookmarkFFFF"],
          title: BookmarksToolbarTitle,
        },
      },
      // Upload E and F with new `parentid`.
      bookmarkEEEE: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "bookmarkEEEE",
          type: "bookmark",
          parentid: "menu",
          hasDupe: true,
          parentName: BookmarksMenuTitle,
          dateAdded: modified.getTime() + 5000,
          bmkUri: "http://example.com/e",
          title: "E",
        },
      },
      bookmarkFFFF: {
        tombstone: false,
        counter: 1,
        synced: false,
        cleartext: {
          id: "bookmarkFFFF",
          type: "bookmark",
          parentid: "toolbar",
          hasDupe: true,
          parentName: BookmarksToolbarTitle,
          dateAdded: modified.getTime() - 5000,
          bmkUri: "http://example.com/f",
          title: "F",
        },
      },
      folderCCCCCC: {
        tombstone: true,
        counter: 1,
        synced: false,
        cleartext: {
          id: "folderCCCCCC",
          deleted: true,
        },
      },
    },
    "Should reupload menu, toolbar, E, F with new structure; tombstone for C"
  );

  await assertLocalTree(
    PlacesUtils.bookmarks.rootGuid,
    {
      guid: PlacesUtils.bookmarks.rootGuid,
      type: PlacesUtils.bookmarks.TYPE_FOLDER,
      index: 0,
      title: "",
      children: [
        {
          guid: PlacesUtils.bookmarks.menuGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 0,
          title: BookmarksMenuTitle,
          children: [
            {
              guid: "bookmarkBBBB",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              title: "B",
              url: "http://example.com/b",
            },
            {
              guid: "bookmarkEEEE",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 1,
              title: "E",
              url: "http://example.com/e",
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.toolbarGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 1,
          title: BookmarksToolbarTitle,
          children: [
            {
              guid: "bookmarkDDDD",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 0,
              title: "D",
              url: "http://example.com/d",
            },
            {
              guid: "bookmarkFFFF",
              type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
              index: 1,
              title: "F",
              url: "http://example.com/f",
            },
          ],
        },
        {
          guid: PlacesUtils.bookmarks.unfiledGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 3,
          title: UnfiledBookmarksTitle,
        },
        {
          guid: PlacesUtils.bookmarks.mobileGuid,
          type: PlacesUtils.bookmarks.TYPE_FOLDER,
          index: 4,
          title: MobileBookmarksTitle,
        },
      ],
    },
    "Should merge children of changed side first, even if they're older"
  );

  let tombstones = await PlacesTestUtils.fetchSyncTombstones();
  deepEqual(
    tombstones.map(({ guid }) => guid),
    ["folderCCCCCC"],
    "Should store local tombstone for C"
  );

  await storeChangesInMirror(buf, changesToUpload);
  deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");

  await buf.finalize();
  await PlacesUtils.bookmarks.eraseEverything();
  await PlacesSyncUtils.bookmarks.reset();
});
