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

#include "components/autofill/content/renderer/form_cache.h"

#include <algorithm>
#include <optional>
#include <string_view>

#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "components/autofill/content/renderer/autofill_agent_test_api.h"
#include "components/autofill/content/renderer/autofill_renderer_test.h"
#include "components/autofill/content/renderer/focus_test_utils.h"
#include "components/autofill/content/renderer/form_autofill_util.h"
#include "components/autofill/content/renderer/test_utils.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/field_data_manager.h"
#include "components/autofill/core/common/form_data_test_api.h"
#include "components/autofill/core/common/form_field_data.h"
#include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h"
#include "content/public/test/render_view_test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_input_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_select_element.h"

using blink::WebDocument;
using blink::WebElement;
using testing::AllOf;
using testing::Each;
using testing::ElementsAre;
using testing::Field;
using testing::IsEmpty;
using testing::Property;
using testing::SizeIs;
using testing::UnorderedElementsAre;

namespace autofill {
namespace {

auto HasId(FormRendererId expected_id) {
  return Property("renderer_id", &FormData::renderer_id, expected_id);
}

auto HasName(std::string_view expected_name) {
  return Property("name", &FormData::name, base::ASCIIToUTF16(expected_name));
}

auto IsToken(FrameToken expected_token, int expected_predecessor) {
  return AllOf(
      Field(&FrameTokenWithPredecessor::token, expected_token),
      Field(&FrameTokenWithPredecessor::predecessor, expected_predecessor));
}

const FormData* GetFormByName(const std::vector<FormData>& forms,
                              std::string_view name) {
  for (const FormData& form : forms) {
    if (form.name() == base::ASCIIToUTF16(name)) {
      return &form;
    }
  }
  return nullptr;
}

class FormCacheBrowserTest : public test::AutofillRendererTest {
 public:
  static constexpr CallTimerState kCallTimerStateDummy = {
      .call_site = CallTimerState::CallSite::kExtractForms,
      .last_autofill_agent_reset = {},
      .last_dom_content_loaded = {},
  };

  ~FormCacheBrowserTest() override = default;

  void SetUp() override {
    test::AutofillRendererTest::SetUp();
    form_cache_.emplace(&autofill_agent());
  }

  void TearDown() override {
    form_cache_.reset();
    test::AutofillRendererTest::TearDown();
  }

  FormCache::UpdateFormCacheResult UpdateFormCache() {
    return form_cache_->UpdateFormCache(GetFieldDataManager(),
                                        kCallTimerStateDummy);
  }

  void ClearCache() { form_cache_->ClearCache(); }

  size_t num_extracted_forms() {
    return std::ranges::count_if(form_cache_->extracted_forms_unsafe(),
                                 [](const auto& id_and_form) {
                                   const auto& [id, form] = id_and_form;
                                   return form != nullptr;
                                 });
  }

  FieldDataManager& GetFieldDataManager() const {
    return *field_data_manager_.get();
  }

  test::FocusTestUtils& focus_test_utils() { return focus_test_utils_; }

 private:
  scoped_refptr<FieldDataManager> field_data_manager_ =
      base::MakeRefCounted<FieldDataManager>();

  test::FocusTestUtils focus_test_utils_{
      base::BindRepeating(&FormCacheBrowserTest::ExecuteJavaScriptForTests,
                          base::Unretained(this))};

  // The subject of this test fixture. The fixture inherits from
  // AutofillRendererTest because we need an AutofillAgent to initialize a
  // FormCache. That AutofillAgent has a FormCache on its own, but we define a
  // separate one for this test fixture because the `AutofillAgent`'s cache is
  // used and populated by `AutofillAgent`.
  std::optional<FormCache> form_cache_;
};

TEST_F(FormCacheBrowserTest, UpdatedForms) {
  LoadHTML(R"(
    <form id="form1">
      <input type="text" name="foo1">
      <input type="text" name="foo2">
      <input type="text" name="foo3">
    </form>
    <input type="text" name="unowned_element">
  )");

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
  EXPECT_TRUE(forms.removed_forms.empty());

  const FormData* form1 = GetFormByName(forms.updated_forms, "form1");
  ASSERT_TRUE(form1);
  EXPECT_EQ(3u, form1->fields().size());
  EXPECT_TRUE(form1->child_frames().empty());

  const FormData* unowned_form = GetFormByName(forms.updated_forms, "");
  ASSERT_TRUE(unowned_form);
  EXPECT_EQ(1u, unowned_form->fields().size());
  EXPECT_TRUE(unowned_form->child_frames().empty());
}

TEST_F(FormCacheBrowserTest, RemovedForms) {
  LoadHTML(R"(
    <form id="form1">
      <input type="text" name="foo1">
      <input type="text" name="foo2">
      <input type="text" name="foo3">
    </form>
    <form id="form2">
      <input type="text" name="foo1">
      <input type="text" name="foo2">
      <input type="text" name="foo3">
    </form>
    <input type="text" id="unowned_element">
  )");

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasId(FormRendererId()), HasName("form1"),
                                   HasName("form2")));
  EXPECT_TRUE(forms.removed_forms.empty());

