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

"use strict";

const EXPECTED_REMAINING_ROOTS = [
  ...PlacesUtils.bookmarks.userContentRoots,
  PlacesUtils.bookmarks.tagsGuid,
];

const EXPECTED_REMOVED_BOOKMARK_GUIDS = [
  // These first ones are the old left-pane folder queries
  "SNLmwJH6GtW9", // Root Query
  "r0dY_2_y4mlx", // History
  "xGGhZK3b6GnW", // Downloads
  "EJG6I1nKkQFQ", // Tags
  "gSyHo5oNSUJV", // All Bookmarks
  // These are simulated add-on injections that we expect to be removed.
  "exaddon_____",
  "exaddon1____",
  "exaddon2____",
  "exaddon3____",
  "test________",
];

const EXPECTED_REMOVED_ANNOTATIONS = [
  "PlacesOrganizer/OrganizerFolder",
  "PlacesOrganizer/OrganizerQuery",
];

const EXPECTED_REMOVED_PLACES_ENTRIES = ["exaddonh____", "exaddonh3___"];
const EXPECTED_KEPT_PLACES_ENTRY = "exaddonh2___";
const EXPECTED_REMOVED_KEYWORDS = ["exaddon", "exaddon2"];

async function assertItemIn(db, table, field, expectedItems) {
  let rows = await db.execute(`SELECT ${field} from ${table}`);

  Assert.ok(
    rows.length >= expectedItems.length,
    "Should be at least the number of annotations we expect to be removed."
  );

  let fieldValues = rows.map(row => row.getResultByName(field));

  for (let item of expectedItems) {
    Assert.ok(
      fieldValues.includes(item),
      `${table} should have ${expectedItems}`
    );
  }
}

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 });

  let rows = await db.execute(`SELECT * FROM moz_bookmarks_deleted`);
  Assert.equal(rows.length, 0, "Should be nothing in moz_bookmarks_deleted");

  // Break roots parenting, to test for Bug 1472127.
  await db.execute(`INSERT INTO moz_bookmarks (title, parent, position, guid)
                    VALUES ("test", 1, 0, "test________")`);
  await db.execute(`UPDATE moz_bookmarks
                    SET parent = (SELECT id FROM moz_bookmarks WHERE guid = "test________")
                    WHERE guid = "menu________"`);

  await assertItemIn(
    db,
    "moz_anno_attributes",
    "name",
    EXPECTED_REMOVED_ANNOTATIONS
  );
  await assertItemIn(
    db,
    "moz_bookmarks",
    "guid",
    EXPECTED_REMOVED_BOOKMARK_GUIDS
  );
  await assertItemIn(db, "moz_keywords", "keyword", EXPECTED_REMOVED_KEYWORDS);
  await assertItemIn(db, "moz_places", "guid", EXPECTED_REMOVED_PLACES_ENTRIES);

  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_roots_removed() {
  let db = await PlacesUtils.promiseDBConnection();
  let rows = await db.execute(
    `
    SELECT id FROM moz_bookmarks
    WHERE guid = :guid
  `,
    { guid: PlacesUtils.bookmarks.rootGuid }
  );
  Assert.equal(rows.length, 1, "Should have exactly one root row.");
  let rootId = rows[0].getResultByName("id");

  rows = await db.execute(
    `
    SELECT guid FROM moz_bookmarks
    WHERE parent = :rootId`,
    { rootId }
  );

  Assert.equal(
    rows.length,
    EXPECTED_REMAINING_ROOTS.length,
    "Should only have the built-in folder roots."
  );

  for (let row of rows) {
    let guid = row.getResultByName("guid");
    Assert.ok(
      EXPECTED_REMAINING_ROOTS.includes(guid),
      `Should have only the expected guids remaining, unexpected guid: ${guid}`
    );
  }

  // Check the reparented menu now.
  rows = await db.execute(
    `
    SELECT id, parent FROM moz_bookmarks
    WHERE guid = :guid
  `,
    { guid: PlacesUtils.bookmarks.menuGuid }
  );
  Assert.equal(rows.length, 1, "Should have found the menu root.");
  Assert.equal(
    rows[0].getResultByName("parent"),
    await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.rootGuid),
    "Should have moved the menu back to the Places root."
  );
});

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

  let rows = await db.execute(`
    SELECT guid FROM moz_bookmarks_deleted
  `);

  for (let row of rows) {
    let guid = row.getResultByName("guid");
    Assert.ok(
      EXPECTED_REMOVED_BOOKMARK_GUIDS.includes(guid),
      `Should have tombstoned the expected guids, unexpected guid: ${guid}`
    );
  }

  Assert.equal(
    rows.length,
    EXPECTED_REMOVED_BOOKMARK_GUIDS.length,
    "Should have removed all the expected bookmarks."
  );
});

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

  await assertAnnotationsRemoved(db, EXPECTED_REMOVED_ANNOTATIONS);
});

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

  for (let entry of EXPECTED_REMOVED_PLACES_ENTRIES) {
    let rows = await db.execute(`
      SELECT id FROM moz_places
      WHERE guid = '${entry}'`);

    Assert.equal(
      rows.length,
      0,
      `Should have removed an orphaned history entry ${EXPECTED_REMOVED_PLACES_ENTRIES}.`
    );
  }

  let rows = await db.execute(
    `
    SELECT foreign_count FROM moz_places
    WHERE guid = :guid
  `,
    { guid: EXPECTED_KEPT_PLACES_ENTRY }
  );

  Assert.equal(
    rows.length,
    1,
    `Should have kept visited history entry ${EXPECTED_KEPT_PLACES_ENTRY}`
  );

  let foreignCount = rows[0].getResultByName("foreign_count");
  Assert.equal(
    foreignCount,
    0,
    `Should have updated the foreign_count for ${EXPECTED_KEPT_PLACES_ENTRY}`
  );
});

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

  for (let keyword of EXPECTED_REMOVED_KEYWORDS) {
    let rows = await db.execute(
      `
      SELECT keyword FROM moz_keywords
      WHERE keyword = :keyword
    `,
      { keyword }
    );

    Assert.equal(
      rows.length,
      0,
      `Should have removed the expected keyword: ${keyword}.`
    );
  }
});

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

  await assertNoOrphanAnnotations(db);
});

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

  let rows = await db.execute(`
    SELECT place_id FROM moz_keywords
    WHERE place_id NOT IN (SELECT id from moz_places)
  `);

  Assert.equal(rows.length, 0, `Should have no orphan keywords.`);
});

add_task(async function test_meta_exists() {
  let db = await PlacesUtils.promiseDBConnection();
  await db.execute(`SELECT 1 FROM moz_meta`);
});
