"use strict";

let AutoMigrateBackstage = Cu.import("resource:///modules/AutoMigrate.jsm"); /* globals AutoMigrate */

let gShimmedMigratorKeyPicker = null;
let gShimmedMigrator = null;

const kUsecPerMin = 60 * 1000000;

// This is really a proxy on MigrationUtils, but if we specify that directly,
// we get in trouble because the object itself is frozen, and Proxies can't
// return a different value to an object when directly proxying a frozen
// object.
AutoMigrateBackstage.MigrationUtils = new Proxy({}, {
  get(target, name) {
    if (name == "getMigratorKeyForDefaultBrowser" && gShimmedMigratorKeyPicker) {
      return gShimmedMigratorKeyPicker;
    }
    if (name == "getMigrator" && gShimmedMigrator) {
      return function() { return gShimmedMigrator };
    }
    return MigrationUtils[name];
  },
});

do_register_cleanup(function() {
  AutoMigrateBackstage.MigrationUtils = MigrationUtils;
});

// This should be replaced by using History.fetch with a fetchVisits option,
// once that becomes available
function* visitsForURL(url)
{
  let visitCount = 0;
  let db = yield PlacesUtils.promiseDBConnection();
  visitCount = yield db.execute(
    `SELECT count(*) FROM moz_historyvisits v
     JOIN moz_places h ON h.id = v.place_id
     WHERE url_hash = hash(:url) AND url = :url`,
     {url});
  visitCount = visitCount[0].getInt64(0);
  return visitCount;
}


/**
 * Test automatically picking a browser to migrate from
 */
add_task(function* checkMigratorPicking() {
  Assert.throws(() => AutoMigrate.pickMigrator("firefox"),
                /Can't automatically migrate from Firefox/,
                "Should throw when explicitly picking Firefox.");

  Assert.throws(() => AutoMigrate.pickMigrator("gobbledygook"),
                /migrator object is not available/,
                "Should throw when passing unknown migrator key");
  gShimmedMigratorKeyPicker = function() {
    return "firefox";
  };
  Assert.throws(() => AutoMigrate.pickMigrator(),
                /Can't automatically migrate from Firefox/,
                "Should throw when implicitly picking Firefox.");
  gShimmedMigratorKeyPicker = function() {
    return "gobbledygook";
  };
  Assert.throws(() => AutoMigrate.pickMigrator(),
                /migrator object is not available/,
                "Should throw when an unknown migrator is the default");
  gShimmedMigratorKeyPicker = function() {
    return "";
  };
  Assert.throws(() => AutoMigrate.pickMigrator(),
                /Could not determine default browser key/,
                "Should throw when an unknown migrator is the default");
});


/**
 * Test automatically picking a profile to migrate from
 */
add_task(function* checkProfilePicking() {
  let fakeMigrator = {sourceProfiles: [{id: "a"}, {id: "b"}]};
  let profB = fakeMigrator.sourceProfiles[1];
  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
                /Don't know how to pick a profile when more/,
                "Should throw when there are multiple profiles.");
  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
                /Profile specified was not found/,
                "Should throw when the profile supplied doesn't exist.");
  let profileToMigrate = AutoMigrate.pickProfile(fakeMigrator, "b");
  Assert.equal(profileToMigrate, profB, "Should return profile supplied");

  fakeMigrator.sourceProfiles = null;
  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
                /Profile specified but only a default profile found./,
                "Should throw when the profile supplied doesn't exist.");
  profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
  Assert.equal(profileToMigrate, null, "Should return default profile when that's the only one.");

  fakeMigrator.sourceProfiles = [];
  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
                /No profile data found/,
                "Should throw when no profile data is present.");

  fakeMigrator.sourceProfiles = [{id: "a"}];
  let profA = fakeMigrator.sourceProfiles[0];
  profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
  Assert.equal(profileToMigrate, profA, "Should return the only profile if only one is present.");
});

/**
 * Test the complete automatic process including browser and profile selection,
 * and actual migration (which implies startup)
 */
