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

#import "components/autofill/core/browser/form_structure.h"

#import <string_view>
#import <vector>

#import "base/apple/foundation_util.h"
#import "base/containers/fixed_flat_set.h"
#import "base/files/file_enumerator.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/memory/ptr_util.h"
#import "base/no_destructor.h"
#import "base/path_service.h"
#import "base/strings/strcat.h"
#import "base/strings/string_util.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/task/thread_pool/thread_pool_instance.h"
#import "base/test/ios/wait_util.h"
#import "base/test/scoped_feature_list.h"
#import "base/threading/thread_restrictions.h"
#import "base/time/time.h"
#import "base/time/time_override.h"
#import "components/autofill/core/browser/foundations/autofill_manager_test_api.h"
#import "components/autofill/core/browser/foundations/browser_autofill_manager.h"
#import "components/autofill/core/browser/foundations/test_autofill_manager_waiter.h"
#import "components/autofill/core/browser/heuristic_source.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/core/common/autofill_payments_features.h"
#import "components/autofill/core/common/autofill_test_utils.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/browser/autofill_agent.h"
#import "components/autofill/ios/browser/autofill_driver_ios.h"
#import "components/autofill/ios/browser/autofill_driver_ios_factory.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/browser/test_autofill_client_ios.h"
#import "components/autofill/ios/browser/test_autofill_manager_injector.h"
#import "components/password_manager/core/browser/password_manager_test_utils.h"
#import "components/password_manager/core/browser/password_store/mock_password_store_interface.h"
#import "components/sync_user_events/fake_user_event_service.h"
#import "ios/chrome/browser/autofill/model/address_normalizer_factory.h"
#import "ios/chrome/browser/autofill/model/form_suggestion_controller.h"
#import "ios/chrome/browser/infobars/model/infobar_manager_impl.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h"
#import "ios/chrome/browser/passwords/model/password_controller.h"
#import "ios/chrome/browser/shared/model/paths/paths.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/sync/model/ios_user_event_service_factory.h"
#import "ios/chrome/browser/web/model/chrome_web_client.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/test/js_test_util.h"
#import "ios/web/public/test/scoped_testing_web_client.h"
#import "ios/web/public/test/task_observer_util.h"
#import "ios/web/public/test/web_state_test_util.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#import "ios/web/public/web_state.h"
#import "testing/data_driven_testing/data_driven_test.h"
#import "testing/platform_test.h"

using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

namespace autofill {
namespace {

const base::FilePath::CharType kFeatureName[] = FILE_PATH_LITERAL("autofill");
const base::FilePath::CharType kTestName[] = FILE_PATH_LITERAL("heuristics");

base::FilePath GetTestDataDir() {
  base::FilePath dir;
  base::PathService::Get(ios::DIR_TEST_DATA, &dir);
  return dir;
}

#if !BUILDFLAG(USE_INTERNAL_AUTOFILL_PATTERNS)

base::FilePath GetIOSInputDirectory() {
  base::FilePath dir;
  CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &dir));

  return dir.AppendASCII("components")
      .AppendASCII("test")
      .AppendASCII("data")
      .Append(kFeatureName)
      .Append(kTestName)
      .AppendASCII("input");
}

base::FilePath GetIOSOutputDirectory() {
  base::FilePath dir;
  CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &dir));

  return dir.AppendASCII("components")
      .AppendASCII("test")
      .AppendASCII("data")
      .Append(kFeatureName)
      .Append(kTestName)
      .AppendASCII("output");
}

const std::vector<base::FilePath>& GetTestFiles() {
  static const base::NoDestructor<std::vector<base::FilePath>> files([] {
    base::ScopedAllowBlockingForTesting allow_blocking;
    base::FilePath dir(GetIOSInputDirectory());
    std::string input_list_string;
    if (!base::ReadFileToString(dir.AppendASCII("autofill_test_files"),
                                &input_list_string)) {
      return std::vector<base::FilePath>{};
    }
    std::vector<base::FilePath> result;
    for (std::string_view piece :
         base::SplitStringPiece(input_list_string, "\n", base::TRIM_WHITESPACE,
                                base::SPLIT_WANT_NONEMPTY)) {
      result.push_back(dir.AppendASCII(piece));
    }
    return result;
  }());
  return *files;
}

