// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/command_line.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/password_manager/factories/profile_password_store_factory.h"
#include "chrome/browser/password_manager/password_manager_interactive_test_base.h"
#include "chrome/browser/password_manager/password_manager_test_util.h"
#include "chrome/browser/password_manager/password_manager_uitest_util.h"
#include "chrome/browser/password_manager/passwords_navigation_observer.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/passwords/password_generation_popup_observer.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/content/browser/content_autofill_client.h"
#include "components/autofill/content/browser/test_autofill_client_injector.h"
#include "components/autofill/core/browser/foundations/test_autofill_client.h"
#include "components/autofill/core/browser/test_utils/autofill_test_utils.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/password_manager/content/browser/content_password_manager_driver.h"
#include "components/password_manager/core/browser/features/password_features.h"
#include "components/password_manager/core/browser/password_form_manager.h"
#include "components/password_manager/core/browser/password_generation_frame_helper.h"
#include "components/password_manager/core/browser/password_manager_util.h"
#include "components/password_manager/core/browser/password_store/password_form_converters.h"
#include "components/password_manager/core/browser/password_store/test_password_store.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/prerender_test_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/keycodes/keyboard_codes.h"

namespace {

enum ReturnCodes {  // Possible results of the JavaScript code.
  RETURN_CODE_OK,
  RETURN_CODE_NO_ELEMENT,
  RETURN_CODE_INVALID,
};

}  // namespace

class PasswordGenerationInteractiveTest
    : public PasswordManagerInteractiveTestBase {
 public:
  void SetUpOnMainThread() override {
    PasswordManagerBrowserTestBase::SetUpOnMainThread();

    // Set observer for popup.
    ChromePasswordManagerClient* client =
        ChromePasswordManagerClient::FromWebContents(WebContents());
    client->SetTestObserver(&observer_);

    password_manager::PasswordFormManager::
        set_wait_for_server_predictions_for_filling(false);

    NavigateToFile("/password/signup_form_new_password.html");
  }

  void TearDownOnMainThread() override {
    // RunUntilIdle() is necessary because otherwise, under the hood
    // PasswordFormManager::OnFetchComplete() callback is run after this test is
    // destroyed meaning that OsCryptImpl will be used instead of OsCryptMocker,
    // causing this test to fail.
    base::RunLoop().RunUntilIdle();
    PasswordManagerBrowserTestBase::TearDownOnMainThread();
  }

  // Waits until the value of the field with id |field_id| becomes non-empty.
  void WaitForNonEmptyFieldValue(const std::string& field_id) {
    const std::string script = base::StringPrintf(
        "element = document.getElementById('%s');"
        "new Promise(resolve => {"
        "  if (!element) {"
        "    setTimeout(() => resolve(%d), 0);"
        "  }"
        "  if (element.value) {"
        "    setTimeout(() => resolve(%d), 0); "
        "  } else {"
        "    element.onchange = function() {"
        "      if (element.value) {"
        "        resove(%d);"
        "      }"
        "    }"
        "  }"
        "});",
        field_id.c_str(), RETURN_CODE_NO_ELEMENT, RETURN_CODE_OK,
        RETURN_CODE_OK);
    EXPECT_EQ(RETURN_CODE_OK,
              content::EvalJs(RenderFrameHost(), script,
                              content::EXECUTE_SCRIPT_NO_USER_GESTURE));
  }

  void WaitForFieldFocused(const std::string& field_id) {
    const std::string script = base::StringPrintf(
        "element = document.getElementById('%s');"
        "new Promise(resolve => {"
        "  if (!element) {"
        "    resolve(%d);"
        "  }"
        "  if (document.activeElement && element.id == "
        "document.activeElement.id) {"
        "    resolve(%d);"
        "  } else {"
        "    element.onfocus = function() {"
        "      resolve(%d);"
        "      element.onfocus = undefined;"
        "    }"
        "  }"
        "});",
        field_id.c_str(), RETURN_CODE_NO_ELEMENT, RETURN_CODE_OK,
        RETURN_CODE_OK);
    EXPECT_EQ(RETURN_CODE_OK,
              content::EvalJs(RenderFrameHost(), script,
                              content::EXECUTE_SCRIPT_NO_USER_GESTURE));
  }

  void WaitForPasswordFieldFocused() { WaitForFieldFocused("password_field"); }

  std::string GetFocusedElement() {
    return content::EvalJs(WebContents(), "document.activeElement.id")
        .ExtractString();
  }

  void SimulateMouseClickOrTapAt(content::RenderWidgetHost* rwh,
                                 gfx::PointF point) {
    blink::WebMouseEvent mouse_event(
        blink::WebInputEvent::Type::kMouseDown,
        blink::WebInputEvent::kNoModifiers,
        blink::WebInputEvent::GetStaticTimeStampForTests());
    mouse_event.button = blink::WebPointerProperties::Button::kLeft;
    mouse_event.SetPositionInWidget(point.x(), point.y());
    rwh->ForwardMouseEvent(mouse_event);
  }

  void FocusPasswordField() {
    content::SimulateMouseClickOrTapElementWithId(WebContents(),
                                                  "password_field");
    WaitForPasswordFieldFocused();
  }

  void FocusUsernameField() {
    ASSERT_TRUE(content::ExecJs(
        WebContents(), "document.getElementById('username_field').focus();"));
  }

  void SendKeyToPopup(ui::KeyboardCode key) {
    input::NativeWebKeyboardEvent event(
        blink::WebKeyboardEvent::Type::kRawKeyDown,
        blink::WebInputEvent::kNoModifiers,
        blink::WebInputEvent::GetStaticTimeStampForTests());
    event.windows_key_code = key;
    WebContents()
        ->GetPrimaryMainFrame()
        ->GetRenderViewHost()
        ->GetWidget()
        ->ForwardKeyboardEvent(event);
  }

  void NavigateToAndAcceptSuggestedPassword() {
    // Cancel button is the first focusable element, hence two down button
    // presses to reach the accept button.
    SendKeyToPopup(ui::VKEY_DOWN);
    SendKeyToPopup(ui::VKEY_DOWN);
    SendKeyToPopup(ui::VKEY_RETURN);
  }

  bool GenerationPopupShowing() {
    return observer_.popup_showing() &&
           observer_.state() ==
               PasswordGenerationPopupController::kOfferGeneration;
  }

  bool EditingPopupShowing() {
    return observer_.popup_showing() &&
           observer_.state() ==
               PasswordGenerationPopupController::kEditGeneratedPassword;
  }

  void WaitForStatus(TestGenerationPopupObserver::GenerationPopup status) {
    observer_.WaitForStatus(status);
  }

  void WaitForGenerationPopupShowing() {
    if (GenerationPopupShowing()) {
      return;
    }
    observer_.WaitForStatusChange();
    EXPECT_TRUE(GenerationPopupShowing());
  }

  void WaitForEditingPopupShowing() {
    if (EditingPopupShowing()) {
      return;
    }
    observer_.WaitForStatusChange();
    EXPECT_TRUE(EditingPopupShowing());
  }

 private:
  TestGenerationPopupObserver observer_;
};

