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

#import <ChromeWebView/ChromeWebView.h>
#import <Foundation/Foundation.h>

#import "base/logging.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/scoped_feature_list.h"
#import "components/autofill/ios/form_util/form_activity_params.h"
#import "components/variations/variations_ids_provider.h"
#import "ios/web/common/uikit_ui_util.h"
#import "ios/web_view/public/cwv_global_state.h"
#import "ios/web_view/public/cwv_navigation_delegate.h"
#import "ios/web_view/test/web_view_inttest_base.h"
#import "ios/web_view/test/web_view_test_util.h"
#import "net/base/apple/url_conversions.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "testing/gtest_mac.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#import "url/gurl.h"

using base::test::ios::kWaitForActionTimeout;
using base::test::ios::kWaitForPageLoadTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;
using FieldType = autofill::FormActivityParams::FieldType;

@interface CWVAutofillController (Testing)
- (void)setForceSubmittedByUserForTesting:(BOOL)force;
@end

// A stub object that observes the |webViewDidFinishNavigation| event of
// CWVNavigationDelegate. CWVNavigationDelegate is also used as navigation
// policy decider, so OCMProtocolMock doesn't work here because it implements
// all protocol methods which will return NO and block the navigation.
@interface CWVNavigationPageLoadObserver : NSObject <CWVNavigationDelegate>

// Whether |webViewDidFinishNavigation| has been called. Initiated as NO.
@property(nonatomic, assign, readonly) BOOL pageLoaded;

- (void)webViewDidFinishNavigation:(CWVWebView*)webView;

@end

@implementation CWVNavigationPageLoadObserver

- (void)webViewDidFinishNavigation:(CWVWebView*)webView {
  _pageLoaded = YES;
}

@end

namespace ios_web_view {

namespace {
NSString* const kTestFormName = @"FormName";
NSString* const kTestFormID = @"FormID";
NSString* const kTestNameFieldID = @"nameID";
NSString* const kTestAddressFieldID = @"addressID";
NSString* const kTestCityFieldID = @"cityID";
NSString* const kTestStateFieldID = @"stateID";
NSString* const kTestZipFieldID = @"zipID";
NSString* const kTestFieldType = @"text";
NSString* const kTestAddressFieldValue = @"123 Main Street";
NSString* const kTestCityFieldValue = @"Springfield";
NSString* const kTestNameFieldValue = @"Homer Simpson";
NSString* const kTestStateFieldValue = @"IL";
NSString* const kTestZipFieldValue = @"55123";
NSString* const kTestSubmitID = @"SubmitID";
NSString* const kTestFormHtml =
    [NSString stringWithFormat:
                  // Direct form to about:blank to avoid unnecessary navigation.
                  @"<form action='about:blank' name='%@' id='%@'>"
                   "Name <input type='text' name='name' id='%@'>"
                   "Address <input type='text' name='address' id='%@'>"
                   "City <input type='text' name='city' id='%@'>"
                   "State <input type='text' name='state' id='%@'>"
                   "Zip <input type='text' name='zip' id='%@'>"
                   "<input type='submit' id='%@'/>"
                   "</form>",
                  kTestFormName,
                  kTestFormID,
                  kTestNameFieldID,
                  kTestAddressFieldID,
                  kTestCityFieldID,
                  kTestStateFieldID,
                  kTestZipFieldID,
                  kTestSubmitID];
}  // namespace

// Tests autofill features in CWVWebViews.
class WebViewAutofillTest : public WebViewInttestBase {
 protected:
  WebViewAutofillTest()
      : autofill_controller_delegate_(
            OCMProtocolMock(@protocol(CWVAutofillControllerDelegate))) {
    data_source_ =
        OCMStrictProtocolMock(@protocol(CWVSyncControllerDataSource));
    OCMStub([data_source_ allKnownIdentities]).andReturn(@[]);
    CWVSyncController.dataSource = data_source_;
    autofill_controller_ = web_view_.autofillController;
    autofill_controller_.delegate = autofill_controller_delegate_;
    [autofill_controller_ setForceSubmittedByUserForTesting:YES];
  }

  void TearDown() override {
    EXPECT_OCMOCK_VERIFY(data_source_);
    EXPECT_OCMOCK_VERIFY(autofill_controller_delegate_);
  }

