/**
 * Test using the generated passwords produces the right doorhangers/notifications
 */

/* eslint no-shadow:"off" */

"use strict";

// The origin for the test URIs.
const TEST_ORIGIN = "https://example.com";
const FORM_PAGE_PATH =
  "/browser/toolkit/components/passwordmgr/test/browser/form_basic.html";
const passwordInputSelector = "#form-basic-password";
const usernameInputSelector = "#form-basic-username";

requestLongerTimeout(2);

async function task_setup() {
  await Services.logins.removeAllUserFacingLoginsAsync();
  LoginTestUtils.resetGeneratedPasswordsCache();
  await cleanupPasswordNotifications();
  await LoginTestUtils.remoteSettings.setupImprovedPasswordRules();
}

async function setup_withOneLogin(username = "username", password = "pass1") {
  // Reset to a single, known login
  await task_setup();
  let login = await LoginTestUtils.addLogin({ username, password });
  return login;
}

async function setup_withNoLogins() {
  // Reset to a single, known login
  await task_setup();
  Assert.equal(
    (await Services.logins.getAllLogins()).length,
    0,
    "0 logins at the start of the test"
  );
}

async function fillGeneratedPasswordFromACPopup(
  browser,
  passwordInputSelector
) {
  let popup = document.getElementById("PopupAutoComplete");
  Assert.ok(popup, "Got popup");
  await openACPopup(popup, browser, passwordInputSelector);
  await fillGeneratedPasswordFromOpenACPopup(browser, passwordInputSelector);
}

async function checkPromptContents(
  anchorElement,
  browser,
  expectedPasswordLength = 0
) {
  let { panel } = PopupNotifications;
  Assert.ok(PopupNotifications.isPanelOpen, "Confirm popup is open");
  let notificationElement = panel.childNodes[0];
  if (expectedPasswordLength) {
    info(
      `Waiting for password value to be ${expectedPasswordLength} chars long`
    );
    await TestUtils.waitForCondition(() => {
      return (
        notificationElement.querySelector("#password-notification-password")
          .value.length == expectedPasswordLength
      );
    }, "Wait for nsLoginManagerPrompter writeDataToUI()");
  }

  return {
    passwordValue: notificationElement.querySelector(
      "#password-notification-password"
    ).value,
    usernameValue: notificationElement.querySelector(
      "#password-notification-username"
    ).value,
  };
}

async function verifyGeneratedPasswordWasFilled(
  browser,
  passwordInputSelector
) {
  await SpecialPowers.spawn(
    browser,
    [[passwordInputSelector]],
    function checkFinalFieldValue(inputSelector) {
      let { LoginTestUtils: LTU } = ChromeUtils.importESModule(
        "resource://testing-common/LoginTestUtils.sys.mjs"
      );
      let passwordInput = content.document.querySelector(inputSelector);
      Assert.equal(
        passwordInput.value.length,
        LTU.generation.LENGTH,
        "Password field was filled with generated password"
      );
    }
  );
}

async function openFormInNewTab(url, formValues, taskFn) {
  let formFilled = listenForTestNotification("FormProcessed");

  await BrowserTestUtils.withNewTab(
    {
      gBrowser,
      url,
    },
    async function (browser) {
      await SimpleTest.promiseFocus(browser.documentGlobal);
      await formFilled;

      await SpecialPowers.spawn(
        browser,
        [formValues],
        async function prepareAndCheckForm({
          password: passwordProps,
          username: usernameProps,
        }) {
          let doc = content.document;
          // give the form an action so we can know when submit is complete
          doc.querySelector("form").action = "/";

          let props = passwordProps;
          if (props) {
            // We'll reuse the form_basic.html, but ensure we'll get the generated password autocomplete option
            let field = doc.querySelector(props.selector);
            if (props.type) {
              // Change the type from 'password' to something else.
              field.type = props.type;
            }

            field.setAttribute("autocomplete", "new-password");
            if (props.hasOwnProperty("expectedValue")) {
              Assert.equal(
                field.value,
                props.expectedValue,
                "Check autofilled password value"
              );
            }
          }
          props = usernameProps;
          if (props) {
            let field = doc.querySelector(props.selector);
            if (props.hasOwnProperty("expectedValue")) {
              Assert.equal(
                field.value,
                props.expectedValue,
                "Check autofilled username value"
              );
            }
          }
        }
      );

      if (formValues.password && formValues.password.setValue !== undefined) {
        info(
          "Editing the password, expectedMessage? " +
            formValues.password.expectedMessage
        );
        let messagePromise = formValues.password.expectedMessage
          ? listenForTestNotification(formValues.password.expectedMessage)
          : Promise.resolve();
        await changeContentInputValue(
          browser,
          formValues.password.selector,
          formValues.password.setValue
        );
        await messagePromise;
        info("messagePromise resolved");
      }

      if (formValues.username && formValues.username.setValue !== undefined) {
        info(
          "Editing the username, expectedMessage? " +
            formValues.username.expectedMessage
        );
        let messagePromise = formValues.username.expectedMessage
          ? listenForTestNotification(formValues.username.expectedMessage)
          : Promise.resolve();
        await changeContentInputValue(
          browser,
          formValues.username.selector,
          formValues.username.setValue
        );
        await messagePromise;
        info("messagePromise resolved");
      }

      await taskFn(browser);
      await closePopup(
        browser.ownerDocument.getElementById("confirmation-hint")
      );
    }
  );
}

