// Copyright 2025 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/ios/browser/autofill_driver_ios.h"

#import <memory>
#import <optional>
#import <string>

#import "base/test/bind.h"
#import "base/test/mock_callback.h"
#import "base/test/test_future.h"
#import "components/autofill/core/browser/test_utils/autofill_form_test_utils.h"
#import "components/autofill/core/browser/test_utils/autofill_test_utils.h"
#import "components/autofill/core/common/autofill_test_utils.h"
#import "components/autofill/core/common/form_data.h"
#import "components/autofill/core/common/form_field_data.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/browser/autofill_driver_ios_bridge.h"
#import "components/autofill/ios/browser/autofill_driver_ios_factory.h"
#import "components/autofill/ios/browser/autofill_java_script_feature.h"
#import "components/autofill/ios/browser/test_autofill_client_ios.h"
#import "components/autofill/ios/common/features.h"
#import "components/autofill/ios/form_util/child_frame_registrar.h"
#import "ios/web/public/test/fakes/fake_web_frame.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_test.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"

// AutofillDriverIosBridge used for testing. Provides a simple implementation of
// the methods that are used during testing, e.g. call the completion block upon
// calling -fetchFormsFiltered.
@interface FakeAutofillDriverIOSBridge : NSObject <AutofillDriverIOSBridge>

- (instancetype)init;
- (void)setForms:(std::vector<autofill::FormData>)forms;
- (void)setFetchFormsCompletionHandler:
    (base::RepeatingCallback<void(FormFetchCompletion)>)handler;
- (const std::string&)lastScrolledFrameId;
@property(nonatomic, assign) autofill::FieldRendererId lastScrolledField;

@end

@implementation FakeAutofillDriverIOSBridge {
  std::vector<autofill::FormData> _forms;
  base::RepeatingCallback<void(FormFetchCompletion)> _fetchHandler;
  std::string _lastScrolledFrameId;
}

- (instancetype)init {
  if ((self = [super init])) {
    _forms = {};
  }
  return self;
}

- (void)setForms:(std::vector<autofill::FormData>)forms {
  _forms = std::move(forms);
}

- (void)setFetchFormsCompletionHandler:
    (base::RepeatingCallback<void(FormFetchCompletion)>)handler {
  _fetchHandler = std::move(handler);
}

- (void)fillData:(const std::vector<autofill::FormFieldData::FillData>&)fields
           inFrame:(web::WebFrame*)frame
    withActionType:(autofill::mojom::FormActionType)actionType {
}
- (void)fillSpecificFormField:(const autofill::FieldRendererId&)field
                    withValue:(const std::u16string)value
                   actionType:(autofill::mojom::FieldActionType)actionType
                      inFrame:(web::WebFrame*)frame {
}
- (void)handleParsedForms:
            (const std::vector<raw_ref<const autofill::FormStructure>>&)forms
                  inFrame:(web::WebFrame*)frame {
}
- (void)fillFormDataPredictions:
            (const std::vector<autofill::FormDataPredictions>&)forms
                        inFrame:(web::WebFrame*)frame {
}
- (void)scanFormsInWebState:(web::WebState*)webState
                    inFrame:(web::WebFrame*)webFrame {
}
- (void)notifyFormsSeen:(std::vector<autofill::FormData>)updatedForms
                inFrame:(web::WebFrame*)frame {
}
- (void)scrollFieldIntoView:(const autofill::FieldRendererId&)field
                    inFrame:(web::WebFrame*)frame {
  _lastScrolledField = field;
  _lastScrolledFrameId = frame ? frame->GetFrameId() : "";
}
- (const std::string&)lastScrolledFrameId {
  return _lastScrolledFrameId;
}
- (void)fetchFormsFiltered:(std::optional<std::u16string>)formNameFilter
                   inFrame:(web::WebFrame*)frame
         completionHandler:(FormFetchCompletion)completionHandler {
  if (_fetchHandler) {
    _fetchHandler.Run(std::move(completionHandler));
  } else {
    std::move(completionHandler).Run(_forms);
  }
}

@end