  // Loads a test page with a single form and waits until Autofill has parsed
  // that form.
  [[nodiscard]] bool LoadTestPage() {
    std::string html = base::SysNSStringToUTF8(kTestFormHtml);
    GURL url = GetUrlForPageWithHtmlBody(html);
    [[autofill_controller_delegate_ expect]
        autofillController:autofill_controller_
              didFindForms:[OCMArg any]
                   frameID:[OCMArg any]];
    if (!test::LoadUrl(web_view_, net::NSURLWithGURL(url))) {
      return false;
    }
    bool frame_appeared =
        WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
          return !!GetMainFrameId();
        });
    if (!frame_appeared) {
      return false;
    }
    [autofill_controller_delegate_
        verifyWithDelay:kWaitForActionTimeout.InSecondsF()];
    return true;
  }

  [[nodiscard]] bool SubmitForm() {
    NSString* submit_script =
        [NSString stringWithFormat:@"document.getElementById('%@').click();",
                                   kTestSubmitID];
    NSError* error = nil;
    test::EvaluateJavaScript(web_view_, submit_script, &error);
    return !error;
  }

  [[nodiscard]] bool SetFormFieldValue(NSString* field_id,
                                       NSString* field_value) {
    NSString* set_value_script = [NSString
        stringWithFormat:@"document.getElementById('%@').value = '%@';",
                         field_id, field_value];
    NSError* error = nil;
    test::EvaluateJavaScript(web_view_, set_value_script, &error);
    return !error;
  }

  NSArray<CWVAutofillSuggestion*>* FetchSuggestions(NSString* main_frame_id) {
    __block bool suggestions_fetched = false;
    __block NSArray<CWVAutofillSuggestion*>* fetched_suggestions = nil;
    [autofill_controller_
        fetchSuggestionsForFormWithName:kTestFormName
                        fieldIdentifier:kTestAddressFieldID
                              fieldType:(NSInteger)FieldType::kText
                                frameID:main_frame_id
                      completionHandler:^(
                          NSArray<CWVAutofillSuggestion*>* suggestions) {
                        fetched_suggestions = suggestions;
                        suggestions_fetched = true;
                      }];
    EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
      return suggestions_fetched;
    }));
    return fetched_suggestions;
  }

  NSString* GetMainFrameId() {
    NSString* main_frame_id_script = @"__gCrWeb.getFrameId();";
    return test::EvaluateJavaScript(web_view_, main_frame_id_script);
  }

  bool WaitUntilPageLoaded() {
    CWVNavigationPageLoadObserver* observer =
        [[CWVNavigationPageLoadObserver alloc] init];
    web_view_.navigationDelegate = observer;
    bool result = WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
      return observer.pageLoaded;
    });
    web_view_.navigationDelegate = nil;
    return result;
  }

  CWVAutofillController* autofill_controller_;
  id autofill_controller_delegate_ = nil;
  id<CWVNavigationDelegate> navigation_delegate_ = nil;
  UIView* dummy_super_view_ = nil;
  id<CWVSyncControllerDataSource> data_source_;
};