async function openAndVerifyDoorhanger(browser, type, expected) {
  // The doorhanger's dismissed state is finalized asynchronously (it depends on
  // async storage lookups), so wait for it to reach the expected state before
  // asserting to avoid racing a still-transitioning notification.
  await TestUtils.waitForCondition(
    () => {
      let n = PopupNotifications.getNotification("password", null);
      return (
        n &&
        n.options.passwordNotificationType == type &&
        n.dismissed == expected.dismissed
      );
    },
    "Waiting for the doorhanger to reach the expected dismissed state",
    100,
    100
  );
  // check a dismissed prompt was shown with extraAttr attribute
  let notif = getCaptureDoorhanger(type);
  Assert.ok(notif, `${type} doorhanger was created`);
  Assert.equal(
    notif.dismissed,
    expected.dismissed,
    "Check notification dismissed property"
  );
  Assert.equal(
    notif.anchorElement.getAttribute("extraAttr"),
    expected.anchorExtraAttr,
    "Check icon extraAttr attribute"
  );
  let { panel } = PopupNotifications;
  // if the doorhanged is dimissed, we will open it to check panel contents
  if (panel.state !== "open") {
    let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
    if (panel.state !== "showing") {
      // synthesize click on anchor as this also blurs the form field triggering
      // a change event
      EventUtils.synthesizeMouseAtCenter(notif.anchorElement, {});
    }
    await promiseShown;
  }
  let { passwordValue, usernameValue } = await checkPromptContents(
    notif.anchorElement,
    browser,
    expected.passwordLength
  );
  Assert.equal(
    passwordValue.length,
    expected.passwordLength || LoginTestUtils.generation.LENGTH,
    "Doorhanger password field has generated 15-char value"
  );
  Assert.equal(
    usernameValue,
    expected.usernameValue,
    "Doorhanger username field was popuplated"
  );
  return notif;
}

async function appendContentInputvalue(browser, selector, str) {
  await SpecialPowers.spawn(
    browser,
    [{ selector, str }],
    async function ({ selector, str }) {
      const EventUtils = ContentTaskUtils.getEventUtils(content);
      let input = content.document.querySelector(selector);
      input.focus();
      input.select();
      await EventUtils.synthesizeKey("KEY_ArrowRight", {}, content);
      let changedPromise = ContentTaskUtils.waitForEvent(input, "change");
      if (str) {
        await EventUtils.sendString(str, content);
      }
      input.blur();
      await changedPromise;
    }
  );
  info("Input value changed");
  await TestUtils.waitForTick();
}

async function submitForm(browser) {
  // Submit the form
  info("Now submit the form");
  let correctPathNamePromise = BrowserTestUtils.browserLoaded(browser);
  await SpecialPowers.spawn(browser, [], async function () {
    content.document.querySelector("form").submit();
  });
  await correctPathNamePromise;
  await SpecialPowers.spawn(browser, [], async () => {
    let win = content;
    await ContentTaskUtils.waitForCondition(() => {
      return (
        win.location.pathname == "/" && win.document.readyState == "complete"
      );
    }, "Wait for form submission load");
  });
}

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["test.wait300msAfterTabSwitch", true],
      ["signon.generation.available", true],
      ["signon.generation.enabled", true],
    ],
  });
  // assert that there are no logins
  let logins = await Services.logins.getAllLogins();
  Assert.equal(logins.length, 0, "There are no logins");
});

add_task(async function autocomplete_generated_password_auto_saved() {
  // confirm behavior when filling a generated password via autocomplete
  // when there are no other logins
  await setup_withNoLogins();
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {
      password: { selector: passwordInputSelector, expectedValue: "" },
      username: { selector: usernameInputSelector, expectedValue: "" },
    },
    async function taskFn(browser) {
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "addLogin"
      );
      // Let the hint hide itself this first time
      let forceClosePopup = false;
      let hintShownAndVerified = verifyConfirmationHint(
        browser,
        forceClosePopup
      );

      await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
      let [{ username, password }] = await storageChangedPromise;
      await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);

      // Make sure confirmation hint was shown
      info("waiting for verifyConfirmationHint");
      await hintShownAndVerified;

      // Check properties of the newly auto-saved login
      Assert.equal(username, "", "Saved login should have no username");
      Assert.equal(
        password.length,
        LoginTestUtils.generation.LENGTH,
        "Saved login should have generated password"
      );

      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: "attention",
        usernameValue: "",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });

      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
      await promiseHidden;

      // confirm the extraAttr attribute is removed after opening & dismissing the doorhanger
      Assert.ok(
        !notif.anchorElement.hasAttribute("extraAttr"),
        "Check if the extraAttr attribute was removed"
      );
      await cleanupDoorhanger(notif);

      storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "modifyLogin"
      );
      let [autoSavedLogin] = await Services.logins.getAllLogins();
      info("waiting for submitForm");
      await submitForm(browser);
      await storageChangedPromise;
      await verifyLogins([
        {
          timesUsed: autoSavedLogin.timesUsed + 1,
          username: "",
        },
      ]);
    }
  );
});

add_task(
  async function autocomplete_generated_password_with_confirm_field_auto_saved() {
    // confirm behavior when filling a generated password via autocomplete
    // when there are no other logins and the form has a confirm password field
    const FORM_WITH_CONFIRM_FIELD_PAGE_PATH =
      "/browser/toolkit/components/passwordmgr/test/browser/form_basic_with_confirm_field.html";
    const confirmPasswordInputSelector = "#form-basic-confirm-password";
    await setup_withNoLogins();
    await openFormInNewTab(
      TEST_ORIGIN + FORM_WITH_CONFIRM_FIELD_PAGE_PATH,
      {
        password: { selector: passwordInputSelector, expectedValue: "" },
        username: { selector: usernameInputSelector, expectedValue: "" },
      },
      async function taskFn(browser) {
        let storageChangedPromise = TestUtils.topicObserved(
          "passwordmgr-storage-changed",
          (_, data) => data == "addLogin"
        );
        // Let the hint hide itself this first time
        let forceClosePopup = false;
        let hintShownAndVerified = verifyConfirmationHint(
          browser,
          forceClosePopup
        );

        await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
        let [{ username, password }] = await storageChangedPromise;
        await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);
        await verifyGeneratedPasswordWasFilled(
          browser,
          confirmPasswordInputSelector
        );

        // Make sure confirmation hint was shown
        info("waiting for verifyConfirmationHint");
        await hintShownAndVerified;

        // Check properties of the newly auto-saved login
        Assert.equal(username, "", "Saved login should have no username");
        Assert.equal(
          password.length,
          LoginTestUtils.generation.LENGTH,
          "Saved login should have generated password"
        );

        let notif = await openAndVerifyDoorhanger(browser, "password-change", {
          dismissed: true,
          anchorExtraAttr: "attention",
          usernameValue: "",
          passwordLength: LoginTestUtils.generation.LENGTH,
        });

        let promiseHidden = BrowserTestUtils.waitForEvent(
          PopupNotifications.panel,
          "popuphidden"
        );
        clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
        await promiseHidden;

        // confirm the extraAttr attribute is removed after opening & dismissing the doorhanger
        Assert.ok(
          !notif.anchorElement.hasAttribute("extraAttr"),
          "Check if the extraAttr attribute was removed"
        );
        await cleanupDoorhanger(notif);

        storageChangedPromise = TestUtils.topicObserved(
          "passwordmgr-storage-changed",
          (_, data) => data == "modifyLogin"
        );
        let [autoSavedLogin] = await Services.logins.getAllLogins();
        info("waiting for submitForm");
        await submitForm(browser);
        await storageChangedPromise;
        await verifyLogins([
          {
            timesUsed: autoSavedLogin.timesUsed + 1,
            username: "",
          },
        ]);
      }
    );
  }
);