  std::vector<FormRendererId> forms_renderer_id;
  for (const FormData& form : forms.updated_forms) {
    if (form.renderer_id() != FormRendererId()) {
      forms_renderer_id.push_back(form.renderer_id());
    }
  }
  ASSERT_EQ(forms_renderer_id.size(), 2u);

  ExecuteJavaScriptForTests(R"(
    document.getElementById("form1").remove();
    document.getElementById("form2").innerHTML = "";
  )");

  forms = UpdateFormCache();

  EXPECT_TRUE(forms.updated_forms.empty());
  EXPECT_THAT(forms.removed_forms,
              UnorderedElementsAre(forms_renderer_id[0], forms_renderer_id[1]));

  ExecuteJavaScriptForTests(R"(
    document.getElementById("unowned_element").remove();
  )");

  forms = UpdateFormCache();

  EXPECT_TRUE(forms.updated_forms.empty());
  EXPECT_THAT(forms.removed_forms, ElementsAre(FormRendererId()));

  ExecuteJavaScriptForTests(R"(
    document.getElementById("form2").innerHTML = `
      <input type="text" name="foo1">
      <input type="text" name="foo2">
      <input type="text" name="foo3">
    `;
  )");

  forms = UpdateFormCache();

  EXPECT_THAT(forms.updated_forms, ElementsAre(HasName("form2")));
  EXPECT_TRUE(forms.removed_forms.empty());

  ExecuteJavaScriptForTests(R"(
    document.getElementById("form2").innerHTML = `
      <input type="text" name="foo1">
      <input type="text" name="foo2">
      <input type="text" name="foo3">
      <input type="text" name="foo4">
    `;
  )");

  forms = UpdateFormCache();

  EXPECT_THAT(forms.updated_forms, ElementsAre(HasName("form2")));
  EXPECT_TRUE(forms.removed_forms.empty());
}

// Test if the form gets re-extracted after a label change.
TEST_F(FormCacheBrowserTest, ExtractFormAfterDynamicFieldChange) {
  LoadHTML(R"(
    <form id="f"><input></form>
    <form id="g"> <label id="label">Name</label><input></form>
  )");

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();
  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasName("f"), HasName("g")));
  EXPECT_TRUE(forms.removed_forms.empty());

  ExecuteJavaScriptForTests(R"(
    document.getElementById("label").innerHTML = "Last Name";
  )");

  forms = UpdateFormCache();
  EXPECT_THAT(forms.updated_forms, ElementsAre(HasName("g")));
  EXPECT_TRUE(forms.removed_forms.empty());
}

TEST_F(FormCacheBrowserTest, ExtractFrames) {
  LoadHTML(R"(
    <form id="form1">
      <iframe id="frame1"></iframe>
    </form>
    <iframe id="frame2"></iframe>
  )");

  FrameToken frame1_token = GetFrameToken(GetDocument(), "frame1");
  FrameToken frame2_token = GetFrameToken(GetDocument(), "frame2");

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
  EXPECT_TRUE(forms.removed_forms.empty());

  const FormData* form1 = GetFormByName(forms.updated_forms, "form1");
  ASSERT_TRUE(form1);
  EXPECT_TRUE(form1->fields().empty());
  EXPECT_THAT(form1->child_frames(), ElementsAre(IsToken(frame1_token, -1)));

  const FormData* unowned_form = GetFormByName(forms.updated_forms, "");
  ASSERT_TRUE(unowned_form);
  EXPECT_TRUE(unowned_form->fields().empty());
  EXPECT_THAT(unowned_form->child_frames(),
              ElementsAre(AllOf(IsToken(frame2_token, -1))));
}