// Tests that CWVAutofillControllerDelegate receives callbacks.
TEST_F(WebViewAutofillTest, TestDelegateCallbacks) {
  ASSERT_TRUE(variations::VariationsIdsProvider::GetInstance());
  ASSERT_TRUE(test_server_->Start());
  ASSERT_TRUE(LoadTestPage());
  ASSERT_TRUE(SetFormFieldValue(kTestAddressFieldID, kTestAddressFieldValue));

  [[autofill_controller_delegate_ expect]
                 autofillController:autofill_controller_
      didFocusOnFieldWithIdentifier:kTestAddressFieldID
                          fieldType:(NSInteger)FieldType::kText
                           formName:kTestFormName
                            frameID:[OCMArg any]
                              value:kTestAddressFieldValue
                      userInitiated:YES];
  NSString* focus_script =
      [NSString stringWithFormat:
                    @"var el = document.getElementById('%@');"
                    @"el.focus();"
                    @"el.dispatchEvent(new Event('focus', {bubbles: true}));",
                    kTestAddressFieldID];
  NSError* focus_error = nil;
  test::EvaluateJavaScript(web_view_, focus_script, &focus_error);
  ASSERT_FALSE(focus_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];

  [[autofill_controller_delegate_ expect]
                autofillController:autofill_controller_
      didBlurOnFieldWithIdentifier:kTestAddressFieldID
                         fieldType:(NSInteger)FieldType::kText
                          formName:kTestFormName
                           frameID:[OCMArg any]
                             value:kTestAddressFieldValue
                     userInitiated:NO];
  NSString* blur_script = [NSString
      stringWithFormat:@"var event = new Event('blur', {bubbles:true});"
                        "document.getElementById('%@').dispatchEvent(event);",
                       kTestAddressFieldID];
  NSError* blur_error = nil;
  test::EvaluateJavaScript(web_view_, blur_script, &blur_error);
  ASSERT_FALSE(blur_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];

  [[autofill_controller_delegate_ expect]
                 autofillController:autofill_controller_
      didInputInFieldWithIdentifier:kTestAddressFieldID
                          fieldType:(NSInteger)FieldType::kText
                           formName:kTestFormName
                            frameID:[OCMArg any]
                              value:kTestAddressFieldValue
                      userInitiated:NO];
  // The 'input' event listener defined in form.js is only called during the
  // bubbling phase.
  NSString* input_script = [NSString
      stringWithFormat:@"var event = new Event('input', {'bubbles': true});"
                        "document.getElementById('%@').dispatchEvent(event);",
                       kTestAddressFieldID];
  NSError* input_error = nil;
  test::EvaluateJavaScript(web_view_, input_script, &input_error);
  ASSERT_FALSE(input_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];

  [[[autofill_controller_delegate_ expect] ignoringNonObjectArgs]
         autofillController:autofill_controller_
      didSubmitFormWithName:kTestFormName
                    frameID:[OCMArg any]
             perfectFilling:[OCMArg any]];

  [[[autofill_controller_delegate_ expect] ignoringNonObjectArgs]
         autofillController:autofill_controller_
      didSubmitFormWithName:kTestFormName
                    frameID:[OCMArg any]
              userInitiated:[OCMArg any]
             perfectFilling:[OCMArg any]];
  // The 'submit' event listener defined in form.js is only called during the
  // bubbling phase.
  NSString* submit_script = [NSString
      stringWithFormat:@"var event = new Event('submit', {'bubbles': true});"
                        "document.getElementById('%@').dispatchEvent(event);",
                       kTestFormID];
  NSError* submit_error = nil;
  test::EvaluateJavaScript(web_view_, submit_script, &submit_error);
  ASSERT_FALSE(submit_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];
}

// Tests that CWVAutofillController can fetch and fill suggestions.
TEST_F(WebViewAutofillTest, TestSuggestionFetchFill) {
  ASSERT_TRUE(test_server_->Start());
  ASSERT_TRUE(LoadTestPage());
  ASSERT_TRUE(SetFormFieldValue(kTestNameFieldID, kTestNameFieldValue));
  ASSERT_TRUE(SetFormFieldValue(kTestAddressFieldID, kTestAddressFieldValue));
  ASSERT_TRUE(SetFormFieldValue(kTestStateFieldID, kTestStateFieldValue));
  ASSERT_TRUE(SetFormFieldValue(kTestCityFieldID, kTestCityFieldValue));
  ASSERT_TRUE(SetFormFieldValue(kTestZipFieldID, kTestZipFieldValue));

  // Stub the confirm save callback to save the new profile right away.
  void (^invocation_handler)(NSInvocation*) = ^(NSInvocation* invocation) {
    void (^decision_handler)(CWVAutofillProfileUserDecision);
    [invocation getArgument:&decision_handler atIndex:5];
    decision_handler(CWVAutofillProfileUserDecisionAccepted);
  };
  [[[autofill_controller_delegate_ stub] andDo:invocation_handler]
                    autofillController:autofill_controller_
      confirmSaveForNewAutofillProfile:[OCMArg any]
                            oldProfile:[OCMArg any]
                       decisionHandler:[OCMArg any]];
  ASSERT_TRUE(SubmitForm());

  // Wait for about:blank to be loaded after <form> submitted.
  ASSERT_TRUE(WaitUntilPageLoaded());

  ASSERT_TRUE(LoadTestPage());

  __block NSString* main_frame_id = nil;

  // The input element needs to be focused before suggestions can be fetched.
  [[autofill_controller_delegate_ expect]
                 autofillController:autofill_controller_
      didFocusOnFieldWithIdentifier:kTestAddressFieldID
                          fieldType:(NSInteger)FieldType::kText
                           formName:kTestFormName
                            frameID:[OCMArg checkWithBlock:^BOOL(id frameId) {
                              main_frame_id = frameId;
                              return frameId != nil;
                            }]
                              value:[OCMArg any]
                      userInitiated:YES];
  NSString* focus_script =
      [NSString stringWithFormat:
                    @"var el = document.getElementById('%@');"
                    @"el.focus();"
                    @"el.dispatchEvent(new Event('focus', {bubbles: true}));",
                    kTestAddressFieldID];
  NSError* focus_error = nil;
  test::EvaluateJavaScript(web_view_, focus_script, &focus_error);
  ASSERT_TRUE(!focus_error);
  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];

  NSArray<CWVAutofillSuggestion*>* fetched_suggestions =
      FetchSuggestions(main_frame_id);
  ASSERT_EQ(1U, fetched_suggestions.count);
  CWVAutofillSuggestion* fetched_suggestion = fetched_suggestions.firstObject;
  EXPECT_NSEQ(kTestAddressFieldValue, fetched_suggestion.value);
  EXPECT_NSEQ(kTestFormName, fetched_suggestion.formName);
  EXPECT_NSEQ(main_frame_id, fetched_suggestion.frameID);

  [autofill_controller_ acceptSuggestion:fetched_suggestion
                                 atIndex:0
                       completionHandler:nil];
  NSString* filled_script =
      [NSString stringWithFormat:@"document.getElementById('%@').value",
                                 kTestAddressFieldID];
  __block NSError* filled_error = nil;
  EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
    NSString* filled_value =
        test::EvaluateJavaScript(web_view_, filled_script, &filled_error);
    // If there is an error, early return so the ASSERT catch the error.
    LOG(INFO) << base::SysNSStringToUTF8(filled_value);
    LOG(INFO) << base::SysNSStringToUTF8(fetched_suggestion.value);
    if (filled_error) {
      return true;
    }
    return [fetched_suggestion.value isEqualToString:filled_value];
  }));
  EXPECT_FALSE(filled_error);
}