namespace autofill {

namespace {

using testing::Eq;
using testing::Optional;
using testing::Property;

class AutofillDriverIOSTest : public web::WebTest {
 protected:
  void SetUp() override {
    web::WebTest::SetUp();

    OverrideJavaScriptFeatures(
        {autofill::AutofillJavaScriptFeature::GetInstance()});

    GURL url("https://example.com");
    web_state_.SetCurrentURL(url);
    web_state_.SetBrowserState(GetBrowserState());
    web_state_.SetContentIsHTML(true);

    // Define frames managers for both content worlds so that the
    // `ChildFrameRegistrar` below can be created.
    for (auto content_world : {web::ContentWorld::kIsolatedWorld,
                               web::ContentWorld::kPageContentWorld}) {
      auto frames_manager = std::make_unique<web::FakeWebFramesManager>();
      web_state_.SetWebFramesManager(content_world, std::move(frames_manager));
    }

    web_frames_manager_ = static_cast<web::FakeWebFramesManager*>(
        web_state_.GetWebFramesManager(AutofillJavaScriptFeature::GetInstance()
                                           ->GetSupportedContentWorld()));

    web::WebFrame* main_frame = CreateFrame(url, /*main_frame=*/true);
    web::WebFrame* iframe = CreateFrame(url, /*main_frame=*/false);

    // Create the main Autofill classes.
    test_bridge_ = [[FakeAutofillDriverIOSBridge alloc] init];
    autofill_client_ =
        std::make_unique<TestAutofillClientIOS>(&web_state_, test_bridge_);
    main_frame_driver_ =
        AutofillDriverIOS::FromWebStateAndWebFrame(&web_state_, main_frame);
    iframe_driver_ =
        AutofillDriverIOS::FromWebStateAndWebFrame(&web_state_, iframe);

    ASSERT_TRUE(main_frame_driver_ && main_frame_driver_->GetFrameToken());
    ASSERT_TRUE(iframe_driver_ && iframe_driver_->GetFrameToken());
    ASSERT_NE(iframe_driver_->GetFrameToken(),
              main_frame_driver_->GetFrameToken());

    // Register the remote-local frame token mapping for the iframe driver so
    // that the main frame driver can accurately set itself as a parent of the
    // iframe driver.
    registrar()->RegisterMapping(GetRemoteFrameToken(iframe_driver_),
                                 iframe_driver_->GetFrameToken());
  }

  AutofillDriverIOS* main_frame_driver() { return main_frame_driver_; }
  AutofillDriverIOS* iframe_driver() { return iframe_driver_; }
  FakeAutofillDriverIOSBridge* bridge() { return test_bridge_; }

  FormData MakeForm(bool main_frame) {
    FormGlobalId form_id{main_frame ? main_frame_driver()->GetFrameToken()
                                    : iframe_driver()->GetFrameToken(),
                         test::MakeFormRendererId()};
    FormData form;
    form.set_renderer_id(form_id.renderer_id);
    form.set_host_frame(form_id.frame_token);

    // Add a field to the form to make it non-trivial.
    FormFieldData field = test::GetFormFieldData({});
    field.set_host_form_id(form.renderer_id());
    field.set_host_frame(form.host_frame());
    field.set_value(main_frame ? u"Main frame field" : u"Iframe field");
    form.set_fields({std::move(field)});

    if (main_frame) {
      std::vector<FrameTokenWithPredecessor> child_frames(1);
      child_frames.back().token = GetRemoteFrameToken(iframe_driver());
      form.set_child_frames(std::move(child_frames));
    }
    return form;
  }

 private:
  web::WebFrame* CreateFrame(GURL url, bool main_frame) {
    auto frame = main_frame ? web::FakeWebFrame::CreateMainWebFrame(url)
                            : web::FakeWebFrame::CreateChildWebFrame(url);
    frame->set_browser_state(GetBrowserState());
    web::WebFrame* frame_ptr = frame.get();
    web_frames_manager_->AddWebFrame(std::move(frame));
    return frame_ptr;
  }

  autofill::ChildFrameRegistrar* registrar() {
    return autofill::ChildFrameRegistrar::GetOrCreateForWebState(&web_state_);
  }

  RemoteFrameToken GetRemoteFrameToken(AutofillDriver* driver) {
    return RemoteFrameToken(driver->GetFrameToken().value());
  }