TEST_F(FormCacheBrowserTest, ExtractFormsTwice) {
  LoadHTML(R"(
    <form id="form1">
      <input type="text" name="foo1">
      <input type="text" name="foo2">
      <input type="text" name="foo3">
    </form>
    <input type="text" name="unowned_element">
  )");

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
  EXPECT_TRUE(forms.removed_forms.empty());

  forms = UpdateFormCache();
  // As nothing has changed, there are no new or removed forms.
  EXPECT_TRUE(forms.updated_forms.empty());
  EXPECT_TRUE(forms.removed_forms.empty());
}

TEST_F(FormCacheBrowserTest, ExtractFramesTwice) {
  LoadHTML(R"(
    <form id="form1">
      <iframe></iframe>
    </form>
    <iframe></iframe>
  )");

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
  EXPECT_TRUE(forms.removed_forms.empty());

  forms = UpdateFormCache();
  // As nothing has changed, there are no new or removed forms.
  EXPECT_TRUE(forms.updated_forms.empty());
  EXPECT_TRUE(forms.removed_forms.empty());
}

// TODO(crbug.com/40144964) Adjust expectations when we omit invisible iframes.
TEST_F(FormCacheBrowserTest, ExtractFramesAfterVisibilityChange) {
  LoadHTML(R"(
    <form id="form1">
      <iframe id="frame1" style="display: none;"></iframe>
      <iframe id="frame2" style="display: none;"></iframe>
    </form>
    <iframe id="frame3" style="display: none;"></iframe>
  )");

  WebElement iframe1 = GetElementById(GetDocument(), "frame1");
  WebElement iframe2 = GetElementById(GetDocument(), "frame2");
  WebElement iframe3 = GetElementById(GetDocument(), "frame3");

  auto GetSize = [](const WebElement& element) {
    gfx::Rect bounds = element.BoundsInWidget();
    return bounds.width() * bounds.height();
  };

  ASSERT_LE(GetSize(iframe1), 0);
  ASSERT_LE(GetSize(iframe2), 0);
  ASSERT_LE(GetSize(iframe3), 0);

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();
  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
  EXPECT_TRUE(forms.removed_forms.empty());

  iframe1.SetAttribute("style", "display: block;");
  iframe2.SetAttribute("style", "display: block;");
  iframe3.SetAttribute("style", "display: block;");

  ASSERT_GT(GetSize(iframe1), 0);
  ASSERT_GT(GetSize(iframe2), 0);
  ASSERT_GT(GetSize(iframe3), 0);

  forms = UpdateFormCache();
  EXPECT_TRUE(forms.updated_forms.empty());
  EXPECT_TRUE(forms.removed_forms.empty());

  iframe2.SetAttribute("style", "display: none;");
  iframe3.SetAttribute("style", "display: none;");

  ASSERT_GT(GetSize(iframe1), 0);
  ASSERT_LE(GetSize(iframe2), 0);
  ASSERT_LE(GetSize(iframe3), 0);

  forms = UpdateFormCache();
  EXPECT_TRUE(forms.updated_forms.empty());
  EXPECT_TRUE(forms.removed_forms.empty());
}

TEST_F(FormCacheBrowserTest, ExtractFormsAfterModification) {
  LoadHTML(R"(
    <form id="form1">
      <input type="text" name="foo1">
      <input type="text" name="foo2">
      <input type="text" name="foo3">
    </form>
    <input type="text" name="unowned_element">
  )");

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();
  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
  EXPECT_TRUE(forms.removed_forms.empty());

  // Append an input element to the form and to the list of unowned inputs.
  ExecuteJavaScriptForTests(R"(
    var new_input_1 = document.createElement("input");
    new_input_1.setAttribute("type", "text");
    new_input_1.setAttribute("name", "foo4");

    var form1 = document.getElementById("form1");
    form1.appendChild(new_input_1);

    var new_input_2 = document.createElement("input");
    new_input_2.setAttribute("type", "text");
    new_input_2.setAttribute("name", "unowned_element_2");
    document.body.appendChild(new_input_2);
  )");

  forms = UpdateFormCache();
  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
  EXPECT_TRUE(forms.removed_forms.empty());

  const FormData* form1 = GetFormByName(forms.updated_forms, "form1");
  ASSERT_TRUE(form1);
  EXPECT_EQ(4u, form1->fields().size());

  const FormData* unowned_form = GetFormByName(forms.updated_forms, "");
  ASSERT_TRUE(unowned_form);
  EXPECT_EQ(2u, unowned_form->fields().size());
}

