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

#import "base/functional/bind.h"
#import "base/ios/ios_util.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "components/strings/grit/components_strings.h"
#import "components/url_formatter/elide_url.h"
#import "ios/chrome/browser/overlays/model/public/web_content_area/alert_constants.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_actions.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/chrome/test/scoped_eg_synchronization_disabler.h"
#import "ios/testing/earl_grey/disabled_test_macros.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/testing/earl_grey/matchers.h"
#import "ios/web/public/test/element_selector.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "net/test/embedded_test_server/request_handler_util.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"

using chrome_test_util::ButtonWithAccessibilityLabel;
using chrome_test_util::OKButton;
using chrome_test_util::SettingsDoneButton;
using chrome_test_util::WebViewMatcher;

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

namespace {

// Body script for test page that shows an alert with kAlertMessage and returns
// kAlertResult.
const char kAlertURLPath[] = "/alert";
const char kAlertMessage[] = "This is a JavaScript alert.";
const char kAlertResult[] = "JAVASCRIPT ALERT WAS DISMISSED";
const char kAlertScriptBodyFormat[] = "alert(\"%s\");"
                                      "return \"%s\";";
std::string GetAlertScriptBody() {
  return base::StringPrintf(kAlertScriptBodyFormat, kAlertMessage,
                            kAlertResult);
}

// Body script for test page that shows a confirmation with kConfirmationMessage
// and returns kConfirmationResultOK or kConfirmationResultCancelled, depending
// on whether the OK or Cancel button was tapped.
const char kConfirmationURLPath[] = "/confirm";
const char kConfirmationMessage[] = "This is a JavaScript confirmation.";
const char kConfirmationResultOK[] = "Okay";
const char kConfirmationResultCancelled[] = "Cancelled";
const char kConfirmationScriptBodyFormat[] = "if (confirm(\"%s\") == true) {"
                                             "  return \"%s\";"
                                             "} else {"
                                             "  return \"%s\";"
                                             "}";
std::string GetConfirmationScriptBody() {
  return base::StringPrintf(kConfirmationScriptBodyFormat, kConfirmationMessage,
                            kConfirmationResultOK,
                            kConfirmationResultCancelled);
}

// Body script for test page that shows a prompt with kPromptMessage
// and returns kPromptTestUserInput or kPromptResultCancelled, depending on
// whether the OK or Cancel button was tapped.
const char kPromptURLPath[] = "/prompt";
const char kPromptMessage[] = "This is a JavaScript prompt.";
const char kPromptTestUserInput[] = "test";
const char kPromptResultCancelled[] = "Cancelled";
const char kPromptTestScriptBodyFormat[] = "var input = prompt(\"%s\");"
                                           "if (input != null) {"
                                           "  return input;"
                                           "} else {"
                                           "  return \"%s\";"
                                           "}";
std::string GetPromptScriptBody() {
  return base::StringPrintf(kPromptTestScriptBodyFormat, kPromptMessage,
                            kPromptResultCancelled);
}

// Script to inject that will show a JavaScript alert in a loop 20 times, then
// returns kAlertLoopFinishedText.
const char kAlertLoopURLPath[] = "/loop";
const char kAlertLoopMessage[] = "This is a looped alert.";
const char kAlertLoopFinishedText[] = "Loop Finished";
const char kAlertLoopScriptBodyFormat[] = "for (i = 0; i < 20; ++i) {"
                                          "  alert(\"%s\");"
                                          "}"
                                          "return \"%s\";";
std::string GetAlertLoopScriptBody() {
  return base::StringPrintf(kAlertLoopScriptBodyFormat, kAlertLoopMessage,
                            kAlertLoopFinishedText);
}

// HTTP server constants.

// The URL path for a test page which has a link to show JavaScript dialogs. The
// page calls the provided script with a timeout so that the JavaScript used to
// simulate the link tap can return while the dialogs are displayed.
const char kTestPageLinkID[] = "show-dialog";
const char kTestPageContentsFormat[] =
    "<!DOCTYPE html><html>"
    "  <body>"
    "    <script> function dialogScript() { %s } </script>"
    "    <script>"
    "      function runDialogScript() {"
    "        var result = dialogScript();"
    "        document.getElementById(\"dialog-result\").innerHTML = result;"
    "      }"
    "    </script>"
    "    <a onclick=\"setTimeout(runDialogScript, 0)\" id=\"%s\" "
    "       href=\"javascript:void(0);\">"
    "      Show Dialog."
    "    </a>"
    "    <p id=\"dialog-result\"></p>"
    "  </body>"
    "</html>";
std::string GetTestPageContents(std::string script_body) {
  return base::StringPrintf(kTestPageContentsFormat, script_body.c_str(),
                            kTestPageLinkID);
}
// The URL path for a page that shows an alert onload.
const char kOnLoadURLPath[] = "/onload";
const char kOnLoadAlertMessage[] = "onload Alert";
const char kOnLoadContentsFormat[] =
    "<!DOCTYPE html><html><body onload=\"alert('%s')\"></body></html>";
std::string GetOnLoadPageContents() {
  return base::StringPrintf(kOnLoadContentsFormat, kOnLoadAlertMessage);
}
// The URL path for a page with a link to kOnLoadURLPath.
const char kLinkPageURLPath[] = "/link";
const char kLinkPageContentsFormat[] =
    "<!DOCTYPE html><html><body><a id=\"%s\" href=\"%s\">%s</a></body></html>";
const char kLinkPageLinkText[] = "LINK TO ONLOAD ALERT PAGE";
const char kLinkID[] = "link-id";
std::string GetLinkPageContents(const GURL& on_load_url) {
  return base::StringPrintf(kLinkPageContentsFormat, kLinkID,
                            on_load_url.spec().c_str(), kLinkPageLinkText);
}

// Helper function that returns a text/html HttpResponse using `content`.
std::unique_ptr<net::test_server::HttpResponse> GetHttpResponseWithContent(
    const std::string& content) {
  std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
      new net::test_server::BasicHttpResponse);
  http_response->set_content_type("text/html");
  http_response->set_content(content);
  return std::move(http_response);
}

