/**
 * Test that logins backup is deleted as expected when logins are deleted.
 */

ChromeUtils.defineESModuleGetters(this, {
  FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.sys.mjs",
  FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.sys.mjs",
});

const nsLoginInfo = new Components.Constructor(
  "@mozilla.org/login-manager/loginInfo;1",
  Ci.nsILoginInfo,
  "init"
);

const login1 = new nsLoginInfo(
  "https://example.com",
  "https://example.com",
  null,
  "notifyu1",
  "notifyp1",
  "user",
  "pass"
);
const login2 = new nsLoginInfo(
  "https://example.com",
  "https://example.com",
  null,
  "",
  "notifyp1",
  "",
  "pass"
);

const fxaKey = new nsLoginInfo(
  FXA_PWDMGR_HOST,
  null,
  FXA_PWDMGR_REALM,
  "foo@bar.com",
  "pass2",
  "",
  ""
);

const loginStorePath = PathUtils.join(PathUtils.profileDir, "logins.json");
const loginBackupPath = PathUtils.join(
  PathUtils.profileDir,
  "logins-backup.json"
);

async function waitForBackupUpdate() {
  return new Promise(resolve => {
    Services.obs.addObserver(function observer(_subject, _topic, _data) {
      Services.obs.removeObserver(observer, "logins-backup-updated");
      resolve();
    }, "logins-backup-updated");
  });
}

async function loginStoreExists() {
  return TestUtils.waitForCondition(() => IOUtils.exists(loginStorePath));
}

async function loginBackupExists() {
  return TestUtils.waitForCondition(() => IOUtils.exists(loginBackupPath));
}

async function loginBackupDeleted() {
  return TestUtils.waitForCondition(
    async () => !(await IOUtils.exists(loginBackupPath))
  );
}

// If a fxa key is stored as a login, test that logins backup is updated to only store
// the fxa key when the last user facing login is deleted.
add_task(
  async function test_deleteLoginsBackup_removeAllUserFacingLogins_fxaKey() {
    info(
      "Testing removeAllUserFacingLogins() case when there is a saved fxa key"
    );
    info("Adding two logins: fxa key and one user facing login");
    let storageUpdatePromise = TestUtils.topicObserved(
      "password-storage-updated"
    );
    await Services.logins.addLoginAsync(login1);
    Assert.ok(true, "added login1");
    await loginStoreExists();
    await Services.logins.addLoginAsync(fxaKey);
    Assert.ok(true, "added fxaKey");
    await loginBackupExists();
    Assert.ok(true, "logins-backup.json now exists");
    await storageUpdatePromise;
    info("Writes to storage are complete for addLogin calls");

    storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
    info("Removing all user facing logins");
    await Services.logins.removeAllUserFacingLoginsAsync();
    await storageUpdatePromise;
    info("Writes to storage are complete after removeAllUserFacingLogins call");
    await waitForBackupUpdate();
    Assert.ok(
      true,
      "logins-backup.json was updated to only store the fxa key, as expected"
    );

    // Clean up.
    // Since there is a fxa key left, we need to call removeAllLogins() or removeLogin(fxaKey)
    // to remove the fxa key. Otherwise the test will fail in verify mode when trying to add login1
    await Services.logins.removeAllLoginsAsync();
    await IOUtils.remove(loginStorePath);
  }
);

// Test that logins backup is deleted when Services.logins.removeAllUserFacingLogins() is called.
add_task(async function test_deleteLoginsBackup_removeAllUserFacingLogins() {
  // Remove logins.json and logins-backup.json before starting.
  info("Testing the removeAllUserFacingLogins() case");

  await IOUtils.remove(loginStorePath, { ignoreAbsent: true });
  await IOUtils.remove(loginBackupPath, { ignoreAbsent: true });

  let storageUpdatePromise = TestUtils.topicObserved(
    "password-storage-updated"
  );
  info("Add a login to create logins.json");
  await Services.logins.addLoginAsync(login1);
  await loginStoreExists();
  Assert.ok(true, "logins.json now exists");

  info("Add a second login to create logins-backup.json");
  await Services.logins.addLoginAsync(login2);
  await loginBackupExists();
  info("logins-backup.json now exists");

  await storageUpdatePromise;
  info("Writes to storage are complete for addLogin calls");

  storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
  info("Removing all user facing logins");
  await Services.logins.removeAllUserFacingLoginsAsync();

  await storageUpdatePromise;
  info(
    "Writes to storage are complete when removeAllUserFacingLogins() is called"
  );
  await loginBackupDeleted();
  info(
    "logins-backup.json was deleted as expected when all logins were removed"
  );

  // Clean up.
  await IOUtils.remove(loginStorePath);
});