// A test fixture that injects an `ObservingAutofillClient` into newly created
// tabs to allow waiting for an Autofill popup to open.
class PasswordGenerationAutofillPopupInteractiveTest
    : public PasswordGenerationInteractiveTest {
 protected:
  ObservingAutofillClient& autofill_client() {
    return *autofill_client_injector_[WebContents()];
  }

 private:
  autofill::TestAutofillClientInjector<ObservingAutofillClient>
      autofill_client_injector_;
};

// TODO(https://crbug.com/501668599): Re-enable this test after flakiness is
// resolved.
IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       DISABLED_PopupShownAndPasswordSelected) {
  FocusPasswordField();
  WaitForGenerationPopupShowing();
  base::HistogramTester histogram_tester;
  NavigateToAndAcceptSuggestedPassword();

  // Selecting the password should fill the field and move focus to the
  // submit button.
  WaitForNonEmptyFieldValue("password_field");
  EXPECT_FALSE(GenerationPopupShowing());
  EXPECT_FALSE(EditingPopupShowing());
  EXPECT_EQ("input_submit_button", GetFocusedElement());

  // Re-focusing the password field should show the editing popup.
  FocusPasswordField();
  WaitForEditingPopupShowing();

  // The metrics are recorded when the form manager is destroyed. Closing the
  // tab enforces it.
  ClearWebContentsPtr();
  CloseAllBrowsers();
  histogram_tester.ExpectUniqueSample(
      "PasswordGeneration.UserDecision",
      password_manager::PasswordFormMetricsRecorder::GeneratedPasswordStatus::
          kPasswordAccepted,
      1);
}