add_task(function* checkIntegration() {
  gShimmedMigrator = {
    get sourceProfiles() {
      do_print("Read sourceProfiles");
      return null;
    },
    getMigrateData(profileToMigrate) {
      this._getMigrateDataArgs = profileToMigrate;
      return Ci.nsIBrowserProfileMigrator.BOOKMARKS;
    },
    migrate(types, startup, profileToMigrate) {
      this._migrateArgs = [types, startup, profileToMigrate];
    },
  };
  gShimmedMigratorKeyPicker = function() {
    return "gobbledygook";
  };
  AutoMigrate.migrate("startup");
  Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
                     "getMigrateData called with 'null' as a profile");

  let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
  let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS;
  Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
                   "migrate called with 'null' as a profile");
});

/**
 * Test the undo preconditions and a no-op undo in the automigrator.
 */
add_task(function* checkUndoPreconditions() {
  let shouldAddData = false;
  gShimmedMigrator = {
    get sourceProfiles() {
      do_print("Read sourceProfiles");
      return null;
    },
    getMigrateData(profileToMigrate) {
      this._getMigrateDataArgs = profileToMigrate;
      return Ci.nsIBrowserProfileMigrator.BOOKMARKS;
    },
    migrate(types, startup, profileToMigrate) {
      this._migrateArgs = [types, startup, profileToMigrate];
      if (shouldAddData) {
        // Insert a login and check that that worked.
        MigrationUtils.insertLoginWrapper({
          hostname: "www.mozilla.org",
          formSubmitURL: "http://www.mozilla.org",
          username: "user",
          password: "pass",
        });
      }
      TestUtils.executeSoon(function() {
        Services.obs.notifyObservers(null, "Migration:Ended", undefined);
      });
    },
  };

  gShimmedMigratorKeyPicker = function() {
    return "gobbledygook";
  };
  AutoMigrate.migrate("startup");
  let migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended");
  Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
                     "getMigrateData called with 'null' as a profile");

  let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
  let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS;
  Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
                   "migrate called with 'null' as a profile");

  yield migrationFinishedPromise;
  Assert.ok(Preferences.has("browser.migrate.automigrate.browser"),
            "Should have set browser pref");
  Assert.ok(!(yield AutoMigrate.canUndo()), "Should not be able to undo migration, as there's no data");
  gShimmedMigrator._migrateArgs = null;
  gShimmedMigrator._getMigrateDataArgs = null;
  Preferences.reset("browser.migrate.automigrate.browser");
  shouldAddData = true;

  AutoMigrate.migrate("startup");
  migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended");
  Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
                     "getMigrateData called with 'null' as a profile");
  Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
                   "migrate called with 'null' as a profile");

  yield migrationFinishedPromise;
  let storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
                                                "http://www.mozilla.org", null);
  Assert.equal(storedLogins.length, 1, "Should have 1 login");

  Assert.ok(Preferences.has("browser.migrate.automigrate.browser"),
            "Should have set browser pref");
  Assert.ok((yield AutoMigrate.canUndo()), "Should be able to undo migration, as now there's data");

  yield AutoMigrate.undo();
  Assert.ok(true, "Should be able to finish an undo cycle.");

  // Check that the undo removed the passwords:
  storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
                                                "http://www.mozilla.org", null);
  Assert.equal(storedLogins.length, 0, "Should have no logins");
});

/**
 * Fake a migration and then try to undo it to verify all data gets removed.
 */