// Tests that correct focus, change and blur events are emitted during the
// autofilling and clearing of the form with an initially focused element.
// TODO: crbug.com/40100455 - Move elsewhere; the test is not about FormCache.
TEST_F(FormCacheBrowserTest,
       VerifyFocusAndBlurEventsAfterAutofillAndClearingWithFocusElement) {
  LoadHTML(R"(<form id='myForm'>
              <label>First Name:</label><input id='fname' name='0'><br>
              <label>Last Name:</label> <input id='lname' name='1'><br>
              </form>)");

  focus_test_utils().SetUpFocusLogging();
  focus_test_utils().FocusElement("fname");

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_THAT(forms.updated_forms,
              UnorderedElementsAre(HasId(FormRendererId()), HasName("myForm")));
  EXPECT_TRUE(forms.removed_forms.empty());

  std::vector<FormFieldData::FillData> values_to_fill;
  values_to_fill.reserve(forms.updated_forms[0].fields().size());
  for (const FormFieldData& field : forms.updated_forms[0].fields()) {
    values_to_fill.emplace_back(field);
  }
  values_to_fill[0].value = u"John";
  values_to_fill[0].is_autofilled = true;
  values_to_fill[1].value = u"Smith";
  values_to_fill[1].is_autofilled = true;

  auto fname = GetFormControlElementById("fname");

  // Simulate filling the form using Autofill.
  form_util::ApplyFieldsAction(
      GetDocument(), values_to_fill, mojom::FormActionType::kFill,
      mojom::ActionPersistence::kFill, GetFieldDataManager());

  // Expected Result in order:
  // - from filling
  //  * Change fname
  //  * Blur fname
  //  * Focus lname
  //  * Change lname
  //  * Blur lname
  //  * Focus fname
  EXPECT_EQ(focus_test_utils().GetFocusLog(GetDocument()), "c0b0f1c1b1f0");
}

// Test that the FormCache does not contain empty forms.
TEST_F(FormCacheBrowserTest, DoNotStoreEmptyForms) {
  LoadHTML(R"(<form></form>)");

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_TRUE(forms.updated_forms.empty());
  EXPECT_TRUE(forms.removed_forms.empty());

  EXPECT_EQ(1u, GetDocument().GetOutermostForms().size());
  EXPECT_EQ(0u, num_extracted_forms());
}

// Test that the FormCache never contains more than |kMaxExtractableFields|
// non-empty extracted forms.
TEST_F(FormCacheBrowserTest, FormCacheSizeUpperBound) {
  // Create a HTML page that contains `kMaxExtractableFields + 1` non-empty
  // forms.
  std::string html;
  for (unsigned int i = 0; i < kMaxExtractableFields + 1; ++i) {
    html += "<form><input></form>";
  }
  LoadHTML(html);

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_EQ(forms.updated_forms.size(), kMaxExtractableFields);
  EXPECT_TRUE(forms.removed_forms.empty());

  EXPECT_EQ(kMaxExtractableFields + 1,
            GetDocument().GetOutermostForms().size());
  EXPECT_EQ(kMaxExtractableFields, num_extracted_forms());
}

// Test that FormCache::UpdateFormCache() limits the number of total fields by
// skipping any additional forms.
TEST_F(FormCacheBrowserTest, FieldLimit) {
  std::string html;
  for (unsigned int i = 0; i < kMaxExtractableFields + 1; ++i) {
    html += "<form><input></form>";
  }
  LoadHTML(html);

  ASSERT_EQ(kMaxExtractableFields + 1,
            GetDocument().GetOutermostForms().size());

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_EQ(kMaxExtractableFields, forms.updated_forms.size());
  EXPECT_TRUE(forms.removed_forms.empty());
}

// Test that FormCache::UpdateFormCache() limits the number of total frames by
// clearing their frames and skipping the then-empty forms.
TEST_F(FormCacheBrowserTest, FrameLimit) {
  std::string html;
  for (unsigned int i = 0; i < kMaxExtractableChildFrames + 1; ++i) {
    html += "<form><iframe></iframe></form>";
  }
  LoadHTML(html);

  ASSERT_EQ(kMaxExtractableChildFrames + 1,
            GetDocument().GetOutermostForms().size());

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_EQ(kMaxExtractableChildFrames, forms.updated_forms.size());
  EXPECT_TRUE(forms.removed_forms.empty());
}