// TODO(http://crbug.com/382272011): Re-enable this test after flakiness is
// resolved.
IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       DISABLED_PopupShownAutomaticallyAndPasswordErased) {
  FocusPasswordField();
  WaitForGenerationPopupShowing();
  NavigateToAndAcceptSuggestedPassword();

  // Wait until the password is filled.
  WaitForNonEmptyFieldValue("password_field");

  // Re-focusing the password field should show the editing popup.
  FocusPasswordField();
  WaitForEditingPopupShowing();

  // Delete the password. The generation prompt should be visible.
  base::HistogramTester histogram_tester;
  SimulateUserDeletingFieldContent("password_field");
  WaitForGenerationPopupShowing();

  // The metrics are recorded on navigation when the frame is destroyed.
  NavigateToFile("/password/done.html");
  histogram_tester.ExpectUniqueSample(
      "PasswordGeneration.UserDecision",
      password_manager::PasswordFormMetricsRecorder::GeneratedPasswordStatus::
          kPasswordDeleted,
      1);
}

// TODO(https://crbug.com/501668599): Re-enable this test after flakiness is
// resolved.
IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       DISABLED_PopupShownManuallyAndPasswordErased) {
  NavigateToFile("/password/password_form.html");
  FocusPasswordField();
  EXPECT_FALSE(GenerationPopupShowing());
  // The same flow happens when user generates a password from the context menu.
  password_manager_util::UserTriggeredManualGenerationFromContextMenu(
      ChromePasswordManagerClient::FromWebContents(WebContents()),
      autofill::ContentAutofillClient::FromWebContents(WebContents()));
  WaitForStatus(TestGenerationPopupObserver::GenerationPopup::kShown);
  EXPECT_TRUE(GenerationPopupShowing());
  NavigateToAndAcceptSuggestedPassword();

  // Wait until the password is filled.
  WaitForNonEmptyFieldValue("password_field");

  // Re-focusing the password field should show the editing popup.
  FocusPasswordField();
  WaitForEditingPopupShowing();

  // Delete the password. The generation prompt should not be visible.
  SimulateUserDeletingFieldContent("password_field");
  WaitForStatus(TestGenerationPopupObserver::GenerationPopup::kHidden);
  EXPECT_FALSE(EditingPopupShowing());
  EXPECT_FALSE(GenerationPopupShowing());
}

// Verify that tapping the password field does not trigger the suggestion popup
// when user triggered the generation popup first (either by choosing it from
// suggestions popup with generation or context menu).
IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       SuggestionsPopupIsNotShownWhenGenerationPopupVisible) {
  // Save the credentials since the autofill popup with generation and
  // password suggestion would not appear without stored passwords.
  password_manager::PasswordStoreInterface* password_store =
      ProfilePasswordStoreFactory::GetForProfile(
          browser()->GetProfile(), ServiceAccessType::IMPLICIT_ACCESS)
          .get();
  password_manager::PasswordForm signin_form;
  signin_form.signon_realm = embedded_test_server()->base_url().spec();
  signin_form.username_value = u"temp";
  signin_form.password_value = u"random123";
  password_store->AddLogin(password_manager::FromPasswordForm(signin_form));
  WaitForPasswordStore();
  NavigateToFile("/password/signup_form_new_password.html");

  FocusPasswordField();
  // The user generates a password from the context menu.
  password_manager_util::UserTriggeredManualGenerationFromContextMenu(
      ChromePasswordManagerClient::FromWebContents(WebContents()),
      autofill::ContentAutofillClient::FromWebContents(WebContents()));
  WaitForStatus(TestGenerationPopupObserver::GenerationPopup::kShown);
  WaitForGenerationPopupShowing();
  EXPECT_TRUE(GenerationPopupShowing());

  // Click on the password field and check that generation popup is still shown.
  content::SimulateMouseClickOrTapElementWithId(WebContents(),
                                                "password_field");
  EXPECT_TRUE(GenerationPopupShowing());
}