add_task(function* checkUndoRemoval() {
  MigrationUtils.initializeUndoData();
  Preferences.set("browser.migrate.automigrate.browser", "automationbrowser");
  // Insert a login and check that that worked.
  MigrationUtils.insertLoginWrapper({
    hostname: "www.mozilla.org",
    formSubmitURL: "http://www.mozilla.org",
    username: "user",
    password: "pass",
  });
  let storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
                                                "http://www.mozilla.org", null);
  Assert.equal(storedLogins.length, 1, "Should have 1 login");

  // Insert a bookmark and check that we have exactly 1 bookmark for that URI.
  yield MigrationUtils.insertBookmarkWrapper({
    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
    url: "http://www.example.org/",
    title: "Some example bookmark",
  });

  let bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"});
  Assert.ok(bookmark, "Should have a bookmark before undo");
  Assert.equal(bookmark.title, "Some example bookmark", "Should have correct bookmark before undo.");

  // Insert 2 history visits
  let now_uSec = Date.now() * 1000;
  let visitedURI = Services.io.newURI("http://www.example.com/", null, null);
  let frecencyUpdatePromise = new Promise(resolve => {
    let expectedChanges = 2;
    let observer = {
      onFrecencyChanged: function() {
        if (!--expectedChanges) {
          PlacesUtils.history.removeObserver(observer);
          resolve();
        }
      },
    };
    PlacesUtils.history.addObserver(observer, false);
  });
  yield MigrationUtils.insertVisitsWrapper([{
    uri: visitedURI,
    visits: [
      {
        transitionType: PlacesUtils.history.TRANSITION_LINK,
        visitDate: now_uSec,
      },
      {
        transitionType: PlacesUtils.history.TRANSITION_LINK,
        visitDate: now_uSec - 100 * kUsecPerMin,
      },
    ]
  }]);
  yield frecencyUpdatePromise;

  // Verify that both visits get reported.
  let opts = PlacesUtils.history.getNewQueryOptions();
  opts.resultType = opts.RESULTS_AS_VISIT;
  let query = PlacesUtils.history.getNewQuery();
  query.uri = visitedURI;
  let visits = PlacesUtils.history.executeQuery(query, opts);
  visits.root.containerOpen = true;
  Assert.equal(visits.root.childCount, 2, "Should have 2 visits");
  // Clean up:
  visits.root.containerOpen = false;

  yield AutoMigrate.saveUndoState();

  // Verify that we can undo, then undo:
  Assert.ok(AutoMigrate.canUndo(), "Should be possible to undo migration");
  yield AutoMigrate.undo();

  let histograms = [
    "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_ERRORCOUNT",
    "FX_STARTUP_MIGRATION_UNDO_LOGINS_ERRORCOUNT",
    "FX_STARTUP_MIGRATION_UNDO_VISITS_ERRORCOUNT",
  ];
  for (let histogramId of histograms) {
    let keyedHistogram = Services.telemetry.getKeyedHistogramById(histogramId);
    let histogramData = keyedHistogram.snapshot().automationbrowser;
    Assert.equal(histogramData.sum, 0, `Should have reported 0 errors to ${histogramId}.`);
    Assert.greaterOrEqual(histogramData.counts[0], 1, `Should have reported value of 0 one time to ${histogramId}.`);
  }
  histograms = [
    "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_MS",
    "FX_STARTUP_MIGRATION_UNDO_LOGINS_MS",
    "FX_STARTUP_MIGRATION_UNDO_VISITS_MS",
    "FX_STARTUP_MIGRATION_UNDO_TOTAL_MS",
  ];
  for (let histogramId of histograms) {
    Assert.greater(Services.telemetry.getKeyedHistogramById(histogramId).snapshot().automationbrowser.sum, 0,
                   `Should have reported non-zero time spent using undo for ${histogramId}`);
  }

  // Check that the undo removed the history visits:
  visits = PlacesUtils.history.executeQuery(query, opts);
  visits.root.containerOpen = true;
  Assert.equal(visits.root.childCount, 0, "Should have no more visits");
  visits.root.containerOpen = false;

  // Check that the undo removed the bookmarks:
  bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"});
  Assert.ok(!bookmark, "Should have no bookmarks after undo");

  // Check that the undo removed the passwords:
  storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
                                            "http://www.mozilla.org", null);
  Assert.equal(storedLogins.length, 0, "Should have no logins");
});

add_task(function* checkUndoBookmarksState() {
  MigrationUtils.initializeUndoData();
  const {TYPE_FOLDER, TYPE_BOOKMARK} = PlacesUtils.bookmarks;
  let title = "Some example bookmark";
  let url = "http://www.example.com";
  let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
  let {guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
    title, url, parentGuid
  });
  Assert.deepEqual((yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks"),
      [{lastModified, parentGuid, guid, type: TYPE_BOOKMARK}]);

  MigrationUtils.initializeUndoData();
  ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
    title, parentGuid, type: TYPE_FOLDER
  }));
  let folder = {guid, lastModified, parentGuid, type: TYPE_FOLDER};
  let folderGuid = folder.guid;
  ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
    title, url, parentGuid: folderGuid
  }));
  let kid1 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};
  ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
    title, url, parentGuid: folderGuid
  }));
  let kid2 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};

  let bookmarksUndo = (yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks");
  Assert.equal(bookmarksUndo.length, 3);
  // We expect that the last modified time from first kid #1 and then kid #2
  // has been propagated to the folder:
  folder.lastModified = kid2.lastModified;
  // Not just using deepEqual on the entire array (which should work) because
  // the failure messages get truncated by xpcshell which is unhelpful.
  Assert.deepEqual(bookmarksUndo[0], folder);
  Assert.deepEqual(bookmarksUndo[1], kid1);
  Assert.deepEqual(bookmarksUndo[2], kid2);
  yield PlacesUtils.bookmarks.eraseEverything();
});