// Tests that submitting a form in a child frame reports the correct frame ID.
TEST_F(WebViewAutofillTest, TestChildFrameSubmission) {
  ASSERT_TRUE(test_server_->Start());

  std::string child_html =
      "<html><body>"
      "<h1>Child Frame</h1>"
      "<form id='child_form' action='about:blank' method='POST'>"
      "<label for='cc'>Credit Card Number</label>"
      "<input type='text' id='cc' name='cc_number' autocomplete='cc-number'>"
      "<input type='submit' id='child_submit' value='Submit Child Form'>"
      "</form>"
      "</body></html>";
  GURL child_url = GetUrlForPageWithHtmlBody(child_html);

  std::string main_html =
      base::StringPrintf("<html><body>"
                         "<h1>Main Frame</h1>"
                         "<iframe id='child_frame' src='%s'></iframe>"
                         "</body></html>",
                         child_url.spec().c_str());
  GURL main_url = GetUrlForPageWithHtmlBody(main_html);

  ASSERT_TRUE(test::LoadUrl(web_view_, net::NSURLWithGURL(main_url)));

  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
    return !!GetMainFrameId();
  }));

  // Expectation:
  [[autofill_controller_delegate_ expect]
         autofillController:autofill_controller_
      didSubmitFormWithName:[OCMArg any]
                    frameID:[OCMArg any]
             perfectFilling:[OCMArg any]];  // Relax just in case.

  [[autofill_controller_delegate_ expect]
         autofillController:autofill_controller_
      didSubmitFormWithName:[OCMArg any]
                    frameID:[OCMArg any]
              userInitiated:YES             // Usually YES if clicked.
             perfectFilling:[OCMArg any]];  // Relax just in case.

  // Wait for child frame and submit.
  NSString* submit_script =
      @"var f = document.getElementById('child_frame');"
      @"if (f.contentWindow.document.getElementById('child_submit')) {"
      @"  f.contentWindow.document.getElementById('child_submit').click();"
      @"  'clicked';"
      @"} else {"
      @"  'not_ready';"
      @"}";

  __block NSError* err = nil;
  ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool {
    NSString* res = test::EvaluateJavaScript(web_view_, submit_script, &err);
    return [res isEqualToString:@"clicked"];
  })) << base::SysNSStringToUTF8(err.debugDescription);

  [autofill_controller_delegate_
      verifyWithDelay:kWaitForActionTimeout.InSecondsF()];
}

}  // namespace ios_web_view