// net::EmbeddedTestServer handler for the test page.
std::unique_ptr<net::test_server::HttpResponse> LoadTestPage(
    std::string script_body,
    const net::test_server::HttpRequest& request) {
  return GetHttpResponseWithContent(GetTestPageContents(script_body));
}
// net::EmbeddedTestServer handler for kOnLoadURLPath.
std::unique_ptr<net::test_server::HttpResponse> LoadPageWithOnLoadAlert(
    const net::test_server::HttpRequest& request) {
  return GetHttpResponseWithContent(GetOnLoadPageContents());
}
// net::EmbeddedTestServer handler for kLinkPageURLPath.
std::unique_ptr<net::test_server::HttpResponse> LoadPageWithLinkToOnLoadPage(
    ChromeTestCase* test_case,
    const net::test_server::HttpRequest& request) {
  GURL on_load_page_url = test_case.testServer->GetURL(kOnLoadURLPath);
  return GetHttpResponseWithContent(GetLinkPageContents(on_load_page_url));
}

// Waits for a JavaScript dialog from `url` with `message` to be shown or
// hidden.
void WaitForJavaScriptDialog(const GURL& url,
                             const char* message,
                             bool visible,
                             bool is_main_frame) {
  // Wait for the JavaScript dialog identifier.
  id<GREYMatcher> visibility_matcher = visible ? grey_notNil() : grey_nil();
  ConditionBlock condition = ^{
    NSError* error = nil;
    id<GREYMatcher> dialog_matcher =
        grey_accessibilityID(kJavaScriptDialogAccessibilityIdentifier);
    [[EarlGrey selectElementWithMatcher:dialog_matcher]
        assertWithMatcher:visibility_matcher
                    error:&error];
    return !error;
  };
  NSString* error_text = visible ? @"JavaScript dialog was not shown."
                                 : @"JavaScript dialog was not hidden.";
  GREYAssert(WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, condition),
             error_text);

  // Check the title.  Non-modal main-frame dialogs do not have a title label.
  if (!is_main_frame) {
    std::u16string url_string = url_formatter::FormatUrlForSecurityDisplay(
        url, url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS);
    NSString* expected_title =
        l10n_util::GetNSStringF(IDS_JAVASCRIPT_MESSAGEBOX_TITLE, url_string);
    id<GREYMatcher> title_matcher =
        chrome_test_util::StaticTextWithAccessibilityLabel(expected_title);
    [[EarlGrey selectElementWithMatcher:title_matcher]
        assertWithMatcher:visibility_matcher];
  }

  // Check the message.
  id<GREYMatcher> message_matcher =
      chrome_test_util::StaticTextWithAccessibilityLabel(@(message));
  [[EarlGrey selectElementWithMatcher:message_matcher]
      assertWithMatcher:visibility_matcher];
}