add_task(function* testBookmarkRemovalByUndo() {
  const {TYPE_FOLDER} = PlacesUtils.bookmarks;
  MigrationUtils.initializeUndoData();
  let title = "Some example bookmark";
  let url = "http://www.mymagicaluniqueurl.com";
  let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
  let {guid} = yield MigrationUtils.insertBookmarkWrapper({
    title: "Some folder", parentGuid, type: TYPE_FOLDER
  });
  let folderGuid = guid;
  let itemsToRemove = [];
  ({guid} = yield MigrationUtils.insertBookmarkWrapper({
    title: "Inner folder", parentGuid: folderGuid, type: TYPE_FOLDER
  }));
  let innerFolderGuid = guid;
  itemsToRemove.push(innerFolderGuid);

  ({guid} = yield MigrationUtils.insertBookmarkWrapper({
    title: "Inner inner folder", parentGuid: innerFolderGuid, type: TYPE_FOLDER
  }));
  itemsToRemove.push(guid);

  ({guid} = yield MigrationUtils.insertBookmarkWrapper({
    title: "Inner nested item", url: "http://inner-nested-example.com", parentGuid: guid
  }));
  itemsToRemove.push(guid);

  ({guid} = yield MigrationUtils.insertBookmarkWrapper({
    title, url, parentGuid: folderGuid
  }));
  itemsToRemove.push(guid);

  for (let toBeRemovedGuid of itemsToRemove) {
    let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(toBeRemovedGuid);
    Assert.ok(dbResultForGuid, "Should be able to find items that will be removed.");
  }
  let bookmarkUndoState = (yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks");
  // Now insert a separate item into this folder, not related to the migration.
  let newItem = yield PlacesUtils.bookmarks.insert(
    {title: "Not imported", parentGuid: folderGuid, url: "http://www.example.com"}
  );

  yield AutoMigrate._removeUnchangedBookmarks(bookmarkUndoState);
  Assert.ok(true, "Successfully removed imported items.");

  let itemFromDB = yield PlacesUtils.bookmarks.fetch(newItem.guid);
  Assert.ok(itemFromDB, "Item we inserted outside of migration is still there.");
  itemFromDB = yield PlacesUtils.bookmarks.fetch(folderGuid);
  Assert.ok(itemFromDB, "Folder we inserted in migration is still there because of new kids.");
  for (let removedGuid of itemsToRemove) {
    let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(removedGuid);
    let dbgStr = dbResultForGuid && dbResultForGuid.title;
    Assert.equal(null, dbResultForGuid, "Should not be able to find items that should have been removed, but found " + dbgStr);
  }
  yield PlacesUtils.bookmarks.eraseEverything();
});

add_task(function* checkUndoLoginsState() {
  MigrationUtils.initializeUndoData();
  MigrationUtils.insertLoginWrapper({
    username: "foo",
    password: "bar",
    hostname: "https://example.com",
    formSubmitURL: "https://example.com/",
    timeCreated: new Date(),
  });
  let storedLogins = Services.logins.findLogins({}, "https://example.com", "", "");
  let storedLogin = storedLogins[0];
  storedLogin.QueryInterface(Ci.nsILoginMetaInfo);
  let {guid, timePasswordChanged} = storedLogin;
  let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins");
  Assert.deepEqual([{guid, timePasswordChanged}], undoLoginData);
  Services.logins.removeAllLogins();
});

add_task(function* testLoginsRemovalByUndo() {
  MigrationUtils.initializeUndoData();
  MigrationUtils.insertLoginWrapper({
    username: "foo",
    password: "bar",
    hostname: "https://example.com",
    formSubmitURL: "https://example.com/",
    timeCreated: new Date(),
  });
  MigrationUtils.insertLoginWrapper({
    username: "foo",
    password: "bar",
    hostname: "https://example.org",
    formSubmitURL: "https://example.org/",
    timeCreated: new Date(new Date().getTime() - 10000),
  });
  // This should update the existing login
  LoginHelper.maybeImportLogin({
    username: "foo",
    password: "bazzy",
    hostname: "https://example.org",
    formSubmitURL: "https://example.org/",
    timePasswordChanged: new Date(),
  });
  Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length,
               "Should be only 1 login for example.org (that was updated)");
  let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins");

  yield AutoMigrate._removeUnchangedLogins(undoLoginData);
  Assert.equal(0, LoginHelper.searchLoginsWithObject({hostname: "https://example.com", formSubmitURL: "https://example.com/"}).length,
                   "unchanged example.com entry should have been removed.");
  Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length,
                   "changed example.org entry should have persisted.");
  Services.logins.removeAllLogins();
});