constexpr int kMaxFilesInShard = 20;

int GetNumShards() {
  const size_t num_files = GetTestFiles().size();
  return std::max(1, static_cast<int>((num_files + kMaxFilesInShard - 1) /
                                      kMaxFilesInShard));
}
#else   // !BUILDFLAG(USE_INTERNAL_AUTOFILL_PATTERNS)
int GetNumShards() {
  return 1;
}
#endif  // !BUILDFLAG(USE_INTERNAL_AUTOFILL_PATTERNS)

// Test fixture for verifying Autofill heuristics. Each input is an HTML
// file that contains one or more forms. The corresponding output file lists the
// heuristically detected type for each field.
// This is based on FormStructureBrowserTest from the Chromium Project.
// TODO(crbug.com/41015125): Unify the tests.
class FormStructureBrowserTest : public PlatformTest,
                                 public testing::DataDrivenTest,
                                 public testing::WithParamInterface<int> {
 public:
  FormStructureBrowserTest(const FormStructureBrowserTest&) = delete;
  FormStructureBrowserTest& operator=(const FormStructureBrowserTest&) = delete;

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

    TestAutofillManagerWaiter& waiter() { return waiter_; }

   private:
    TestAutofillManagerWaiter waiter_{*this,
                                      {AutofillManagerEvent::kFormsSeen}};
  };

  FormStructureBrowserTest();
  ~FormStructureBrowserTest() override {}

  void SetUp() override;
  void TearDown() override;

  bool LoadHtmlWithoutSubresourcesAndInitRendererIds(const std::string& html);

  // DataDrivenTest:
  void GenerateResults(const std::string& input, std::string* output) override;

  // Serializes the given `forms` into a string.
  std::string FormStructuresToString(
      base::span<const FormStructure* const> forms);

  web::WebState* web_state() const { return web_state_.get(); }
  static base::Time GetTestTime() { return test_time_; }

  static inline base::Time test_time_;
  std::unique_ptr<base::subtle::ScopedTimeClockOverrides> time_override_;

  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  web::ScopedTestingWebClient web_client_;
  web::WebTaskEnvironment task_environment_;
  test::AutofillBrowserTestEnvironment autofill_test_environment_;
  std::unique_ptr<TestProfileIOS> profile_;
  std::unique_ptr<web::WebState> web_state_;
  std::unique_ptr<AutofillClient> autofill_client_;
  AutofillAgent* autofill_agent_;
  std::unique_ptr<TestAutofillManagerInjector<TestAutofillManager>>
      autofill_manager_injector_;
  FormSuggestionController* suggestion_controller_;

 private:
  base::test::ScopedFeatureList feature_list_;
  PasswordController* password_controller_;
};

FormStructureBrowserTest::FormStructureBrowserTest()
    : DataDrivenTest(GetTestDataDir(), kFeatureName, kTestName),
      web_client_(std::make_unique<ChromeWebClient>()) {
  std::ignore = base::Time::FromString("Sat, 01 Feb 2025 09:00:00 +0000",
                                       &FormStructureBrowserTest::test_time_);
  time_override_ = std::make_unique<base::subtle::ScopedTimeClockOverrides>(
      &FormStructureBrowserTest::GetTestTime, nullptr, nullptr);
  TestProfileIOS::Builder builder;
  builder.AddTestingFactory(
      IOSChromeProfilePasswordStoreFactory::GetInstance(),
      base::BindOnce(
          &password_manager::BuildPasswordStoreInterface<
              ProfileIOS, password_manager::MockPasswordStoreInterface>));
  builder.AddTestingFactory(
      IOSUserEventServiceFactory::GetInstance(),
      base::BindOnce([](ProfileIOS*) -> std::unique_ptr<KeyedService> {
        return std::make_unique<syncer::FakeUserEventService>();
      }));
  profile_ = std::move(builder).Build();

  web::WebState::CreateParams params(profile_.get());
  web_state_ = web::WebState::Create(params);
  feature_list_.InitWithFeatures(
      // Enabled
      {
          // TODO(crbug.com/40266396): Remove once launched.
          features::kAutofillEnableExpirationDateImprovements,
          // TODO(crbug.com/369503318): Remove once launched.
          features::kAutofillSupportSplitZipCode,
          features::kAutofillSupportStandaloneZipCodeGlobally,
          features::kAutofillEnableOneTimeCodeHeuristics,
      },
      // Disabled
      {
          // TODO(crbug.com/320965828): This feature is not supported on the iOS
          // renderer side and disabled to avoid too many differences between
          // the expectations.
          features::kAutofillBetterLocalHeuristicPlaceholderSupport,
          // TODO(crbug.com/360322019): kAutofillPageLanguageDetection needs to
          // be disabled because the page language detection is an asynchronous
          // process in the renderer. If the form parsing in the browser
          // completes before the language detection triggers a second run with
          // a known language, the results are different from results without
          // such a second run: Form parsing with a known language applies fewer
          // regular expressions than formparsing without a known language. It
          // would be ideal if the browser could just wait until the page
          // language detection is complete but at the moment the browser is
          // only informed if a non-null language could be determined. See
          // crbug.com/409067352. We disable page language detection to get a
          // deterministic result until this is fixed.
          features::kAutofillPageLanguageDetection,
      });
}