// Verify that password suggestions popup is still showing up even after
// user rejected password generation.
IN_PROC_BROWSER_TEST_F(
    PasswordGenerationAutofillPopupInteractiveTest,
    PopupWithSuggestionsShowingUpAfterUserRejectedGeneration) {
  // Save a credential since the autofill popup with generation and
  // password suggestion would not appear otherwise.
  password_manager::PasswordStoreInterface* password_store =
      ProfilePasswordStoreFactory::GetForProfile(
          browser()->GetProfile(), ServiceAccessType::IMPLICIT_ACCESS)
          .get();
  password_manager::PasswordForm signin_form;
  signin_form.signon_realm = embedded_test_server()->base_url().spec();
  signin_form.username_value = u"temp";
  signin_form.password_value = u"random123";
  password_store->AddLogin(password_manager::FromPasswordForm(signin_form));
  WaitForPasswordStore();

  NavigateToFile("/password/signup_form_new_password.html");

  // Trigger password generation and reject by navigating to "Cancel" button.
  password_manager_util::UserTriggeredManualGenerationFromContextMenu(
      ChromePasswordManagerClient::FromWebContents(WebContents()),
      autofill::ContentAutofillClient::FromWebContents(WebContents()));
  SendKeyToPopup(ui::VKEY_DOWN);
  SendKeyToPopup(ui::VKEY_RETURN);
  WaitForStatus(TestGenerationPopupObserver::GenerationPopup::kHidden);

  // Check that the suggestions popup still shows up on password field tap.
  content::SimulateMouseClickOrTapElementWithId(WebContents(),
                                                "password_field");
  autofill_client().WaitForAutofillPopup();
}

// TODO(https://crbug.com/501668599): Re-enable this test after flakiness is
// resolved.
IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       DISABLED_PopupShownAndDismissed) {
  FocusPasswordField();
  WaitForGenerationPopupShowing();

  FocusUsernameField();

  // Popup is dismissed.
  WaitForStatus(TestGenerationPopupObserver::GenerationPopup::kHidden);
}

IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       PopupShownAndDismissedByKeyPress) {
  FocusPasswordField();
  WaitForGenerationPopupShowing();

  SendKeyToPopup(ui::VKEY_ESCAPE);

  // Popup is dismissed.
  EXPECT_FALSE(GenerationPopupShowing());
}

// TODO(https://crbug.com/501668599): Re-enable this test after flakiness is
// resolved.
IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       DISABLED_PopupShownAndDismissedByScrolling) {
  FocusPasswordField();
  WaitForGenerationPopupShowing();

  ASSERT_TRUE(content::ExecJs(WebContents(), "window.scrollTo(100, 0);"));

  EXPECT_FALSE(GenerationPopupShowing());
}

IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       GenerationTriggeredInIFrame) {
  NavigateToFile("/password/framed_signup_form.html");

  // Execute the script in the context of the iframe so that it kinda receives a
  // user gesture.
  content::RenderFrameHost* child_frame = ChildFrameAt(WebContents(), 0);
  gfx::PointF click_point =
      GetCenterCoordinatesOfElementWithId(child_frame, "password_field");
  SimulateMouseClickOrTapAt(child_frame->GetRenderWidgetHost(), click_point);

  WaitForGenerationPopupShowing();
}

IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       GenerationTriggeredOnTap) {
  ASSERT_TRUE(content::ExecJs(
      RenderFrameHost(),
      "var submitRect = document.getElementById('password_field')"
      ".getBoundingClientRect();",
      content::EXECUTE_SCRIPT_NO_USER_GESTURE));
  double y = content::EvalJs(RenderFrameHost(),
                             "(submitRect.top + submitRect.bottom) / 2;",
                             content::EXECUTE_SCRIPT_NO_USER_GESTURE)
                 .ExtractDouble();
  double x = content::EvalJs(RenderFrameHost(),
                             "(submitRect.left + submitRect.right) / 2;",
                             content::EXECUTE_SCRIPT_NO_USER_GESTURE)
                 .ExtractDouble();

  // Tap in the middle of the field.
  content::SimulateTapAt(WebContents(),
                         gfx::Point(static_cast<int>(x), static_cast<int>(y)));
  WaitForStatus(TestGenerationPopupObserver::GenerationPopup::kShown);
}

IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       GenerationTriggeredOnClick) {
  ASSERT_TRUE(content::ExecJs(
      RenderFrameHost(),
      "var submitRect = document.getElementById('password_field')"
      ".getBoundingClientRect();",
      content::EXECUTE_SCRIPT_NO_USER_GESTURE));
  double y = content::EvalJs(RenderFrameHost(),
                             "(submitRect.top + submitRect.bottom) / 2;",
                             content::EXECUTE_SCRIPT_NO_USER_GESTURE)
                 .ExtractDouble();
  double x = content::EvalJs(RenderFrameHost(),
                             "(submitRect.left + submitRect.right) / 2;",
                             content::EXECUTE_SCRIPT_NO_USER_GESTURE)
                 .ExtractDouble();

  // Click in the middle of the field.
  content::SimulateMouseClickAt(
      WebContents(), 0, blink::WebMouseEvent::Button::kLeft,
      gfx::Point(static_cast<int>(x), static_cast<int>(y)));
  WaitForStatus(TestGenerationPopupObserver::GenerationPopup::kShown);
}

IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       AutoSavingGeneratedPassword) {
  scoped_refptr<password_manager::TestPasswordStore> password_store =
      GetDefaultPasswordStore(browser()->GetProfile());

  FocusPasswordField();
  WaitForGenerationPopupShowing();
  NavigateToAndAcceptSuggestedPassword();

  // Change username.
  FocusUsernameField();
  content::SimulateCharTyped(WebContents(), 'U');
  content::SimulateCharTyped(WebContents(), 'N');

  // Submit form.
  PasswordsNavigationObserver observer(WebContents());
  std::string submit_script =
      "document.getElementById('input_submit_button').click()";
  ASSERT_TRUE(content::ExecJs(WebContents(), submit_script));
  ASSERT_TRUE(observer.Wait());

  WaitForPasswordStore();
  EXPECT_FALSE(GetAllLoginsSync(password_store.get()).empty());

  // Make sure the username is correct.
  password_manager::TestPasswordStore::PasswordMap stored_passwords =
      GetAllLoginsSync(password_store.get());
  EXPECT_EQ(1u, stored_passwords.size());
  EXPECT_EQ(1u, stored_passwords.begin()->second.size());
  EXPECT_EQ(u"UN", (stored_passwords.begin()->second)[0].username_value);
}

// Verify that navigating away closes the popup.
IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       NavigatingAwayClosesPopup) {
  // Open popup.
  FocusPasswordField();
  WaitForGenerationPopupShowing();

  // Simulate navigating to a different page.
  NavigateToFile("/password/signup_form.html");

  // Check that popup is dismissed.
  EXPECT_FALSE(GenerationPopupShowing());
}

// TODO(https://crbug.com/501668599): Re-enable this test after flakiness is
// resolved.
IN_PROC_BROWSER_TEST_F(PasswordGenerationInteractiveTest,
                       DISABLED_GenerationPopupNotShownAfterUserRejected) {
  FocusPasswordField();
  WaitForGenerationPopupShowing();

  // Reject generation by navigating to "Cancel" button.
  SendKeyToPopup(ui::VKEY_DOWN);
  SendKeyToPopup(ui::VKEY_RETURN);

  FocusPasswordField();
  EXPECT_FALSE(GenerationPopupShowing());
}

class PasswordGenerationPopupViewPrerenderingTest
    : public PasswordGenerationInteractiveTest {
 public:
  PasswordGenerationPopupViewPrerenderingTest()
      : prerender_helper_(base::BindRepeating(
            &PasswordGenerationPopupViewPrerenderingTest::WebContents,
            base::Unretained(this))) {}
  ~PasswordGenerationPopupViewPrerenderingTest() override = default;

  void SetUp() override {
    prerender_helper_.RegisterServerRequestMonitor(embedded_test_server());
    PasswordGenerationInteractiveTest::SetUp();
  }

  content::test::PrerenderTestHelper* prerender_helper() {
    return &prerender_helper_;
  }

 protected:
  content::test::PrerenderTestHelper prerender_helper_;
};

IN_PROC_BROWSER_TEST_F(PasswordGenerationPopupViewPrerenderingTest,
                       PasswordGenerationPopupControllerInPrerendering) {
  // Open popup.
  FocusPasswordField();
  WaitForGenerationPopupShowing();

  auto prerender_url = embedded_test_server()->GetURL("/empty.html");
  // Loads a page in the prerender.
  content::PrerenderHostId host_id =
      prerender_helper()->AddPrerender(prerender_url);
  content::test::PrerenderHostObserver host_observer(*WebContents(), host_id);
  // It should keep the current popup controller since the prerenedering should
  // not affect the current page.
  EXPECT_TRUE(GenerationPopupShowing());

  // Navigates the primary page to the URL.
  prerender_helper()->NavigatePrimaryPage(prerender_url);
  // Makes sure that the page is activated from the prerendering.
  EXPECT_TRUE(host_observer.was_activated());
  // It should clear the current popup controller since the page loading deletes
  // the popup controller from the previous page.
  EXPECT_FALSE(GenerationPopupShowing());
}