// 1. Test that logins backup is deleted when Services.logins.removeAllLogins() is called
// 2. If a FxA key is stored as a login, test that logins backup is deleted when
//   Services.logins.removeAllLogins() is called
add_task(async function test_deleteLoginsBackup_removeAllLogins() {
  // Remove logins.json and logins-backup.json before starting.
  info("Testing the removeAllLogins() case");

  await IOUtils.remove(loginStorePath, { ignoreAbsent: true });
  await IOUtils.remove(loginBackupPath, { ignoreAbsent: true });

  let storageUpdatePromise = TestUtils.topicObserved(
    "password-storage-updated"
  );
  info("Add a login to create logins.json");
  await Services.logins.addLoginAsync(login1);
  Assert.ok(true, "added login1");
  await loginStoreExists();
  Assert.ok(true, "logins.json now exists");
  await Services.logins.addLoginAsync(login2);
  Assert.ok(true, "added login2");
  await loginBackupExists();
  info("logins-backup.json now exists");

  await storageUpdatePromise;
  info("Writes to storage are complete for addLogin calls");

  storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
  info("Removing all logins");
  await Services.logins.removeAllLoginsAsync();

  await storageUpdatePromise;
  info("Writes to storage are complete when removeAllLogins() is called");

  await loginBackupDeleted();
  info(
    "logins-backup.json was deleted as expected when all logins were removed"
  );
  await IOUtils.remove(loginStorePath);

  info("Testing the removeAllLogins() case when FxA key is present");
  storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
  await Services.logins.addLoginAsync(login1);
  await loginStoreExists();
  await Services.logins.addLoginAsync(fxaKey);
  await loginBackupExists();
  info("logins-backup.json now exists");
  await storageUpdatePromise;
  info("Write to storage are complete for addLogin calls");

  storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
  info("Removing all logins, including FxA key");
  await Services.logins.removeAllLoginsAsync();
  await storageUpdatePromise;
  info("Writes to storage are complete after the last removeAllLogins call");
  await loginBackupDeleted();
  info(
    "logins-backup.json was deleted when the last logins were removed, as expected"
  );

  // Clean up.
  await IOUtils.remove(loginStorePath);
});

// 1. Test that logins backup is deleted when the last saved login is removed using
//    Services.logins.removeLogin() when no fxa key is saved.
// 2. Test that logins backup is updated when the last saved login is removed using
//    Services.logins.removeLogin() when a fxa key is present.
add_task(async function test_deleteLoginsBackup_removeLogin() {
  info("Testing the removeLogin() case when there is no saved fxa key");
  info("Adding two logins");
  let storageUpdatePromise = TestUtils.topicObserved(
    "password-storage-updated"
  );
  await Services.logins.addLoginAsync(login1);
  await loginStoreExists();
  await Services.logins.addLoginAsync(login2);
  await loginBackupExists();
  info("logins-backup.json now exists");

  await storageUpdatePromise;
  info("Writes to storage are complete for addLogin calls");

  storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
  info("Removing one login");
  await Services.logins.removeLoginAsync(login1);
  await storageUpdatePromise;
  info("Writes to storage are complete after one removeLogin call");
  await loginBackupExists();

  storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
  info("Removing the last login");
  await Services.logins.removeLoginAsync(login2);
  await storageUpdatePromise;
  info("Writes to storage are complete after the last removeLogin call");
  await loginBackupDeleted();
  info(
    "logins-backup.json was deleted as expected when the last saved login was removed"
  );
  await IOUtils.remove(loginStorePath);

  info("Testing the removeLogin() case when there is a saved fxa key");
  info("Adding two logins: one user facing and the fxa key");
  storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
  await Services.logins.addLoginAsync(login1);
  await loginStoreExists();
  await Services.logins.addLoginAsync(fxaKey);
  await loginBackupExists();
  info("logins-backup.json now exists");

  await storageUpdatePromise;
  info("Writes to storage are complete for addLogin calls");

  storageUpdatePromise = TestUtils.topicObserved("password-storage-updated");
  let backupUpdate = waitForBackupUpdate();
  await Services.logins.removeLoginAsync(login1);
  await storageUpdatePromise;
  info("Writes to storage are complete after one removeLogin call");
  await backupUpdate;

  await loginBackupExists();
  info("logins-backup.json was updated to contain only the fxa key");

  // Clean up.
  // Since there is a fxa key left, we need to call removeAllLogins() or removeLogin(fxaKey)
  // to remove the fxa key. Otherwise the test will fail in verify mode when trying to add login1
  await Services.logins.removeAllLoginsAsync();
  await IOUtils.remove(loginStorePath);
});