void FormStructureBrowserTest::SetUp() {
  PlatformTest::SetUp();

  // Create a PasswordController instance that will handle set up for renderer
  // ids.
  password_controller_ =
      [[PasswordController alloc] initWithWebState:web_state()];

  // AddressNormalizerFactory must be initialized in a blocking allowed scoped.
  // Initialize it now as it may DCHECK if it is initialized during the test.
  AddressNormalizerFactory::GetInstance();

  autofill_agent_ =
      [[AutofillAgent alloc] initWithPrefService:profile_->GetPrefs()
                                        webState:web_state()];
  suggestion_controller_ =
      [[FormSuggestionController alloc] initWithWebState:web_state()
                                               providers:@[ autofill_agent_ ]];

  InfoBarManagerImpl::CreateForWebState(web_state());
  autofill_client_ =
      std::make_unique<TestAutofillClientIOS>(web_state(), autofill_agent_);

  autofill_manager_injector_ =
      std::make_unique<TestAutofillManagerInjector<TestAutofillManager>>(
          web_state());
}

void FormStructureBrowserTest::TearDown() {
  autofill_manager_injector_.reset();
  autofill_client_.reset();
  suggestion_controller_ = nil;
  autofill_agent_ = nil;
  password_controller_ = nil;
  web::test::WaitForBackgroundTasks();
  web_state_.reset();
}

bool FormStructureBrowserTest::LoadHtmlWithoutSubresourcesAndInitRendererIds(
    const std::string& html) {
  if (!web::test::LoadHtmlWithoutSubresources(base::SysUTF8ToNSString(html),
                                              web_state())) {
    return false;
  }

  return WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool {
    web::WebFramesManager* frames_manager =
        GetWebFramesManagerForAutofill(web_state());
    return frames_manager->GetMainWebFrame() != nullptr;
  });
}

void FormStructureBrowserTest::GenerateResults(const std::string& input,
                                               std::string* output) {
  ASSERT_TRUE(LoadHtmlWithoutSubresourcesAndInitRendererIds(input));
  base::ThreadPoolInstance::Get()->FlushForTesting();
  TestAutofillManager* autofill_manager =
      autofill_manager_injector_->GetForMainFrame();
  ASSERT_NE(nullptr, autofill_manager);
  ASSERT_TRUE(autofill_manager->waiter().Wait(1));
  *output =
      FormStructuresToString(test_api(*autofill_manager).form_structures());
}