// Types `input` in the prompt.
void TypeInPrompt(NSString* input) {
  id<GREYMatcher> text_field_matcher = grey_allOf(
      grey_kindOfClass([UITextField class]),
      grey_accessibilityID(kJavaScriptDialogTextFieldAccessibilityIdentifier),
      nil);
  [[EarlGrey selectElementWithMatcher:text_field_matcher]
      performAction:grey_replaceText(input)];
}

void TapCancel() {
  [[EarlGrey selectElementWithMatcher:chrome_test_util::CancelButton()]
      performAction:grey_tap()];
}

void TapSuppressDialogsButton() {
  id<GREYMatcher> suppress_dialogs_button =
      chrome_test_util::ButtonWithAccessibilityLabelId(
          IDS_IOS_JAVA_SCRIPT_DIALOG_BLOCKING_BUTTON_TEXT);
  [[EarlGrey selectElementWithMatcher:suppress_dialogs_button]
      performAction:grey_tap()];
}

}  // namespace

@interface JavaScriptDialogTestCase : ChromeTestCase
@end

@implementation JavaScriptDialogTestCase

#pragma mark - ChromeTestCase

- (void)setUp {
  [super setUp];

  self.testServer->RegisterDefaultHandler(base::BindRepeating(
      net::test_server::HandlePrefixedRequest, kAlertURLPath,
      base::BindRepeating(&LoadTestPage, GetAlertScriptBody())));
  self.testServer->RegisterDefaultHandler(base::BindRepeating(
      net::test_server::HandlePrefixedRequest, kConfirmationURLPath,
      base::BindRepeating(&LoadTestPage, GetConfirmationScriptBody())));
  self.testServer->RegisterDefaultHandler(base::BindRepeating(
      net::test_server::HandlePrefixedRequest, kPromptURLPath,
      base::BindRepeating(&LoadTestPage, GetPromptScriptBody())));
  self.testServer->RegisterDefaultHandler(base::BindRepeating(
      net::test_server::HandlePrefixedRequest, kAlertLoopURLPath,
      base::BindRepeating(&LoadTestPage, GetAlertLoopScriptBody())));
  self.testServer->RegisterDefaultHandler(base::BindRepeating(
      net::test_server::HandlePrefixedRequest, kOnLoadURLPath,
      base::BindRepeating(&LoadPageWithOnLoadAlert)));
  self.testServer->RegisterDefaultHandler(base::BindRepeating(
      net::test_server::HandlePrefixedRequest, kLinkPageURLPath,
      base::BindRepeating(&LoadPageWithLinkToOnLoadPage, self)));
  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start");
}

- (void)tearDownHelper {
  NSError* errorOK = nil;
  NSError* errorCancel = nil;

  // Dismiss JavaScript alert by tapping Cancel.
  [[EarlGrey selectElementWithMatcher:chrome_test_util::CancelButton()]
      performAction:grey_tap()
              error:&errorCancel];
  // Dismiss JavaScript alert by tapping OK.
  id<GREYMatcher> OKButton =
      chrome_test_util::ButtonWithAccessibilityLabelId(IDS_OK);
  [[EarlGrey selectElementWithMatcher:OKButton] performAction:grey_tap()
                                                        error:&errorOK];

  if (!errorOK || !errorCancel) {
    GREYFail(@"There are still alerts");
  }
  [super tearDownHelper];
}

#pragma mark - Tests

// Tests that an alert is shown, and that the completion block is called.
- (void)testShowJavaScriptAlert {
  // Load the alert test page and tap on the link.
  const GURL kURL = self.testServer->GetURL(kAlertURLPath);
  [ChromeEarlGrey loadURL:kURL];
  [ChromeEarlGrey waitForWebStateContainingElement:
                      [ElementSelector selectorWithElementID:kTestPageLinkID]];
  [ChromeEarlGrey tapWebStateElementWithID:@(kTestPageLinkID)];
  WaitForJavaScriptDialog(kURL, kAlertMessage, /*visible=*/true,
                          /*is_main_frame=*/true);

  // Tap the OK button to close the alert.
  [[EarlGrey selectElementWithMatcher:OKButton()] performAction:grey_tap()];
  WaitForJavaScriptDialog(kURL, kAlertMessage, /*visible=*/false,
                          /*is_main_frame=*/true);

  // Wait for the expected text to be added to the test page.
  [ChromeEarlGrey waitForWebStateContainingText:kAlertResult];
}