// Test that FormCache::UpdateFormCache() limits the number of total fields and
// total frames:
// - the forms [0, kMaxExtractableChildFrames) should be unchanged,
// - the forms [kMaxExtractableChildFrames, kMaxExtractableFields) should have
//   empty FormData::child_frames,
// - the forms [kMaxExtractableFields, end) should be skipped.
TEST_F(FormCacheBrowserTest, FieldAndFrameLimit) {
  // Ideally, the test would create `kMaxExtractableFields + 1` forms
  //   <form><input><iframe></iframe></form>
  // But that many iframes seem to cause test timeouts. So only the first
  // `kMaxExtractableChildFrames + 1` forms will get an <iframe>.
  ASSERT_LE(kMaxExtractableChildFrames, kMaxExtractableFields);
  constexpr size_t kNumFormsWithFrame = kMaxExtractableChildFrames + 1;
  constexpr size_t kNumFormsWithoutFrame =
      kMaxExtractableFields + 1 - kNumFormsWithFrame;

  std::string html;
  for (size_t i = 0; i < kNumFormsWithFrame; ++i) {
    html += "<form><input><iframe></iframe></form>";
  }
  for (size_t i = 0; i < kNumFormsWithoutFrame; ++i) {
    html += "<form><input></form>";
  }
  LoadHTML(html);

  ASSERT_EQ(kMaxExtractableFields + 1,
            GetDocument().GetElementsByHTMLTagName("form").length());
  ASSERT_EQ(kMaxExtractableFields + 1,
            GetDocument().GetElementsByHTMLTagName("input").length());
  ASSERT_EQ(kMaxExtractableChildFrames + 1,
            GetDocument().GetElementsByHTMLTagName("iframe").length());

  FormCache::UpdateFormCacheResult forms = UpdateFormCache();

  EXPECT_EQ(forms.updated_forms.size(), kMaxExtractableFields);
  EXPECT_THAT(forms.updated_forms,
              Each(Property("fields", &FormData::fields, SizeIs(1))));
  EXPECT_THAT(
      base::span(forms.updated_forms).first<kMaxExtractableChildFrames>(),
      Each(Property("child_frames", &FormData::child_frames, SizeIs(1))));
  EXPECT_THAT(
      base::span(forms.updated_forms).subspan<kMaxExtractableChildFrames>(),
      Each(Property("child_frames", &FormData::child_frames, IsEmpty())));

  EXPECT_THAT(forms.removed_forms, IsEmpty());
}

// Tests that form extraction measures its total time.
TEST_F(FormCacheBrowserTest, UpdateFormCacheMeasuresTotalTime) {
  base::HistogramTester histogram_tester;
  LoadHTML(R"(
    <input>
  )");
  // FormCache::UpdateFormCache() is called by AutofillAgent.
  histogram_tester.ExpectTotalCount("Autofill.TimingPrecise.UpdateFormCache",
                                    1);
  histogram_tester.ExpectTotalCount(
      "Autofill.TimingPrecise.UpdateFormCache.DidDispatchDomContentLoadedEvent",
      1);
  // / On pageload `AutofillAgent::DidDispatchDomContentLoadedEvent()` and
  // `PasswordAutofillAgent::DidFinishLoad()` are called, each triggering form
  //  extraction.
  histogram_tester.ExpectTotalCount("Autofill.TimingPrecise.ExtractFormData",
                                    2);
  histogram_tester.ExpectTotalCount(
      "Autofill.TimingPrecise.ExtractFormData.UpdateFormCache", 1);
}

TEST_F(FormCacheBrowserTest, ClearCache) {
  LoadHTML(R"(<form id="f"><input></form>)");
  FormCache::UpdateFormCacheResult result1 = UpdateFormCache();
  EXPECT_THAT(result1.updated_forms, SizeIs(1));

  // Running UpdateFormCache again without DOM changes returns no updated forms.
  FormCache::UpdateFormCacheResult result2 = UpdateFormCache();
  EXPECT_THAT(result2.updated_forms, IsEmpty());

  // After ClearCache, UpdateFormCache returns the form again as updated.
  ClearCache();
  FormCache::UpdateFormCacheResult result3 = UpdateFormCache();
  EXPECT_THAT(result3.updated_forms, SizeIs(1));
  EXPECT_THAT(result3.removed_forms, IsEmpty());
}

}  // namespace
}  // namespace autofill