add_task(async function autocomplete_generated_password_saved_empty_username() {
  // confirm behavior when filling a generated password via autocomplete
  // when there is an existing saved login with a "" username
  await setup_withOneLogin("", "xyzpassword");
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {
      password: {
        selector: passwordInputSelector,
        expectedValue: "xyzpassword",
        setValue: "",
        expectedMessage: "PasswordEditedOrGenerated",
      },
      username: { selector: usernameInputSelector, expectedValue: "" },
    },
    async function taskFn(browser) {
      let [savedLogin] = await Services.logins.getAllLogins();
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "modifyLogin"
      );
      await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
      await waitForDoorhanger(browser, "password-change");
      info("Waiting to openAndVerifyDoorhanger");
      await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: null,
        usernameValue: "",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });
      await hideDoorhangerPopup();
      info("Waiting to verifyGeneratedPasswordWasFilled");
      await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);

      info("waiting for submitForm");
      await submitForm(browser);
      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: false,
        anchorExtraAttr: null,
        usernameValue: "",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });

      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, CHANGE_BUTTON);
      await promiseHidden;

      info("Waiting for modifyLogin");
      await storageChangedPromise;
      await verifyLogins([
        {
          timesUsed: savedLogin.timesUsed + 1,
          username: "",
        },
      ]);
      await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
    }
  );
});

add_task(async function autocomplete_generated_password_saved_username() {
  // confirm behavior when filling a generated password via autocomplete
  // into a form with username matching an existing saved login
  await setup_withOneLogin("user1", "xyzpassword");
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {
      password: {
        selector: passwordInputSelector,
        expectedValue: "xyzpassword",
        setValue: "",
        expectedMessage: "PasswordEditedOrGenerated",
      },
      username: {
        selector: usernameInputSelector,
        expectedValue: "user1",
      },
    },
    async function taskFn(browser) {
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "addLogin"
      );
      // We don't need to wait to confirm the hint hides itelf every time
      let forceClosePopup = true;
      let hintShownAndVerified = verifyConfirmationHint(
        browser,
        forceClosePopup
      );

      await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);

      // Make sure confirmation hint was shown
      info("waiting for verifyConfirmationHint");
      await hintShownAndVerified;

      info("waiting for addLogin");
      await storageChangedPromise;
      await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);

      // Check properties of the newly auto-saved login
      let [user1LoginSnapshot, autoSavedLogin] = await verifyLogins([
        {
          username: "user1",
          password: "xyzpassword", // user1 is unchanged
        },
        {
          timesUsed: 1,
          username: "",
          passwordLength: LoginTestUtils.generation.LENGTH,
        },
      ]);

      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: "attention",
        usernameValue: "user1",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });

      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
      await promiseHidden;

      // confirm the extraAttr attribute is removed after opening & dismissing the doorhanger
      Assert.ok(
        !notif.anchorElement.hasAttribute("extraAttr"),
        "Check if the extraAttr attribute was removed"
      );
      await cleanupDoorhanger(notif);

      storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "modifyLogin"
      );
      // Confirming the change both updates user1 and removes the auto-saved
      // login, so wait for the removal too before checking the final state.
      let removeLoginPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "removeLogin"
      );
      info("waiting for submitForm");
      await submitForm(browser);
      promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, CHANGE_BUTTON);
      await promiseHidden;
      await storageChangedPromise;
      await removeLoginPromise;
      await verifyLogins([
        {
          timesUsed: user1LoginSnapshot.timesUsed + 1,
          username: "user1",
          password: autoSavedLogin.password,
        },
      ]);
    }
  );
});

add_task(async function ac_gen_pw_saved_empty_un_stored_non_empty_un_in_form() {
  // confirm behavior when when the form's username field has a non-empty value
  // and there is an existing saved login with a "" username
  await setup_withOneLogin("", "xyzpassword");
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {
      password: {
        selector: passwordInputSelector,
        expectedValue: "xyzpassword",
        setValue: "",
        expectedMessage: "PasswordEditedOrGenerated",
      },
      username: {
        selector: usernameInputSelector,
        expectedValue: "",
        setValue: "myusername",
        // with an empty password value, no message is sent for a username change
        expectedMessage: "",
      },
    },
    async function taskFn(browser) {
      let [savedLogin] = await Services.logins.getAllLogins();
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "addLogin"
      );
      await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
      await waitForDoorhanger(browser, "password-save");
      info("Waiting to openAndVerifyDoorhanger");
      await openAndVerifyDoorhanger(browser, "password-save", {
        dismissed: true,
        anchorExtraAttr: null,
        usernameValue: "myusername",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });
      await hideDoorhangerPopup();
      info("Waiting to verifyGeneratedPasswordWasFilled");
      await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);

      info("waiting for submitForm");
      await submitForm(browser);
      let notif = await openAndVerifyDoorhanger(browser, "password-save", {
        dismissed: false,
        anchorExtraAttr: null,
        usernameValue: "myusername",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });

      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, REMEMBER_BUTTON);
      await promiseHidden;

      info("Waiting for addLogin");
      await storageChangedPromise;
      await verifyLogins([
        {
          timesUsed: savedLogin.timesUsed,
          username: "",
          password: "xyzpassword",
        },
        {
          timesUsed: 1,
          username: "myusername",
        },
      ]);
      await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
    }
  );
});