add_task(function* checkUndoVisitsState() {
  MigrationUtils.initializeUndoData();
  yield MigrationUtils.insertVisitsWrapper([{
    uri: NetUtil.newURI("http://www.example.com/"),
    title: "Example",
    visits: [{
      visitDate: new Date("2015-07-10").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }, {
      visitDate: new Date("2015-09-10").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }, {
      visitDate: new Date("2015-08-10").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }],
  }, {
    uri: NetUtil.newURI("http://www.example.org/"),
    title: "Example",
    visits: [{
      visitDate: new Date("2016-04-03").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }, {
      visitDate: new Date("2015-08-03").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }],
  }, {
    uri: NetUtil.newURI("http://www.example.com/"),
    title: "Example",
    visits: [{
      visitDate: new Date("2015-10-10").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }],
  }]);
  let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits");
  Assert.deepEqual(Array.from(undoVisitData.map(v => v.url)).sort(),
                   ["http://www.example.com/", "http://www.example.org/"]);
  Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.com/"), {
    url: "http://www.example.com/",
    visitCount: 4,
    first: new Date("2015-07-10").getTime() * 1000,
    last: new Date("2015-10-10").getTime() * 1000,
  });
  Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.org/"), {
    url: "http://www.example.org/",
    visitCount: 2,
    first: new Date("2015-08-03").getTime() * 1000,
    last: new Date("2016-04-03").getTime() * 1000,
  });

  yield PlacesTestUtils.clearHistory();
});