  test::AutofillUnitTestEnvironment autofill_test_environment_;
  web::FakeWebState web_state_;
  raw_ptr<web::FakeWebFramesManager> web_frames_manager_ = nullptr;
  FakeAutofillDriverIOSBridge* test_bridge_ = nullptr;
  std::unique_ptr<TestAutofillClientIOS> autofill_client_;
  raw_ptr<AutofillDriverIOS> main_frame_driver_ = nullptr;
  raw_ptr<AutofillDriverIOS> iframe_driver_ = nullptr;
};

// Test when ExtractFormWithField() is called and where the form is found.
TEST_F(AutofillDriverIOSTest, ExtractFormWithField_FormFound) {
  FormData iframe_form = MakeForm(/*main_frame=*/false);
  FormData main_frame_form = MakeForm(/*main_frame=*/true);

  // Notify the drivers of seeing these forms so that the router can register
  // them along with their frame.
  main_frame_driver()->FormsSeen({main_frame_form}, /*removed_forms=*/{});
  iframe_driver()->FormsSeen({iframe_form}, /*removed_forms=*/{});
  ASSERT_EQ(iframe_driver()->GetParent(), main_frame_driver());

  // Update the value of the fields in both forms to check the result of the
  // extraction later.
  auto update_form = [](FormData& form) {
    std::vector<FormFieldData> fields = form.ExtractFields();
    fields.back().set_value(fields.back().value() + u" extracted");
    form.set_fields(std::move(fields));
  };
  update_form(main_frame_form);
  update_form(iframe_form);

  // Setup the forms that the bridge should return upon calling
  // fetchFormsFiltered(). Those should be the two forms created above.
  std::vector<FormData> bridge_result{iframe_form, main_frame_form};
  [bridge() setForms:(bridge_result)];

  base::MockCallback<
      base::OnceCallback<void(AutofillDriver*, const std::optional<FormData>&)>>
      final_callback;
  // The callback must be called with the result of the extraction operation,
  // this should be the browser form containing the iframe form, with the
  // updated version of the iframe form.
  EXPECT_CALL(
      final_callback,
      Run(main_frame_driver(),
          Optional(
              AllOf(Property(&FormData::global_id, main_frame_form.global_id()),
                    Property(&FormData::fields,
                             ElementsAre(Property(&FormFieldData::value,
                                                  u"Iframe field extracted"),
                                         Property(&FormFieldData::value,
                                                  u"Main frame field")))))));
  main_frame_driver()->ExtractFormWithField(iframe_form.fields()[0].global_id(),
                                            final_callback.Get());
}

// Test the case where the bridge returns forms, but not the one we want.
TEST_F(AutofillDriverIOSTest, ExtractFormWithField_FormNotFound) {
  FieldGlobalId field_id{main_frame_driver()->GetFrameToken(),
                         test::MakeFieldRendererId()};
  FormData form_not_to_find = MakeForm(/*main_frame=*/true);

  // Setup the forms that the bridge should return upon calling
  // fetchFormsFiltered().
  std::vector<FormData> bridge_result = {form_not_to_find};
  [bridge() setForms:(bridge_result)];

  // Notify the driver of seeing this form so that the router can register it
  // along with its frame.
  main_frame_driver()->FormsSeen({form_not_to_find}, /*removed_forms=*/{});

  // Expect that `final_callback` will be called with a null form, since the
  // bridge couldn't find a form with the corresponding renderer id.
  base::MockCallback<
      base::OnceCallback<void(AutofillDriver*, const std::optional<FormData>&)>>
      final_callback;
  EXPECT_CALL(final_callback, Run(nullptr, Eq(std::nullopt)));
  main_frame_driver()->ExtractFormWithField(field_id, final_callback.Get());
}

// Tests that ScanForms works with scan throttling.
TEST_F(AutofillDriverIOSTest, ScanForms_Throttling) {
  // Use a custom completion handler to track calls.
  int fetch_calls = 0;
  auto callback =
      base::BindLambdaForTesting([&](FormFetchCompletion completion) {
        fetch_calls++;
        std::move(completion).Run({});
      });
  [bridge() setFetchFormsCompletionHandler:std::move(callback)];

  // Trigger a throttled scan.
  main_frame_driver()->ScanForms(/*immediately=*/false);

  // Verify that the fetch was NOT executed immediately.
  EXPECT_EQ(0, fetch_calls);

  // Trigger an immediate scan (forcing first scan behavior).
  main_frame_driver()->ScanForms(/*immediately=*/true);

  // Verify that the fetch WAS executed immediately.
  EXPECT_EQ(1, fetch_calls);
}

TEST_F(AutofillDriverIOSTest, Unregister_InvalidatesWeakPtrs) {
  auto weak_ptr = main_frame_driver()->GetWeakPtr();
  ASSERT_TRUE(weak_ptr);
  main_frame_driver()->Unregister();
  EXPECT_FALSE(weak_ptr);
}

// Tests that ScrollFieldIntoView on the main frame driver forwards the field
// renderer ID and main frame to the bridge.
TEST_F(AutofillDriverIOSTest, ScrollFieldIntoView_MainFrame) {
  FormData main_frame_form = MakeForm(/*main_frame=*/true);
  main_frame_driver()->FormsSeen({main_frame_form}, /*removed_forms=*/{});

  FieldRendererId field_id(123);
  FieldGlobalId global_id(main_frame_driver()->GetFrameToken(), field_id);

  main_frame_driver()->ScrollFieldIntoView(global_id);

  EXPECT_EQ(bridge().lastScrolledField, field_id);
  EXPECT_EQ(bridge().lastScrolledFrameId,
            main_frame_driver()->web_frame()->GetFrameId());
}

// Tests that ScrollFieldIntoView on a child frame is routed correctly.
TEST_F(AutofillDriverIOSTest, ScrollFieldIntoView_Iframe) {
  FormData iframe_form = MakeForm(/*main_frame=*/false);
  FormData main_frame_form = MakeForm(/*main_frame=*/true);
  main_frame_driver()->FormsSeen({main_frame_form}, /*removed_forms=*/{});
  iframe_driver()->FormsSeen({iframe_form}, /*removed_forms=*/{});

  FieldRendererId field_id(456);
  FieldGlobalId global_id(iframe_driver()->GetFrameToken(), field_id);

  main_frame_driver()->ScrollFieldIntoView(global_id);

  EXPECT_EQ(bridge().lastScrolledField, field_id);
  EXPECT_EQ(bridge().lastScrolledFrameId,
            iframe_driver()->web_frame()->GetFrameId());
}

}  // namespace

}  // namespace autofill