add_task(async function contextfill_generated_password_saved_empty_username() {
  // confirm behavior when filling a generated password via context menu
  // when there is an existing saved login with a "" username
  await setup_withOneLogin("", "xyzpassword");
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {
      password: {
        selector: passwordInputSelector,
        expectedValue: "xyzpassword",
        setValue: "",
        expectedMessage: "PasswordEditedOrGenerated",
      },
      username: { selector: usernameInputSelector, expectedValue: "" },
    },
    async function taskFn(browser) {
      let [savedLogin] = await Services.logins.getAllLogins();
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "modifyLogin"
      );
      await doFillGeneratedPasswordContextMenuItem(
        browser,
        passwordInputSelector
      );
      await waitForDoorhanger(browser, "password-change");
      info("Waiting to openAndVerifyDoorhanger");
      await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: null,
        usernameValue: "",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });
      await hideDoorhangerPopup();
      info("Waiting to verifyGeneratedPasswordWasFilled");
      await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);

      info("waiting for submitForm");
      await submitForm(browser);
      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: false,
        anchorExtraAttr: null,
        usernameValue: "",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });

      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, CHANGE_BUTTON);
      await promiseHidden;

      info("Waiting for modifyLogin");
      await storageChangedPromise;
      await verifyLogins([
        {
          timesUsed: savedLogin.timesUsed + 1,
          username: "",
        },
      ]);
      await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
    }
  );
});

async function autocomplete_generated_password_edited_no_auto_save(
  passwordType = "password"
) {
  // confirm behavior when filling a generated password via autocomplete
  // when there is an existing saved login with a "" username and then editing
  // the password and autocompleting again.
  await setup_withOneLogin("", "xyzpassword");
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {
      password: {
        selector: passwordInputSelector,
        expectedValue: "xyzpassword",
        setValue: "",
        type: passwordType,
        expectedMessage: "PasswordEditedOrGenerated",
      },
      username: { selector: usernameInputSelector, expectedValue: "" },
    },
    async function taskFn(browser) {
      let [savedLogin] = await Services.logins.getAllLogins();
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "modifyLogin"
      );
      await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);
      info(
        "Filled generated password, waiting for dismissed password-change doorhanger"
      );
      await waitForDoorhanger(browser, "password-change");
      info("Waiting to openAndVerifyDoorhanger");
      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: null,
        usernameValue: "",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });

      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
      await promiseHidden;

      info("Waiting to verifyGeneratedPasswordWasFilled");
      await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);

      await BrowserTestUtils.sendChar("!", browser);
      await BrowserTestUtils.sendChar("@", browser);
      await BrowserTestUtils.synthesizeKey("KEY_Tab", undefined, browser);

      await waitForDoorhanger(browser, "password-change");
      info("Waiting to openAndVerifyDoorhanger");
      notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: null,
        usernameValue: "",
        passwordLength: LoginTestUtils.generation.LENGTH + 2,
      });

      promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
      await promiseHidden;

      await verifyLogins([
        {
          timesUsed: savedLogin.timesUsed,
          username: "",
          password: "xyzpassword",
        },
      ]);

      info("waiting for submitForm");
      await submitForm(browser);
      notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: false,
        anchorExtraAttr: null,
        usernameValue: "",
        passwordLength: LoginTestUtils.generation.LENGTH + 2,
      });

      promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, CHANGE_BUTTON);
      await promiseHidden;

      info("Waiting for modifyLogin");
      await storageChangedPromise;
      await verifyLogins([
        {
          timesUsed: savedLogin.timesUsed + 1,
          username: "",
        },
      ]);
      await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
    }
  );

  LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear();
}

add_task(autocomplete_generated_password_edited_no_auto_save);

add_task(
  async function autocomplete_generated_password_edited_no_auto_save_type_text() {
    await autocomplete_generated_password_edited_no_auto_save("text");
  }
);

add_task(async function contextmenu_fill_generated_password_and_set_username() {
  // test when filling with a generated password and editing the username in the form
  // * the prompt should display the form's username
  // * the auto-saved login should have "" for username
  // * confirming the prompt should edit the "" login and add the username
  await setup_withOneLogin("olduser", "xyzpassword");
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {
      password: {
        selector: passwordInputSelector,
        expectedValue: "xyzpassword",
        setValue: "",
        expectedMessage: "PasswordEditedOrGenerated",
      },
      username: {
        selector: usernameInputSelector,
        expectedValue: "olduser",
        setValue: "differentuser",
        // with an empty password value, no message is sent for a username change
        expectedMessage: "",
      },
    },
    async function taskFn(browser) {
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "addLogin"
      );
      await SpecialPowers.spawn(
        browser,
        [passwordInputSelector],
        function checkEmptyPasswordField(passwordSelector) {
          Assert.equal(
            content.document.querySelector(passwordSelector).value,
            "",
            "Password field is empty"
          );
        }
      );

      // Let the hint hide itself this first time
      let forceClosePopup = false;
      let hintShownAndVerified = verifyConfirmationHint(
        browser,
        forceClosePopup
      );

      info("waiting to fill generated password using context menu");
      await doFillGeneratedPasswordContextMenuItem(
        browser,
        passwordInputSelector
      );

      info("waiting for verifyConfirmationHint");
      await hintShownAndVerified;
      info("waiting for dismissed password-change notification");
      await waitForDoorhanger(browser, "password-change");

      info("waiting for addLogin");
      await storageChangedPromise;

      // Check properties of the newly auto-saved login
      await verifyLogins([
        null, // ignore the first one
        {
          timesUsed: 1,
          username: "",
          passwordLength: LoginTestUtils.generation.LENGTH,
        },
      ]);

      info("Waiting to openAndVerifyDoorhanger");
      await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: "attention",
        usernameValue: "differentuser",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });
      await hideDoorhangerPopup();
      info("Waiting to verifyGeneratedPasswordWasFilled");
      await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);

      info("waiting for submitForm");
      await submitForm(browser);
      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: false,
        anchorExtraAttr: null,
        usernameValue: "differentuser",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });

      storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "modifyLogin"
      );

      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      clickDoorhangerButton(notif, CHANGE_BUTTON);
      await promiseHidden;

      info("Waiting for modifyLogin");
      await storageChangedPromise;
      await verifyLogins([
        null,
        {
          username: "differentuser",
          passwordLength: LoginTestUtils.generation.LENGTH,
          timesUsed: 2,
        },
      ]);
      await cleanupDoorhanger(notif); // cleanup the doorhanger for next test
    }
  );
});

