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

"use strict";

const BASE_GUID = "null".padEnd(11, "_");
const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
const LAST_USED_META_DATA = "places/bookmarks/edit/lastusedfolder";

let expectedGuids = [];

async function adjustIndices(db, itemGuid) {
  await db.execute(
    `
    UPDATE moz_bookmarks SET
      position = position - 1
    WHERE parent = (SELECT parent FROM moz_bookmarks
                    WHERE guid = :itemGuid) AND
          position >= (SELECT position FROM moz_bookmarks
                       WHERE guid = :itemGuid)`,
    { itemGuid }
  );
}

async function fetchChildInfos(db, parentGuid) {
  let rows = await db.execute(
    `
    SELECT b.guid, b.position, b.syncChangeCounter
    FROM moz_bookmarks b
    JOIN moz_bookmarks p ON p.id = b.parent
    WHERE p.guid = :parentGuid
    ORDER BY b.position`,
    { parentGuid }
  );
  return rows.map(row => ({
    guid: row.getResultByName("guid"),
    position: row.getResultByName("position"),
    syncChangeCounter: row.getResultByName("syncChangeCounter"),
  }));
}

add_task(async function setup() {
  await setupPlacesDatabase("places_v43.sqlite");

  // Setup database contents to be migrated.
  let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME);
  let db = await Sqlite.openConnection({ path });
  // We can reuse the same guid, it doesn't matter for this test.
  await db.execute(
    `INSERT INTO moz_anno_attributes (name)
                    VALUES (:last_used_anno)`,
    { last_used_anno: LAST_USED_ANNO }
  );

  for (let i = 0; i < 3; i++) {
    let guid = `${BASE_GUID}${i}`;
    await db.execute(
      `INSERT INTO moz_bookmarks (guid, type)
                      VALUES (:guid, :type)
                      `,
      { guid, type: PlacesUtils.bookmarks.TYPE_FOLDER }
    );
    await db.execute(
      `INSERT INTO moz_items_annos (item_id, anno_attribute_id, content)
                      VALUES ((SELECT id FROM moz_bookmarks WHERE guid = :guid),
                              (SELECT id FROM moz_anno_attributes WHERE name = :last_used_anno),
                              :content)`,
      {
        guid,
        content: new Date(1517318477569) - (3 - i) * 60 * 60 * 1000,
        last_used_anno: LAST_USED_ANNO,
      }
    );
    expectedGuids.unshift(guid);
  }

  info("Move menu into unfiled");
  await adjustIndices(db, "menu________");
  await db.execute(
    `
    UPDATE moz_bookmarks SET
      parent = (SELECT id FROM moz_bookmarks WHERE guid = :newParentGuid),
      position = IFNULL((SELECT MAX(position) + 1 FROM moz_bookmarks
                         WHERE guid = :newParentGuid), 0)
    WHERE guid = :itemGuid`,
    { newParentGuid: "unfiled_____", itemGuid: "menu________" }
  );

  info("Move toolbar into mobile");
  let mobileChildren = [
    "bookmarkAAAA",
    "bookmarkBBBB",
    "toolbar_____",
    "bookmarkCCCC",
    "bookmarkDDDD",
  ];
  await adjustIndices(db, "toolbar_____");
  for (let position = 0; position < mobileChildren.length; position++) {
    await db.execute(
      `
      INSERT INTO moz_bookmarks(guid, parent, position)
      VALUES(:guid, (SELECT id FROM moz_bookmarks WHERE guid = 'mobile______'),
             :position)
      ON CONFLICT(guid) DO UPDATE SET
        parent = excluded.parent,
        position = excluded.position`,
      { guid: mobileChildren[position], position }
    );
  }

  info("Reset Sync change counters");
  await db.execute(`UPDATE moz_bookmarks SET syncChangeCounter = 0`);

  await db.close();
});

add_task(async function database_is_valid() {
  // Accessing the database for the first time triggers migration.
  Assert.equal(
    PlacesUtils.history.databaseStatus,
    PlacesUtils.history.DATABASE_STATUS_UPGRADED
  );

  let db = await PlacesUtils.promiseDBConnection();
  Assert.equal(await db.getSchemaVersion(), CURRENT_SCHEMA_VERSION);
});

add_task(async function test_folders_migrated() {
  let metaData = await PlacesUtils.metadata.get(LAST_USED_META_DATA);

  Assert.deepEqual(JSON.parse(metaData), expectedGuids);
});

add_task(async function test_annotations_removed() {
  let db = await PlacesUtils.promiseDBConnection();

  await assertAnnotationsRemoved(db, [LAST_USED_ANNO]);
});

add_task(async function test_no_orphan_annotations() {
  let db = await PlacesUtils.promiseDBConnection();

  await assertNoOrphanAnnotations(db);
});

add_task(async function test_roots_fixed() {
  let db = await PlacesUtils.promiseDBConnection();

  let expectedRootInfos = [
    {
      guid: PlacesUtils.bookmarks.tagsGuid,
      position: 0,
      syncChangeCounter: 0,
    },
    {
      guid: PlacesUtils.bookmarks.unfiledGuid,
      position: 1,
      syncChangeCounter: 1,
    },
    {
      guid: PlacesUtils.bookmarks.mobileGuid,
      position: 2,
      syncChangeCounter: 1,
    },
    {
      guid: PlacesUtils.bookmarks.menuGuid,
      position: 3,
      syncChangeCounter: 1,
    },
    {
      guid: PlacesUtils.bookmarks.toolbarGuid,
      position: 4,
      syncChangeCounter: 1,
    },
  ];
  Assert.deepEqual(
    expectedRootInfos,
    await fetchChildInfos(db, PlacesUtils.bookmarks.rootGuid),
    "All roots should be reparented to the Places root"
  );

  let expectedMobileInfos = [
    {
      guid: "bookmarkAAAA",
      position: 0,
      syncChangeCounter: 0,
    },
    {
      guid: "bookmarkBBBB",
      position: 1,
      syncChangeCounter: 0,
    },
    {
      guid: "bookmarkCCCC",
      position: 2,
      syncChangeCounter: 0,
    },
    {
      guid: "bookmarkDDDD",
      position: 3,
      syncChangeCounter: 0,
    },
  ];
  Assert.deepEqual(
    expectedMobileInfos,
    await fetchChildInfos(db, PlacesUtils.bookmarks.mobileGuid),
    "Should fix misparented root sibling positions"
  );
});
