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

// These tests ensure we correctly chunk statements that exceed SQLite's
// binding parameter limit.

// Inserts 1500 unfiled bookmarks. Using `PlacesUtils.bookmarks.insertTree`
// is an order of magnitude slower, so we write bookmarks directly into the
// database.
async function insertManyUnfiledBookmarks(db, url) {
  await db.executeCached(
    `
    INSERT OR IGNORE INTO moz_places(id, url, url_hash, rev_host, hidden,
                                     frecency, guid)
    VALUES((SELECT id FROM moz_places
            WHERE url_hash = hash(:url) AND
                  url = :url), :url, hash(:url), :revHost, 0, -1,
           generate_guid())`,
    { url: url.href, revHost: PlacesUtils.getReversedHost(url) }
  );

  let guids = [];

  for (let position = 0; position < 1500; ++position) {
    let title = position.toString(10);
    let guid = title.padStart(12, "A");
    await db.executeCached(
      `
      INSERT INTO moz_bookmarks(guid, parent, fk, position, type, title,
                                syncStatus, syncChangeCounter)
      VALUES(:guid, (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid),
             (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND
                                              url = :url),
             :position, :type, :title, :syncStatus, 1)`,
      {
        guid,
        parentGuid: PlacesUtils.bookmarks.unfiledGuid,
        position,
        type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
        title,
        syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
      }
    );
    guids.push(guid);
  }

  return guids;
}

add_task(async function test_merged_item_chunking() {
  let buf = await openMirror("merged_item_chunking");

  info("Set up local tree with 1500 bookmarks");
  let localGuids = await buf.db.executeTransaction(function () {
    let url = new URL("http://example.com/a");
    return insertManyUnfiledBookmarks(buf.db, url);
  });
  await PlacesTestUtils.markBookmarksAsSynced();

  info("Set up remote tree with 1500 bookmarks");
  let toolbarRecord = makeRecord({
    id: "toolbar",
    parentid: "places",
    type: "folder",
    children: [],
  });
  let records = [toolbarRecord];
  for (let i = 0; i < 1500; ++i) {
    let title = i.toString(10);
    let guid = title.padStart(12, "B");
    toolbarRecord.children.push(guid);
    records.push(
      makeRecord({
        id: guid,
        parentid: "toolbar",
        type: "bookmark",
        title,
        bmkUri: "http://example.com/b",
      })
    );
  }
  await buf.store(shuffle(records));

  info("Apply remote");
  let changesToUpload = await buf.apply();
  deepEqual(
    await buf.fetchUnmergedGuids(),
    [PlacesUtils.bookmarks.unfiledGuid],
    "Should leave unfiled with new remote structure unmerged"
  );

  let localChildRecordIds =
    await PlacesSyncUtils.bookmarks.fetchChildRecordIds("toolbar");
  deepEqual(
    localChildRecordIds,
    toolbarRecord.children,
    "Should apply all remote toolbar children"
  );

  let guidsToUpload = Object.keys(changesToUpload);
  deepEqual(
    guidsToUpload.sort(),
    ["unfiled", ...localGuids].sort(),
    "Should upload unfiled and all new 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_deletion_chunking() {
  let buf = await openMirror("deletion_chunking");

  info("Set up local tree with 1500 bookmarks");
  let guids = await buf.db.executeTransaction(function () {
    let url = new URL("http://example.com/a");
    return insertManyUnfiledBookmarks(buf.db, url);
  });
  await PlacesTestUtils.markBookmarksAsSynced();

  info("Delete them all on the server");
  let records = [
    makeRecord({
      id: "unfiled",
      parentid: "places",
      type: "folder",
      children: [],
    }),
  ];
  for (let guid of guids) {
    records.push(
      makeRecord({
        id: guid,
        deleted: true,
      })
    );
  }
  await buf.store(shuffle(records));

  info("Apply remote");
  let changesToUpload = await buf.apply();
  deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
  deepEqual(changesToUpload, {}, "Should take all remote deletions");

  let tombstones = await PlacesTestUtils.fetchSyncTombstones();
  deepEqual(tombstones, [], "Shouldn't store tombstones for remote deletions");

  let localChildRecordIds =
    await PlacesSyncUtils.bookmarks.fetchChildRecordIds("unfiled");
  deepEqual(
    localChildRecordIds,
    [],
    "Should delete all unfiled children locally"
  );

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