add_task(async function contextmenu_password_change_form_without_username() {
  // test doorhanger behavior when a generated password is filled into a change-password
  // form with no username
  await setup_withOneLogin("user1", "xyzpassword");
  await LoginTestUtils.addLogin({ username: "username2", password: "pass2" });
  const passwordInputSelector = "#newpass";

  const CHANGE_FORM_PATH =
    "/browser/toolkit/components/passwordmgr/test/browser/form_password_change.html";
  await openFormInNewTab(
    TEST_ORIGIN + CHANGE_FORM_PATH,
    {
      password: {
        selector: passwordInputSelector,
        expectedValue: "",
      },
    },
    async function taskFn(browser) {
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "addLogin"
      );

      // We don't need to wait to confirm the hint hides itelf every time
      let forceClosePopup = true;
      let hintShownAndVerified = verifyConfirmationHint(
        browser,
        forceClosePopup
      );

      // Make the 2nd field use a generated password
      info("Using contextmenu to fill with a generated password");
      await doFillGeneratedPasswordContextMenuItem(
        browser,
        passwordInputSelector
      );

      info("waiting for dismissed password-change notification");
      await waitForDoorhanger(browser, "password-change");

      // Make sure confirmation hint was shown
      info("waiting for verifyConfirmationHint");
      await hintShownAndVerified;

      info("waiting for addLogin");
      await storageChangedPromise;
      // Check properties of the newly auto-saved login
      await verifyLogins([
        null, // ignore the first one
        null, // ignore the 2nd one
        {
          timesUsed: 1,
          username: "",
          passwordLength: LoginTestUtils.generation.LENGTH,
        },
      ]);

      info("Waiting to openAndVerifyDoorhanger");
      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: "attention",
        usernameValue: "",
        passwordLength: LoginTestUtils.generation.LENGTH,
      });
      // remove notification so we can unambiguously check no new notification gets created later
      await cleanupDoorhanger(notif);

      info("Waiting to verifyGeneratedPasswordWasFilled");
      await verifyGeneratedPasswordWasFilled(browser, passwordInputSelector);

      storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "modifyLogin"
      );
      let { timeLastUsed } = (await Services.logins.getAllLogins())[2];

      info("waiting for submitForm");
      await submitForm(browser);

      info("Waiting for modifyLogin");
      await storageChangedPromise;
      await verifyLogins([
        null, // ignore the first one
        null, // ignore the 2nd one
        {
          timesUsed: 2,
          usedSince: timeLastUsed,
        },
      ]);
      // Check no new doorhanger was shown
      notif = getCaptureDoorhanger("password-change");
      Assert.ok(!notif, "No new doorhanger should be shown");
      await cleanupDoorhanger(); // cleanup for next test
    }
  );
});

add_task(
  async function autosaved_login_updated_to_existing_login_via_doorhanger() {
    // test when filling with a generated password and editing the username in the
    // doorhanger to match an existing login:
    // * the matching login should be updated
    // * the auto-saved login should be deleted
    // * the metadata for the matching login should be updated
    // * the by-origin cache for the password should point at the updated login
    await setup_withOneLogin("user1", "xyzpassword");
    await LoginTestUtils.addLogin({
      username: "user2",
      password: "abcpassword",
    });
    await openFormInNewTab(
      TEST_ORIGIN + FORM_PAGE_PATH,
      {
        password: {
          selector: passwordInputSelector,
          expectedValue: "",
        },
        username: {
          selector: usernameInputSelector,
          expectedValue: "",
        },
      },
      async function taskFn(browser) {
        await SimpleTest.promiseFocus(browser.documentGlobal);

        let storageChangedPromise = TestUtils.topicObserved(
          "passwordmgr-storage-changed",
          (_, data) => data == "addLogin"
        );
        // We don't need to wait to confirm the hint hides itelf every time
        let forceClosePopup = true;
        let hintShownAndVerified = verifyConfirmationHint(
          browser,
          forceClosePopup
        );

        info("waiting to fill generated password using context menu");
        await doFillGeneratedPasswordContextMenuItem(
          browser,
          passwordInputSelector
        );

        info("waiting for dismissed password-change notification");
        await waitForDoorhanger(browser, "password-change");

        // Make sure confirmation hint was shown
        info("waiting for verifyConfirmationHint");
        await hintShownAndVerified;

        info("waiting for addLogin");
        await storageChangedPromise;
        info("addLogin promise resolved");
        // Check properties of the newly auto-saved login
        let [user1LoginSnapshot, unused, autoSavedLogin] = await verifyLogins([
          null, // ignore the first one
          null, // ignore the 2nd one
          {
            timesUsed: 1,
            username: "",
            passwordLength: LoginTestUtils.generation.LENGTH,
          },
        ]);
        info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid);
        info("unused, guid: " + unused.guid);
        info("autoSavedLogin, guid: " + autoSavedLogin.guid);

        info("verifyLogins ok");
        let passwordCacheEntry =
          LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
            "https://example.com"
          );

        Assert.ok(
          passwordCacheEntry,
          "Got the cached generated password entry for https://example.com"
        );
        Assert.equal(
          passwordCacheEntry.value,
          autoSavedLogin.password,
          "Cached password matches the auto-saved login password"
        );
        Assert.equal(
          passwordCacheEntry.storageGUID,
          autoSavedLogin.guid,
          "Cached password guid matches the auto-saved login guid"
        );

        info("Waiting to openAndVerifyDoorhanger");
        let notif = await openAndVerifyDoorhanger(browser, "password-change", {
          dismissed: true,
          anchorExtraAttr: "attention",
          usernameValue: "",
          password: autoSavedLogin.password,
        });
        Assert.ok(notif, "Got password-change notification");

        info("Calling updateDoorhangerInputValues");
        await updateDoorhangerInputValues({
          username: "user1",
        });
        info("doorhanger inputs updated");

        let loginModifiedPromise = TestUtils.topicObserved(
          "passwordmgr-storage-changed",
          (subject, data) => {
            if (data == "modifyLogin") {
              info("passwordmgr-storage-changed, action: " + data);
              info("subject: " + JSON.stringify(subject));
              return true;
            }
            return false;
          }
        );
        let loginRemovedPromise = TestUtils.topicObserved(
          "passwordmgr-storage-changed",
          (subject, data) => {
            if (data == "removeLogin") {
              info("passwordmgr-storage-changed, action: " + data);
              info("subject: " + JSON.stringify(subject));
              return true;
            }
            return false;
          }
        );

        let promiseHidden = BrowserTestUtils.waitForEvent(
          PopupNotifications.panel,
          "popuphidden"
        );
        info("clicking change button");
        clickDoorhangerButton(notif, CHANGE_BUTTON);
        await promiseHidden;

        info("Waiting for modifyLogin promise");
        await loginModifiedPromise;

        info("Waiting for removeLogin promise");
        await loginRemovedPromise;

        info("storage-change promises resolved");
        // Check the auto-saved login was removed and the original login updated
        await verifyLogins([
          {
            username: "user1",
            password: autoSavedLogin.password,
            timeCreated: user1LoginSnapshot.timeCreated,
            timeLastUsed: user1LoginSnapshot.timeLastUsed,
            passwordChangedSince: autoSavedLogin.timePasswordChanged,
          },
          null, // ignore user2
        ]);

        // Check we have no notifications at this point
        Assert.ok(!PopupNotifications.isPanelOpen, "No doorhanger is open");
        Assert.ok(
          !PopupNotifications.getNotification("password", browser),
          "No notifications"
        );

        // make sure the cache entry is unchanged with the removal of the auto-saved login
        Assert.equal(
          autoSavedLogin.password,
          LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
            "https://example.com"
          ).value,
          "Generated password cache entry has the expected password value"
        );
      }
    );
  }
);