add_task(function* checkUndoVisitsState() {
  MigrationUtils.initializeUndoData();
  yield MigrationUtils.insertVisitsWrapper([{
    uri: NetUtil.newURI("http://www.example.com/"),
    title: "Example",
    visits: [{
      visitDate: new Date("2015-07-10").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }, {
      visitDate: new Date("2015-09-10").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }, {
      visitDate: new Date("2015-08-10").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }],
  }, {
    uri: NetUtil.newURI("http://www.example.org/"),
    title: "Example",
    visits: [{
      visitDate: new Date("2016-04-03").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }, {
      visitDate: new Date("2015-08-03").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }],
  }, {
    uri: NetUtil.newURI("http://www.example.com/"),
    title: "Example",
    visits: [{
      visitDate: new Date("2015-10-10").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }],
  }, {
    uri: NetUtil.newURI("http://www.mozilla.org/"),
    title: "Example",
    visits: [{
      visitDate: new Date("2015-01-01").getTime() * 1000,
      transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
    }],
  }]);

  // We have to wait until frecency updates have been handled in order
  // to accurately determine whether we're doing the right thing.
  let frecencyUpdatesHandled = new Promise(resolve => {
    PlacesUtils.history.addObserver({
      onFrecencyChanged(aURI) {
        if (aURI.spec == "http://www.unrelated.org/") {
          PlacesUtils.history.removeObserver(this);
          resolve();
        }
      }
    }, false);
  });
  yield PlacesUtils.history.insertMany([{
    url: "http://www.example.com/",
    title: "Example",
    visits: [{
      date: new Date("2015-08-16"),
    }],
  }, {
    url: "http://www.example.org/",
    title: "Example",
    visits: [{
      date: new Date("2016-01-03"),
    }, {
      date: new Date("2015-05-03"),
    }],
  }, {
    url: "http://www.unrelated.org/",
    title: "Unrelated",
    visits: [{
      date: new Date("2015-09-01"),
    }],
  }]);
  yield frecencyUpdatesHandled;
  let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits");

  let frecencyChangesExpected = new Map([
    ["http://www.example.com/", PromiseUtils.defer()],
    ["http://www.example.org/", PromiseUtils.defer()]
  ]);
  let uriDeletedExpected = new Map([
    ["http://www.mozilla.org/", PromiseUtils.defer()],
  ]);
  let wrongMethodDeferred = PromiseUtils.defer();
  let observer = {
    onBeginUpdateBatch: function() {},
    onEndUpdateBatch: function() {},
    onVisit: function(uri) {
      wrongMethodDeferred.reject(new Error("Unexpected call to onVisit " + uri.spec));
    },
    onTitleChanged: function(uri) {
      wrongMethodDeferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec));
    },
    onClearHistory: function() {
      wrongMethodDeferred.reject("Unexpected call to onClearHistory");
    },
    onPageChanged: function(uri) {
      wrongMethodDeferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec));
    },
    onFrecencyChanged: function(aURI) {
      do_print("frecency change");
      Assert.ok(frecencyChangesExpected.has(aURI.spec),
                "Should be expecting frecency change for " + aURI.spec);
      frecencyChangesExpected.get(aURI.spec).resolve();
    },
    onManyFrecenciesChanged: function() {
      do_print("Many frecencies changed");
      wrongMethodDeferred.reject(new Error("This test can't deal with onManyFrecenciesChanged to be called"));
    },
    onDeleteURI: function(aURI) {
      do_print("delete uri");
      Assert.ok(uriDeletedExpected.has(aURI.spec),
                "Should be expecting uri deletion for " + aURI.spec);
      uriDeletedExpected.get(aURI.spec).resolve();
    },
  };
  PlacesUtils.history.addObserver(observer, false);

  yield AutoMigrate._removeSomeVisits(undoVisitData);
  PlacesUtils.history.removeObserver(observer);
  yield Promise.all(uriDeletedExpected.values());
  yield Promise.all(frecencyChangesExpected.values());

  Assert.equal(yield visitsForURL("http://www.example.com/"), 1,
               "1 example.com visit (out of 5) should have persisted despite being within the range, due to limiting");
  Assert.equal(yield visitsForURL("http://www.mozilla.org/"), 0,
               "0 mozilla.org visits should have persisted (out of 1).");
  Assert.equal(yield visitsForURL("http://www.example.org/"), 2,
               "2 example.org visits should have persisted (out of 4).");
  Assert.equal(yield visitsForURL("http://www.unrelated.org/"), 1,
               "1 unrelated.org visits should have persisted as it's not involved in the import.");
  yield PlacesTestUtils.clearHistory();
});

add_task(function* checkHistoryRemovalCompletion() {
  AutoMigrate._errorMap = {bookmarks: 0, visits: 0, logins: 0};
  yield AutoMigrate._removeSomeVisits([{url: "http://www.example.com/", limit: -1}]);
  ok(true, "Removing visits should complete even if removing some visits failed.");
  Assert.equal(AutoMigrate._errorMap.visits, 1, "Should have logged the error for visits.");

  // Unfortunately there's not a reliable way to make removing bookmarks be
  // unhappy unless the DB is messed up (e.g. contains children but has
  // parents removed already).
  yield AutoMigrate._removeUnchangedBookmarks([
    {guid: PlacesUtils.bookmarks, lastModified: new Date(0), parentGuid: 0},
    {guid: "gobbledygook", lastModified: new Date(0), parentGuid: 0},
  ]);
  ok(true, "Removing bookmarks should complete even if some items are gone or bogus.");
  Assert.equal(AutoMigrate._errorMap.bookmarks, 0,
               "Should have ignored removing non-existing (or builtin) bookmark.");


  yield AutoMigrate._removeUnchangedLogins([
    {guid: "gobbledygook", timePasswordChanged: new Date(0)},
  ]);
  ok(true, "Removing logins should complete even if logins don't exist.");
  Assert.equal(AutoMigrate._errorMap.logins, 0,
               "Should have ignored removing non-existing logins.");
});
