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

#include <string>
#include <unordered_map>

#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/autofill/autofill_uitest_util.h"
#include "chrome/browser/autofill/personal_data_manager_factory.h"
#include "chrome/browser/policy/policy_test_utils.h"
#include "chrome/browser/ui/autofill/chrome_autofill_client.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/content/browser/test_autofill_client_injector.h"
#include "components/autofill/content/browser/test_autofill_manager_injector.h"
#include "components/autofill/core/browser/data_manager/addresses/address_data_manager.h"
#include "components/autofill/core/browser/data_manager/personal_data_manager.h"
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/foundations/autofill_manager.h"
#include "components/autofill/core/browser/foundations/browser_autofill_manager.h"
#include "components/autofill/core/browser/foundations/test_autofill_manager_waiter.h"
#include "components/autofill/core/browser/suggestions/suggestion.h"
#include "components/autofill/core/browser/test_utils/autofill_test_utils.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "url/gurl.h"

namespace policy {
namespace {

using ::testing::AssertionResult;
using ::testing::Contains;
using ::testing::Field;
using ::testing::IsEmpty;

const char kAutofillTestPageURL[] = "/autofill/autofill_address_enabled.html";

class AutofillPolicyTest : public PolicyTest,
                           public testing::WithParamInterface<bool> {
 public:
  bool disabled_by_policy() const { return !GetParam(); }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    // Wait for Personal Data Manager to be fully loaded to prevent that
    // spurious notifications deceive the tests.
    autofill::WaitForPersonalDataManagerToBeLoaded(browser()->GetProfile());
    ASSERT_TRUE(ImportAddress());

    PolicyMap policies;
    SetPolicy(&policies, key::kAutofillAddressEnabled, base::Value(GetParam()));
    UpdateProviderPolicy(policies);

    // The base test fixture creates a tab before we set the policy. We create a
    // new tab so a new ChromeAutofillClient is created.
    AddBlankTabAndShow(browser());
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  content::WebContents* GetWebContents() {
    return browser()->tab_strip_model()->GetActiveWebContents();
  }

  autofill::PersonalDataManager* personal_data_manager() {
    return autofill::PersonalDataManagerFactory::GetForBrowserContext(
        browser()->GetProfile());
  }

  [[nodiscard]] testing::AssertionResult ImportAddress() {
    if (personal_data_manager()->address_data_manager().GetProfiles().size() !=
        0u) {
      return testing::AssertionFailure() << "Should be empty profile.";
    }
    autofill::AddTestProfile(browser()->GetProfile(),
                             autofill::test::GetFullProfile());
    expected_suggestions_["name"] = u"John H. Doe";
    expected_suggestions_["street-address"] = u"666 Erebus St., Apt 8";
    expected_suggestions_["postal-code"] = u"91111";
    expected_suggestions_["city"] = u"Elysium";
    expected_suggestions_["phone"] = u"+1 650-211-1111";
    expected_suggestions_["email"] = u"johndoe@hades.com";
    return personal_data_manager()
                       ->address_data_manager()
                       .GetProfiles()
                       .size() == 1u
               ? testing::AssertionSuccess()
               : testing::AssertionFailure() << "Should be one profile.";
  }

  std::unordered_map<std::string, std::u16string> GetExpectedSuggestions() {
    return expected_suggestions_;
  }

  [[nodiscard]] testing::AssertionResult NavigateToTestPage() {
    if (!(ui_test_utils::NavigateToURL(
            browser(), embedded_test_server()->GetURL(kAutofillTestPageURL)))) {
      return testing::AssertionFailure();
    }
    // Wait for the test page to be rendered to receive clicks.
    content::MainThreadFrameObserver frame_observer(
        GetWebContents()->GetRenderWidgetHostView()->GetRenderWidgetHost());
    frame_observer.Wait();
    return testing::AssertionSuccess();
  }

 protected:
  class TestAutofillManager : public autofill::BrowserAutofillManager {
   public:
    explicit TestAutofillManager(autofill::ContentAutofillDriver* driver)
        : autofill::BrowserAutofillManager(driver) {}

    [[nodiscard]] AssertionResult WaitForFormsSeen() {
      return forms_seen_waiter_.Wait();
    }

    // The test can not wait for autofill popup to show, because when
    // autofill gets disabled, the test will hang there. An alternative is to
    // have a timeout, but it could be flaky on bots with different specs.
    // Hence the test checks the OnAskForValues event, if this event got
    // fired, Autofill popup should have appeared, otherwise it is disabled by
    // policy.
    [[nodiscard]] AssertionResult WaitForAskForValuesToFill() {
      return ask_for_value_to_fill_waiter_.Wait();
    }

   private:
    autofill::TestAutofillManagerWaiter forms_seen_waiter_{
        *this,
        {autofill::AutofillManagerEvent::kFormsSeen}};
    autofill::TestAutofillManagerWaiter ask_for_value_to_fill_waiter_{
        *this,
        {autofill::AutofillManagerEvent::kAskForValuesToFill}};
  };

  class TestAutofillClient : public autofill::ChromeAutofillClient {
   public:
    explicit TestAutofillClient(content::WebContents* web_contents)
        : autofill::ChromeAutofillClient(web_contents) {}

    SuggestionUiSessionId ShowAutofillSuggestions(
        const autofill::AutofillClient::PopupOpenArgs& open_args,
        base::WeakPtr<autofill::AutofillSuggestionDelegate> delegate) override {
      suggestions_ = open_args.suggestions;
      return autofill::ChromeAutofillClient::ShowAutofillSuggestions(open_args,
                                                                     delegate);
    }

    const std::vector<autofill::Suggestion>& suggestions() const {
      return suggestions_;
    }

    void ResetSuggestions() { suggestions_ = {}; }

   private:
    std::vector<autofill::Suggestion> suggestions_;
  };

  TestAutofillClient* autofill_client() {
    return autofill_client_injector_[GetWebContents()];
  }

  TestAutofillManager* autofill_manager() {
    return autofill_manager_injector_[GetWebContents()];
  }

 private:
  autofill::TestAutofillClientInjector<TestAutofillClient>
      autofill_client_injector_;

  autofill::TestAutofillManagerInjector<TestAutofillManager>
      autofill_manager_injector_;

  std::unordered_map<std::string, std::u16string> expected_suggestions_;
};

INSTANTIATE_TEST_SUITE_P(, AutofillPolicyTest, testing::Bool());

IN_PROC_BROWSER_TEST_P(AutofillPolicyTest, AutofillDisabledByPolicy) {
  ASSERT_TRUE(NavigateToTestPage());
  EXPECT_TRUE(autofill_manager()->WaitForFormsSeen());
  for (const auto& [element, expectation] : GetExpectedSuggestions()) {
    SCOPED_TRACE(testing::Message() << "element = " << element
                                    << ", expectation = " << expectation);
    content::SimulateMouseClickOrTapElementWithId(GetWebContents(), element);

    // Showing the Autofill Popup is an asynchronous task.
    EXPECT_TRUE(autofill_manager()->WaitForAskForValuesToFill());
    if (disabled_by_policy()) {
      // Autofill currently does not have the event infrastructure for asserting
      // that no popup was shown. RunUntilIdle() is not sufficient, so this test
      // is misleading.
      // TODO: crbug.com/373703937 - Fix this.
      base::RunLoop().RunUntilIdle();
    } else {
      EXPECT_TRUE(base::test::RunUntil([&]() {
        return !autofill_client()->suggestions().empty();
      })) << "Showing the Autofill Popup timed out.";
    }

    EXPECT_EQ(autofill_client()->suggestions().empty(), disabled_by_policy());
    if (!disabled_by_policy()) {
      // There may be more suggestions, but the first one in the vector
      // should be the expected and shown in the popup.
      EXPECT_THAT(autofill_client()->suggestions(),
                  Contains(Field(
                      &autofill::Suggestion::main_text,
                      Field(&autofill::Suggestion::Text::value, expectation))));
    }
    autofill_client()->ResetSuggestions();
  }
}

}  // namespace
}  // namespace policy