add_task(async function autosaved_login_updated_to_existing_login_onsubmit() {
  // test when selecting auto-saved generated password in a form filled with an
  // existing login and submitting the form:
  // * the matching login should be updated
  // * the auto-saved login should be deleted
  // * the metadata for the matching login should be updated
  // * the by-origin cache for the password should point at the updated login

  // clear both fields which should be autofilled with our single login
  await setup_withOneLogin("user1", "xyzpassword");
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {
      password: {
        selector: passwordInputSelector,
        expectedValue: "xyzpassword",
        setValue: "",
        expectedMessage: "PasswordEditedOrGenerated",
      },
      username: {
        selector: usernameInputSelector,
        expectedValue: "user1",
        setValue: "",
        // with an empty password value, no message is sent for a username change
        expectedMessage: "",
      },
    },
    async function taskFn(browser) {
      await SimpleTest.promiseFocus(browser.documentGlobal);

      // first, create an auto-saved login with generated password
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "addLogin"
      );
      // We don't need to wait to confirm the hint hides itelf every time
      let forceClosePopup = true;
      let hintShownAndVerified = verifyConfirmationHint(
        browser,
        forceClosePopup
      );

      info("waiting to fill generated password using context menu");
      await doFillGeneratedPasswordContextMenuItem(
        browser,
        passwordInputSelector
      );

      info("waiting for dismissed password-change notification");
      await waitForDoorhanger(browser, "password-change");

      // Make sure confirmation hint was shown
      info("waiting for verifyConfirmationHint");
      await hintShownAndVerified;

      info("waiting for addLogin");
      await storageChangedPromise;
      info("addLogin promise resolved");
      // Check properties of the newly auto-saved login
      let [user1LoginSnapshot, autoSavedLogin] = await verifyLogins([
        null, // ignore the first one
        {
          timesUsed: 1,
          username: "",
          passwordLength: LoginTestUtils.generation.LENGTH,
        },
      ]);
      info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid);
      info("autoSavedLogin, guid: " + autoSavedLogin.guid);

      info("verifyLogins ok");
      let passwordCacheEntry =
        LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
          "https://example.com"
        );

      Assert.ok(
        passwordCacheEntry,
        "Got the cached generated password entry for https://example.com"
      );
      Assert.equal(
        passwordCacheEntry.value,
        autoSavedLogin.password,
        "Cached password matches the auto-saved login password"
      );
      Assert.equal(
        passwordCacheEntry.storageGUID,
        autoSavedLogin.guid,
        "Cached password guid matches the auto-saved login guid"
      );

      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: "attention",
        usernameValue: "",
        password: autoSavedLogin.password,
      });
      await cleanupDoorhanger(notif);

      // now update and submit the form with the user1 username and the generated password
      info(`submitting form`);
      let submitResults = await submitFormAndGetResults(
        browser,
        "formsubmit.sjs",
        {
          "#form-basic-username": "user1",
        }
      );
      Assert.equal(
        submitResults.username,
        "user1",
        "Form submitted with expected username"
      );
      Assert.equal(
        submitResults.password,
        autoSavedLogin.password,
        "Form submitted with expected password"
      );
      info(
        `form was submitted, got username/password ${submitResults.username}/${submitResults.password}`
      );

      await waitForDoorhanger(browser, "password-change");
      notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: false,
        anchorExtraAttr: null,
        usernameValue: "user1",
        password: autoSavedLogin.password,
      });

      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      let loginModifiedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => {
          if (data == "modifyLogin") {
            info("passwordmgr-storage-changed, action: " + data);
            info("subject: " + JSON.stringify(_));
            return true;
          }
          return false;
        }
      );
      let loginRemovedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => {
          if (data == "removeLogin") {
            info("passwordmgr-storage-changed, action: " + data);
            info("subject: " + JSON.stringify(_));
            return true;
          }
          return false;
        }
      );

      info("clicking change button");
      clickDoorhangerButton(notif, CHANGE_BUTTON);
      await promiseHidden;

      info("Waiting for modifyLogin promise");
      await loginModifiedPromise;

      info("Waiting for removeLogin promise");
      await loginRemovedPromise;

      info("storage-change promises resolved");
      // Check the auto-saved login was removed and the original login updated
      await verifyLogins([
        {
          username: "user1",
          password: autoSavedLogin.password,
          timeCreated: user1LoginSnapshot.timeCreated,
          timeLastUsed: user1LoginSnapshot.timeLastUsed,
          passwordChangedSince: autoSavedLogin.timePasswordChanged,
        },
      ]);

      // Check we have no notifications at this point
      Assert.ok(!PopupNotifications.isPanelOpen, "No doorhanger is open");
      Assert.ok(
        !PopupNotifications.getNotification("password", browser),
        "No notifications"
      );

      // make sure the cache entry is unchanged with the removal of the auto-saved login
      Assert.equal(
        autoSavedLogin.password,
        LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
          "https://example.com"
        ).value,
        "Generated password cache entry has the expected password value"
      );
    }
  );
});