// Tests that a confirmation dialog is shown, and that the completion block is
// called with the correct value when the OK button is tapped.
- (void)testShowJavaScriptConfirmationOK {
  // Load the confirmation test page and tap on the link.
  const GURL kURL = self.testServer->GetURL(kConfirmationURLPath);
  [ChromeEarlGrey loadURL:kURL];
  [ChromeEarlGrey waitForWebStateContainingElement:
                      [ElementSelector selectorWithElementID:kTestPageLinkID]];
  [ChromeEarlGrey tapWebStateElementWithID:@(kTestPageLinkID)];
  WaitForJavaScriptDialog(kURL, kConfirmationMessage, /*visible=*/true,
                          /*is_main_frame=*/true);

  // Tap the OK button to close the confirmation.
  [[EarlGrey selectElementWithMatcher:OKButton()] performAction:grey_tap()];
  WaitForJavaScriptDialog(kURL, kConfirmationMessage, /*visible=*/false,
                          /*is_main_frame=*/true);

  // Wait for the expected text to be added to the test page.
  [ChromeEarlGrey waitForWebStateContainingText:kConfirmationResultOK];
}

// Tests that a confirmation dialog is shown, and that the completion block is
// called with the correct value when the Cancel button is tapped.
- (void)testShowJavaScriptConfirmationCancelled {
  // Load the confirmation test page and tap on the link.
  const GURL kURL = self.testServer->GetURL(kConfirmationURLPath);
  [ChromeEarlGrey loadURL:kURL];
  [ChromeEarlGrey waitForWebStateContainingElement:
                      [ElementSelector selectorWithElementID:kTestPageLinkID]];
  [ChromeEarlGrey tapWebStateElementWithID:@(kTestPageLinkID)];
  WaitForJavaScriptDialog(kURL, kConfirmationMessage, /*visible=*/true,
                          /*is_main_frame=*/true);

  // Tap the OK button to close the confirmation.
  [[EarlGrey selectElementWithMatcher:OKButton()] performAction:grey_tap()];

  WaitForJavaScriptDialog(kURL, kConfirmationMessage, /*visible=*/false,
                          /*is_main_frame=*/true);

  // Wait for the expected text to be added to the test page.
  [ChromeEarlGrey waitForWebStateContainingText:kConfirmationResultCancelled];
}

// Tests that a prompt dialog is shown, and that the completion block is called
// with the correct value when the OK button is tapped.
- (void)testShowJavaScriptPromptOK {
  // Load the prompt test page and tap on the link.
  const GURL kURL = self.testServer->GetURL(kPromptURLPath);
  [ChromeEarlGrey loadURL:kURL];
  [ChromeEarlGrey waitForWebStateContainingElement:
                      [ElementSelector selectorWithElementID:kTestPageLinkID]];
  [ChromeEarlGrey tapWebStateElementWithID:@(kTestPageLinkID)];
  WaitForJavaScriptDialog(kURL, kPromptMessage, /*visible=*/true,
                          /*is_main_frame=*/true);

  // Enter text into text field.
  TypeInPrompt(@(kPromptTestUserInput));

  // Tap the OK button to close the confirmation.
  [[EarlGrey selectElementWithMatcher:OKButton()] performAction:grey_tap()];
  WaitForJavaScriptDialog(kURL, kPromptMessage, /*visible=*/false,
                          /*is_main_frame=*/true);

  // Wait for the html  to be reset to the input text.
  [ChromeEarlGrey waitForWebStateContainingText:kPromptTestUserInput];
}

// Tests that a prompt dialog is shown, and that the completion block is called
// with the correct value when the Cancel button is tapped.
- (void)testShowJavaScriptPromptCancelled {
  // Load the prompt test page and tap on the link.
  const GURL kURL = self.testServer->GetURL(kPromptURLPath);
  [ChromeEarlGrey loadURL:kURL];
  [ChromeEarlGrey waitForWebStateContainingElement:
                      [ElementSelector selectorWithElementID:kTestPageLinkID]];
  [ChromeEarlGrey tapWebStateElementWithID:@(kTestPageLinkID)];
  WaitForJavaScriptDialog(kURL, kPromptMessage, /*visible=*/true,
                          /*is_main_frame=*/true);

  // Enter text into text field.
  TypeInPrompt(@(kPromptTestUserInput));

  // Tap the Cancel button.
  TapCancel();
  WaitForJavaScriptDialog(kURL, kPromptMessage, /*visible=*/false,
                          /*is_main_frame=*/true);

  // Wait for the html  to be reset to the cancel text.
  [ChromeEarlGrey waitForWebStateContainingText:kPromptResultCancelled];
}