std::string FormStructureBrowserTest::FormStructuresToString(
    base::span<const FormStructure* const> forms) {
  std::vector<std::string> forms_string;
  // The forms are sorted by their global ID, which should make the order
  // deterministic.
  for (const FormStructure* form : forms) {
    std::string form_string;
    std::map<std::string, int> section_to_index;
    for (const auto& field : *form) {
      std::string name = base::UTF16ToUTF8(field->name());
      if (base::StartsWith(name, "gChrome~field~",
                           base::CompareCase::SENSITIVE)) {
        // The name has been generated by iOS JavaScript. Output an empty name
        // to have a behavior similar to other platforms.
        name = "";
      }
      std::string section = field->section().ToString();
      if (base::StartsWith(section, "gChrome~field~",
                           base::CompareCase::SENSITIVE)) {
        // The name has been generated by iOS JavaScript. Output an empty name
        // to have a behavior similar to other platforms.
        size_t first_underscore = section.find_first_of('_');
        section = section.substr(first_underscore);
      }
      if (field->section().is_from_fieldidentifier()) {
        // Normalize the section by replacing the unique but platform-dependent
        // integers in `field->section` with consecutive unique integers.
        // The section string is of the form "fieldname_id1_id2-suffix", where
        // id1, id2 are platform-dependent and thus need to be substituted.
        size_t last_underscore = section.find_last_of('_');
        size_t second_last_underscore =
            section.find_last_of('_', last_underscore - 1);
        int new_section_index = static_cast<int>(section_to_index.size() + 1);
        int section_index =
            section_to_index.insert(std::make_pair(section, new_section_index))
                .first->second;
        if (second_last_underscore != std::string::npos) {
          section = base::StringPrintf(
              "%s%d", section.substr(0, second_last_underscore + 1).c_str(),
              section_index);
        }
      }
      form_string += base::StrCat({field->Type().ToString(), " | ", name, " | ",
                                   base::UTF16ToUTF8(field->label()), " | ",
                                   base::UTF16ToUTF8(field->value()), " | ",
                                   section, "\n"});
    }
    forms_string.push_back(form_string);
  }
  sort(forms_string.begin(), forms_string.end());
  return base::JoinString(forms_string, "\n");
}

#if !BUILDFLAG(USE_INTERNAL_AUTOFILL_PATTERNS)
// To disable a data driven test, please add the name of the test file
// (i.e., "NNN_some_site.html") as a literal to the initializer_list given
// to the kFailingTestNames constructor.
bool IsFailingTestName(const std::string& test_name) {
  if (test_name == "132_bug_469012.html") {
    if (@available(iOS 27.0, *)) {
      return false;
    }
    return true;
  }
  static constexpr auto kFailingTestNames =
      base::MakeFixedFlatSet<std::string_view>({
          // TODO(crbug.com/40266699): These pages contains iframes. Until
          // filling across iframes is also supported on iOS, iOS has has
          // different expectations compared to non-iOS platforms.
          "049_register_ebay.com.html",
          "148_payment_dickblick.com.html",
          // TODO(crbug.com/40229922): These pages contain labels which are only
          // inferred by the label detection improvements that haven't been
          // implemented on iOS.
          "074_register_threadless.com.html",
          "097_register_alaskaair.com.html",
          "115_checkout_walgreens.com.html",
          "116_cc_checkout_walgreens.com.html",
          "150_checkout_venus.com_search_field.html",
          // TODO(crbug.com/473467160): Analyze the root causes of these
          // regressions.
          "110_checkout_harryanddavid.com.html",
          "123_bug_459132.html",
      });
  return kFailingTestNames.contains(test_name);
}
#endif  // !BUILDFLAG(USE_INTERNAL_AUTOFILL_PATTERNS)

// If disabling a test, prefer to add the name names of the specific test cases
// to IsFailingTestName(), directly above, instead of renaming the test to
// DISABLED_DataDrivenHeuristics.
TEST_P(FormStructureBrowserTest, DataDrivenHeuristics) {
#if BUILDFLAG(USE_INTERNAL_AUTOFILL_PATTERNS)
  GTEST_SKIP() << "DataDrivenHeuristics tests are only supported with legacy "
                  "parsing patterns";
#else   // BUILDFLAG(USE_INTERNAL_AUTOFILL_PATTERNS)
  const int shard = GetParam();
  const int num_shards = GetNumShards();
  const std::vector<base::FilePath>& files = GetTestFiles();
  for (size_t i = shard; i < files.size(); i += num_shards) {
    const base::FilePath& file = files[i];
    SCOPED_TRACE("Running " + file.MaybeAsASCII());
    const bool is_expected_to_pass =
        !IsFailingTestName(file.BaseName().value());
    RunOneDataDrivenTest(file, GetIOSOutputDirectory(), is_expected_to_pass);
  }
#endif  // BUILDFLAG(USE_INTERNAL_AUTOFILL_PATTERNS)
}

INSTANTIATE_TEST_SUITE_P(AllForms,
                         FormStructureBrowserTest,
                         testing::Range(0, GetNumShards()));

}  // namespace
}  // namespace autofill