add_task(async function form_change_from_autosaved_login_to_existing_login() {
  // test when changing from a generated password in a form to an existing saved login
  // * the auto-saved login should not be deleted
  // * the metadata for the matching login should be updated
  // * the by-origin cache for the password should point at the autosaved login

  await setup_withOneLogin("user1", "xyzpassword");
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {
      password: {
        selector: passwordInputSelector,
        expectedValue: "xyzpassword",
        setValue: "",
        expectedMessage: "PasswordEditedOrGenerated",
      },
      username: {
        selector: usernameInputSelector,
        expectedValue: "user1",
        setValue: "",
        // with an empty password value, no message is sent for a username change
        expectedMessage: "",
      },
    },
    async function taskFn(browser) {
      await SimpleTest.promiseFocus(browser);

      // first, create an auto-saved login with generated password
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "addLogin"
      );
      // Wait for the confirmation hint's "shown" event rather than asserting
      // the panel's transient open state, which races with the now-async
      // auto-save.
      let confirmationHint = document.getElementById("confirmation-hint");
      let autoSaveHintShown = BrowserTestUtils.waitForPopupEvent(
        confirmationHint,
        "shown"
      );

      info("Filling generated password from AC menu");
      await fillGeneratedPasswordFromACPopup(browser, passwordInputSelector);

      info("waiting for dismissed password-change notification");
      await waitForDoorhanger(browser, "password-change");

      // Make sure confirmation hint was shown, then close it so it doesn't leak
      // into the later "no confirmation hint" assertion.
      info("waiting for confirmation hint");
      await autoSaveHintShown;
      await closePopup(confirmationHint);

      info("waiting for addLogin");
      await storageChangedPromise;
      info("addLogin promise resolved");
      // Check properties of the newly auto-saved login
      let [user1LoginSnapshot, autoSavedLogin] = await verifyLogins([
        null, // ignore the first one
        {
          timesUsed: 1,
          username: "",
          passwordLength: LoginTestUtils.generation.LENGTH,
        },
      ]);
      info("user1LoginSnapshot, guid: " + user1LoginSnapshot.guid);
      info("autoSavedLogin, guid: " + autoSavedLogin.guid);

      info("verifyLogins ok");
      let passwordCacheEntry =
        LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
          "https://example.com"
        );

      Assert.ok(
        passwordCacheEntry,
        "Got the cached generated password entry for https://example.com"
      );
      Assert.equal(
        passwordCacheEntry.value,
        autoSavedLogin.password,
        "Cached password matches the auto-saved login password"
      );
      Assert.equal(
        passwordCacheEntry.storageGUID,
        autoSavedLogin.guid,
        "Cached password guid matches the auto-saved login guid"
      );

      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: "attention",
        usernameValue: "",
        password: autoSavedLogin.password,
      });

      // close but don't remove the doorhanger, we want to ensure it is updated/replaced on further form edits
      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      let PN = notif.owner;
      PN.panel.hidePopup();
      await promiseHidden;
      await TestUtils.waitForTick();

      // now update the form with the user1 username and password
      info(`updating form`);
      let passwordEditedMessages = listenForTestNotification(
        "PasswordEditedOrGenerated",
        2
      );
      let passwordChangeDoorhangerPromise = waitForDoorhanger(
        browser,
        "password-change"
      );
      let hintDidShow = false;
      let hintPromiseShown = BrowserTestUtils.waitForPopupEvent(
        document.getElementById("confirmation-hint"),
        "shown"
      );
      hintPromiseShown.then(() => (hintDidShow = true));

      info("Entering username and password for the previously saved login");

      await changeContentFormValues(browser, {
        [passwordInputSelector]: user1LoginSnapshot.password,
        [usernameInputSelector]: user1LoginSnapshot.username,
      });
      info(
        "form edited, waiting for test notification of PasswordEditedOrGenerated"
      );

      await passwordEditedMessages;
      info("Resolved listenForTestNotification promise");

      await passwordChangeDoorhangerPromise;
      // wait to ensure there's no confirmation hint
      try {
        await TestUtils.waitForCondition(
          () => {
            return hintDidShow;
          },
          `Waiting for confirmationHint popup`,
          undefined,
          25
        );
      } catch (ex) {
        info("Got expected timeout from the waitForCondition: ", ex);
      } finally {
        Assert.ok(!hintDidShow, "No confirmation hint shown");
      }

      // the previous doorhanger would have old values, verify it was updated/replaced with new values from the form
      notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: null,
        usernameValue: user1LoginSnapshot.username,
        passwordLength: user1LoginSnapshot.password.length,
      });
      await cleanupDoorhanger(notif);

      storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "modifyLogin"
      );

      // submit the form to ensure the correct updates are made
      await submitForm(browser);
      info("form submitted, waiting for storage changed");
      await storageChangedPromise;

      // Check the auto-saved login has not changed and only metadata on the original login updated
      await verifyLogins([
        {
          username: "user1",
          password: "xyzpassword",
          timeCreated: user1LoginSnapshot.timeCreated,
          usedSince: user1LoginSnapshot.timeLastUsed,
        },
        {
          username: "",
          password: autoSavedLogin.password,
          timeCreated: autoSavedLogin.timeCreated,
          timeLastUsed: autoSavedLogin.timeLastUsed,
        },
      ]);

      // Check we have no notifications at this point
      Assert.ok(!PopupNotifications.isPanelOpen, "No doorhanger is open");
      Assert.ok(
        !PopupNotifications.getNotification("password", browser),
        "No notifications"
      );

      // make sure the cache entry is unchanged with the removal of the auto-saved login
      Assert.equal(
        autoSavedLogin.password,
        LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().get(
          "https://example.com"
        ).value,
        "Generated password cache entry has the expected password value"
      );
    }
  );
});