// Tests that JavaScript alerts that are shown in a loop can be suppressed.
- (void)testShowJavaScriptAlertLoop {
  // Load the alert test page and tap on the link.
  const GURL kURL = self.testServer->GetURL(kAlertLoopURLPath);
  [ChromeEarlGrey loadURL:kURL];
  [ChromeEarlGrey waitForWebStateContainingElement:
                      [ElementSelector selectorWithElementID:kTestPageLinkID]];
  [ChromeEarlGrey tapWebStateElementWithID:@(kTestPageLinkID)];
  WaitForJavaScriptDialog(kURL, kAlertLoopMessage, /*visible=*/true,
                          /*is_main_frame=*/true);

  // Tap the OK button to close the alert, then verify that the next alert in
  // the loop is shown.
  [[EarlGrey selectElementWithMatcher:OKButton()] performAction:grey_tap()];
  WaitForJavaScriptDialog(kURL, kAlertLoopMessage, /*visible=*/true,
                          /*is_main_frame=*/true);

  // Tap the suppress dialogs button.
  TapSuppressDialogsButton();
  WaitForJavaScriptDialog(kURL, kAlertLoopMessage, /*visible=*/false,
                          /*is_main_frame=*/true);

  // Wait for the html  to be reset to the loop finished text.
  [ChromeEarlGrey waitForWebStateContainingText:kAlertLoopFinishedText];
}

// Tests to ensure crbug.com/658260 does not regress.
// Tests that if an alert should be called when settings are displays, the alert
// waits for the dismiss of the settings.
- (void)MAYBE_testShowJavaScriptBehindSettings {
// TODO(crbug.com/40182086): test failing on ipad device
#if !TARGET_OS_SIMULATOR
  if ([ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_SKIPPED(@"This test doesn't pass on iPad device.");
  }
#endif
  // Load the alert test page.
  const GURL kURL = self.testServer->GetURL(kAlertURLPath);
  [ChromeEarlGrey loadURL:kURL];
  [ChromeEarlGrey waitForWebStateContainingElement:
                      [ElementSelector selectorWithElementID:kTestPageLinkID]];

  // Show settings.
  [ChromeEarlGreyUI openSettingsMenu];
  [[EarlGrey
      selectElementWithMatcher:grey_allOf(grey_accessibilityLabel(
                                              l10n_util::GetNSString(
                                                  IDS_IOS_SETTINGS_TITLE)),
                                          grey_accessibilityTrait(
                                              UIAccessibilityTraitHeader),
                                          nil)]
      assertWithMatcher:grey_sufficientlyVisible()];

  // Tap the link to trigger the dialog.
  [ChromeEarlGrey tapWebStateElementWithID:@(kTestPageLinkID)];

  // Close the settings.
  [[EarlGrey selectElementWithMatcher:SettingsDoneButton()]
      performAction:grey_tap()];

  // Make sure the alert is present.
  WaitForJavaScriptDialog(kURL, kAlertMessage, /*visible=*/true,
                          /*is_main_frame=*/true);

  [[EarlGrey selectElementWithMatcher:OKButton()] performAction:grey_tap()];
  WaitForJavaScriptDialog(kURL, kAlertMessage, /*visible=*/false,
                          /*is_main_frame=*/true);

  // Wait for the expected text to be added to the test page.
  [ChromeEarlGrey waitForWebStateContainingText:kAlertResult];
}

// Tests that an alert is presented after displaying the share menu.
- (void)testShowJavaScriptAfterShareMenu {
  // Load the blank test page.
  const GURL kURL = self.testServer->GetURL(kAlertURLPath);
  [ChromeEarlGrey loadURL:kURL];
  [ChromeEarlGrey waitForWebStateContainingElement:
                      [ElementSelector selectorWithElementID:kTestPageLinkID]];

  [ChromeEarlGreyUI openShareMenu];

  // Copy URL, dismissing the share menu.
  [ChromeEarlGrey tapButtonInActivitySheetWithID:@"Copy"];

  // Show an alert and assert it is present.
  [ChromeEarlGrey tapWebStateElementWithID:@(kTestPageLinkID)];
  WaitForJavaScriptDialog(kURL, kAlertMessage, /*visible=*/true,
                          /*is_main_frame=*/true);

  [[EarlGrey selectElementWithMatcher:OKButton()] performAction:grey_tap()];
  WaitForJavaScriptDialog(kURL, kAlertMessage, /*visible=*/false,
                          /*is_main_frame=*/true);

  // Wait for the expected text to be added to the test page.
  [ChromeEarlGrey waitForWebStateContainingText:kAlertResult];
}

@end