add_task(async function form_edit_username_and_password_of_generated_login() {
  // test when changing the username and then the password in a form with a generated password (bug 1625242)
  // * the toast is not shown for the username change as the auto-saved login is not modified
  // * the dismissed doorhanger for the username change has the correct username and password
  // * the toast is shown for the change to the generated password
  // * the dismissed doorhanger for the password change has the correct username and password

  await setup_withNoLogins();
  await openFormInNewTab(
    TEST_ORIGIN + FORM_PAGE_PATH,
    {},
    async function taskFn(browser) {
      await SimpleTest.promiseFocus(browser);

      // first, create an auto-saved login with generated password
      let storageChangedPromise = TestUtils.topicObserved(
        "passwordmgr-storage-changed",
        (_, data) => data == "addLogin"
      );
      // We don't need to wait to confirm the hint hides itelf every time
      let forceClosePopup = true;
      let hintShownAndVerified = verifyConfirmationHint(
        browser,
        forceClosePopup
      );

      info("Filling generated password from context menu");
      // there's no new-password field in this form so we'll use the context menu
      await doFillGeneratedPasswordContextMenuItem(
        browser,
        passwordInputSelector
      );

      info("waiting for dismissed password-change notification");
      await waitForDoorhanger(browser, "password-change");

      // Make sure confirmation hint was shown
      info("waiting for verifyConfirmationHint");
      await hintShownAndVerified;

      info("waiting for addLogin");
      await storageChangedPromise;
      info("addLogin promise resolved");
      // Check properties of the newly auto-saved login
      let [autoSavedLoginSnapshot] = await verifyLogins([
        {
          timesUsed: 1,
          username: "",
          passwordLength: LoginTestUtils.generation.LENGTH,
        },
      ]);

      let notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: true,
        anchorExtraAttr: "attention",
        usernameValue: "",
        password: autoSavedLoginSnapshot.password,
      });

      // close but don't remove the doorhanger, we want to ensure it is updated/replaced on further form edits
      let promiseHidden = BrowserTestUtils.waitForEvent(
        PopupNotifications.panel,
        "popuphidden"
      );
      let PN = notif.owner;
      PN.panel.hidePopup();
      await promiseHidden;
      await TestUtils.waitForTick();

      // change the username then the password in the form
      for (let {
        fieldSelector,
        fieldValue,
        expectedConfirmation,
        expectedDoorhangerUsername,
        expectedDoorhangerPassword,
        expectedDoorhangerType,
      } of [
        {
          fieldSelector: usernameInputSelector,
          fieldValue: "someuser",
          expectedConfirmation: false,
          expectedDoorhangerUsername: "someuser",
          expectedDoorhangerPassword: autoSavedLoginSnapshot.password,
          expectedDoorhangerType: "password-change",
        },
        {
          fieldSelector: passwordInputSelector,
          fieldValue: "!!",
          expectedConfirmation: true,
          expectedDoorhangerUsername: "someuser",
          expectedDoorhangerPassword: autoSavedLoginSnapshot.password + "!!",
          expectedDoorhangerType: "password-change",
        },
      ]) {
        let loginModifiedPromise = expectedConfirmation
          ? TestUtils.topicObserved(
              "passwordmgr-storage-changed",
              (_, data) => data == "modifyLogin"
            )
          : Promise.resolve();

        // now edit the field value
        let passwordEditedMessage = listenForTestNotification(
          "PasswordEditedOrGenerated"
        );
        let passwordChangeDoorhangerPromise = waitForDoorhanger(
          browser,
          expectedDoorhangerType
        );
        let hintDidShow = false;
        let hintPromiseShown = BrowserTestUtils.waitForPopupEvent(
          document.getElementById("confirmation-hint"),
          "shown"
        );
        hintPromiseShown.then(() => (hintDidShow = true));

        info(`updating form: ${fieldSelector}: ${fieldValue}`);
        await appendContentInputvalue(browser, fieldSelector, fieldValue);
        info(
          "form edited, waiting for test notification of PasswordEditedOrGenerated"
        );
        await passwordEditedMessage;
        info(
          "Resolved listenForTestNotification promise, waiting for doorhanger"
        );
        await passwordChangeDoorhangerPromise;
        // wait for possible confirmation hint
        try {
          info("Waiting for hintDidShow");
          await TestUtils.waitForCondition(
            () => hintDidShow,
            `Waiting for confirmationHint popup`,
            undefined,
            25
          );
        } catch (ex) {
          info("Got expected timeout from the waitForCondition: " + ex);
        } finally {
          info("confirmationHint check done, assert on hintDidShow");
          Assert.equal(
            hintDidShow,
            expectedConfirmation,
            "Confirmation hint shown"
          );
        }
        info(
          "Waiting for loginModifiedPromise, expectedConfirmation? " +
            expectedConfirmation
        );
        await loginModifiedPromise;

        // the previous doorhanger would have old values, verify it was updated/replaced with new values from the form
        info("Verifying the doorhanger");
        notif = await openAndVerifyDoorhanger(browser, "password-change", {
          dismissed: true,
          anchorExtraAttr: expectedConfirmation ? "attention" : null,
          usernameValue: expectedDoorhangerUsername,
          passwordLength: expectedDoorhangerPassword.length,
        });
        await cleanupDoorhanger(notif);
      }

      // submit the form to verify we still get the right doorhanger values
      let passwordChangeDoorhangerPromise = waitForDoorhanger(
        browser,
        "password-change"
      );
      await submitForm(browser);
      info("form submitted, waiting for doorhanger");
      await passwordChangeDoorhangerPromise;
      notif = await openAndVerifyDoorhanger(browser, "password-change", {
        dismissed: false,
        anchorExtraAttr: null,
        usernameValue: "someuser",
        passwordLength: LoginTestUtils.generation.LENGTH + 2,
      });
      await cleanupDoorhanger(notif);
    }
  );
});
