// 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 <XCTest/XCTest.h>

#import <map>
#import <memory>
#import <string>

#import "base/functional/bind.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "ios/chrome/browser/shared/public/features/features.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/earl_grey/scoped_block_popups_pref.h"
#import "ios/net/url_test_util.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/web/common/features.h"
#import "net/http/http_response_headers.h"
#import "net/test/embedded_test_server/default_handlers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/expectation_handler.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"

using chrome_test_util::OmniboxContainingText;
using chrome_test_util::OmniboxText;

namespace {

// URL used for the reload test.
const char kReloadTestUrl[] = "/reloadTest";

class ReloadHandler {
 public:
  ReloadHandler() : count_(0) {}
  std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
      const net::test_server::HttpRequest& request) {
    if (request.relative_url == kReloadTestUrl) {
      auto response = std::make_unique<net::test_server::BasicHttpResponse>();
      response->set_code(net::HTTP_OK);
      response->set_content_type("text/html");
      response->set_content(base::StringPrintf("Load request %d", count_++));
      return response;
    }
    return nullptr;
  }

 private:
  int count_;
};

}  // namespace

// Tests web browsing scenarios.
@interface BrowsingTestCase : ChromeTestCase {
  std::unique_ptr<net::test_server::ExpectationHandler> _expectationHandler;
  std::unique_ptr<ReloadHandler> _reloadHandler;
}
@end

@implementation BrowsingTestCase

- (void)setUp {
  [super setUp];
  _reloadHandler = std::make_unique<ReloadHandler>();
  self.testServer->RegisterRequestHandler(base::BindRepeating(
      &ReloadHandler::HandleRequest, base::Unretained(_reloadHandler.get())));
  _expectationHandler =
      std::make_unique<net::test_server::ExpectationHandler>(self.testServer);
  net::test_server::RegisterDefaultHandlers(self.testServer);
  GREYAssertTrue(self.testServer->Start(), @"Server failed to start.");
}

- (AppLaunchConfiguration)appConfigurationForTestCase {
  AppLaunchConfiguration config = [super appConfigurationForTestCase];
  if ([self isRunningTest:@selector(testLoad)] ||
      [self isRunningTest:@selector(testDocumentWrite)]) {
    config.features_enabled.push_back(web::features::kAssertOnJavaScriptErrors);
  }
  return config;
}

// Matcher for the title of the current tab (on tablet only), which is
// sufficiently visible.
id<GREYMatcher> TabWithTitle(const std::string& tab_title) {
  return grey_allOf(
      grey_accessibilityLabel(base::SysUTF8ToNSString(tab_title)),
      grey_ancestor(grey_kindOfClassName(@"TabStripTabCell")),
      grey_not(grey_accessibilityTrait(UIAccessibilityTraitStaticText)),
      grey_sufficientlyVisible(), nil);
}

// Tests that page successfully loads.
- (void)testLoad {
  const GURL URL = self.testServer->GetURL("/echo");
  [ChromeEarlGrey loadURL:URL];
  [ChromeEarlGrey waitForWebStateContainingText:"Echo"];
}

// Tests that page successfully loads when using `document.write`.
- (void)testDocumentWrite {
  const GURL URL = self.testServer->GetURL("/echo");
  [ChromeEarlGrey loadURL:URL];
  [ChromeEarlGrey waitForWebStateContainingText:"Echo"];

  base::Value result = [ChromeEarlGrey
      evaluateJavaScript:
          @"document.open(); document.write('<p>Rewritten</p>'); "
          @"document.close(); true;"];
  GREYAssertTrue(result.is_bool() && result.GetBool(), @"JS execution failed.");
  [ChromeEarlGrey waitForWebStateContainingText:"Rewritten"];
}

// Tests that page successfully reloads.
- (void)testReload {
  GURL URL = self.testServer->GetURL(kReloadTestUrl);
  [ChromeEarlGrey loadURL:URL];
  std::string expectedBodyBeforeReload = "Load request 0";
  [ChromeEarlGrey waitForWebStateContainingText:expectedBodyBeforeReload];

  [ChromeEarlGreyUI reload];
  std::string expectedBodyAfterReload = "Load request 1";
  [ChromeEarlGrey waitForWebStateContainingText:expectedBodyAfterReload];
}

// Tests that a tab's title is based on the URL when no other information is
// available.
- (void)testBrowsingTabTitleSetFromURL {
  if (![ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_SKIPPED(@"Tab Title not displayed on handset.");
  }

  const GURL destinationURL = self.testServer->GetURL("/destination.html");
  [ChromeEarlGrey loadURL:destinationURL];

  // Add 3 for the "://" which is not considered part of the scheme
  std::string URLWithoutScheme =
      destinationURL.spec().substr(destinationURL.GetScheme().length() + 3);

  [[EarlGrey selectElementWithMatcher:TabWithTitle(URLWithoutScheme)]
      assertWithMatcher:grey_notNil()];
}

// Tests that after a PDF is loaded, the title appears in the tab bar on iPad.
- (void)testPDFLoadTitle {
  if (![ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_SKIPPED(@"Tab Title not displayed on handset.");
  }

  const GURL destinationURL = self.testServer->GetURL("/testpage.pdf");
  [ChromeEarlGrey loadURL:destinationURL];

  // Add 3 for the "://" which is not considered part of the scheme
  std::string URLWithoutScheme =
      destinationURL.spec().substr(destinationURL.GetScheme().length() + 3);

  [[EarlGrey selectElementWithMatcher:TabWithTitle(URLWithoutScheme)]
      assertWithMatcher:grey_notNil()];
}

// Tests that tab title is set to the specified title from a JavaScript.
- (void)testBrowsingTabTitleSetFromScript {
  if (![ChromeEarlGrey isIPadIdiom]) {
    EARL_GREY_TEST_SKIPPED(@"Tab Title not displayed on handset.");
  }

  const char* kPageTitle = "Some title";
  const GURL URL = GURL(base::StringPrintf(
      "data:text/html;charset=utf-8,<script>document.title = "
      "\"%s\"</script>",
      kPageTitle));
  [ChromeEarlGrey loadURL:URL];

  [[EarlGrey selectElementWithMatcher:TabWithTitle(kPageTitle)]
      assertWithMatcher:grey_notNil()];
}

// Tests that clicking a link with URL changed by onclick uses the href of the
// anchor tag instead of the one specified in JavaScript. Also verifies a new
// tab is opened by target '_blank'.
// TODO(crbug.com/41299306): WKWebView does not open a new window as expected by
// this test.
- (void)DISABLED_testBrowsingPreventDefaultWithLinkOpenedByJavascript {
  // Create map of canned responses and set up the test HTML server.
  std::map<GURL, std::string> responses;
  const GURL URL =
      self.testServer->GetURL("/preventDefaultWithLinkOpenedByJavascript");
  const GURL anchorURL = self.testServer->GetURL("/anchorDestination");
  const GURL destinationURL = self.testServer->GetURL("/javaScriptDestination");
  // This is a page with a link where the href and JavaScript are setting the
  // destination to two different URLs so the test can verify which one the
  // browser uses.
  responses[URL] = base::StringPrintf(
      "<a id='link' href='%s' target='_blank' "
      "onclick='window.location.href=\"%s\"; "
      "event.stopPropagation()' id='link'>link</a>",
      anchorURL.spec().c_str(), destinationURL.spec().c_str());
  responses[anchorURL] = "anchor destination";

  for (const auto& [url, content] : responses) {
    _expectationHandler->OnRequest(url.path())
        .RespondWith("text/html", content);
  }

  ScopedBlockPopupsPref prefSetter(CONTENT_SETTING_ALLOW);

  [ChromeEarlGrey loadURL:URL];
  [ChromeEarlGrey waitForMainTabCount:1];

  [ChromeEarlGrey tapWebStateElementWithID:@"link"];
  [ChromeEarlGrey waitForMainTabCount:2];

  // Verify the new tab was opened with the expected URL.
  [[EarlGrey selectElementWithMatcher:OmniboxText(anchorURL.GetContent())]
      assertWithMatcher:grey_notNil()];
}

// Tests tapping a link that navigates to a page that immediately navigates
// again via document.location.href.
// TODO(crbug.com/40234734): Flaky on iPhone.
- (void)DISABLED_testBrowsingWindowDataLinkScriptRedirect {
  // Create map of canned responses and set up the test HTML server.
  std::map<GURL, std::string> responses;
  const GURL URL = self.testServer->GetURL("/windowDataLinkScriptRedirect");
  const GURL intermediateURL = self.testServer->GetURL("/intermediate");
  const GURL destinationURL = self.testServer->GetURL("/destination");
  // This is a page with a link to the intermediate page.
  responses[URL] =
      base::StringPrintf("<a id='link' href='%s' target='_blank'>link</a>",
                         intermediateURL.spec().c_str());
  // This intermediate page uses JavaScript to immediately navigate to the
  // destination page.
  responses[intermediateURL] =
      base::StringPrintf("<script>document.location.href=\"%s\"</script>",
                         destinationURL.spec().c_str());
  // This is the page that should be showing at the end of the test.
  responses[destinationURL] = "You've arrived!";

  for (const auto& [url, content] : responses) {
    _expectationHandler->OnRequest(url.path())
        .RespondWith("text/html", content);
  }

  ScopedBlockPopupsPref prefSetter(CONTENT_SETTING_ALLOW);

  [ChromeEarlGrey loadURL:URL];
  [ChromeEarlGrey waitForMainTabCount:1];

  [ChromeEarlGrey tapWebStateElementWithID:@"link"];
  [ChromeEarlGrey waitForMainTabCount:2];

  // Verify the new tab was opened with the expected URL.
  [[EarlGrey selectElementWithMatcher:OmniboxText(destinationURL.GetContent())]
      assertWithMatcher:grey_notNil()];
}

// Tests that a link with a JavaScript-based navigation changes the page and
// that the back button works as expected afterwards.
- (void)testBrowsingJavaScriptBasedNavigation {
  std::map<GURL, std::string> responses;
  const GURL URL = self.testServer->GetURL("/origin");
  const GURL destURL = self.testServer->GetURL("/destination");
  // Page containing a link with onclick attribute that sets window.location
  // to the destination URL.
  responses[URL] = base::StringPrintf(
      "<a href='#' onclick=\"window.location='%s';\" id='link'>Link</a>",
      destURL.spec().c_str());
  // Page with some text.
  responses[destURL] = "You've arrived!";

  for (const auto& [url, content] : responses) {
    _expectationHandler->OnRequest(url.path())
        .RespondWith("text/html", content);
  }

  [ChromeEarlGrey loadURL:URL];
  [ChromeEarlGrey tapWebStateElementWithID:@"link"];
  [ChromeEarlGrey waitForWebStateVisibleURL:destURL];

  [ChromeEarlGrey goBack];
  [ChromeEarlGrey waitForWebStateContainingText:"Link"];
  const GURL newOriginURL = self.testServer->GetURL("/origin#");

  // The displayed URL is now "http://origin/#" due to the link click. This is
  // consistent with all other browsers.
  [ChromeEarlGrey waitForWebStateVisibleURL:newOriginURL];
}

// Tests that a link with WebUI URL does not trigger a load. WebUI pages may
// have increased power and using the same web process (which may potentially
// be controlled by an attacker) is dangerous.
- (void)testTapLinkWithWebUIURL {
  // Create map of canned responses and set up the test HTML server.
  std::map<GURL, std::string> responses;
  const GURL URL = self.testServer->GetURL("/pageWithWebUILink");
  const char kPageHTML[] =
      "<script>"
      "  function printMsg() {"
      "    window.setTimeout(function() {"
      "      document.body.appendChild("
      "          document.createTextNode('Hello world!'));"
      "      }, 1000);"
      "  }"
      "</script>"
      "<a href='chrome://version' id='link' onclick='printMsg()'>Version</a>";
  responses[URL] = kPageHTML;

  for (const auto& [url, content] : responses) {
    _expectationHandler->OnRequest(url.path())
        .RespondWith("text/html", content);
  }

  // Assert that test is starting with one tab.
  [ChromeEarlGrey waitForMainTabCount:1];
  [ChromeEarlGrey waitForIncognitoTabCount:0];

  [ChromeEarlGrey loadURL:URL];

  // Tap on chrome://version link.
  [ChromeEarlGrey tapWebStateElementWithID:@"link"];

  // Verify that page did not change by checking its URL and message printed by
  // onclick event.
  [[EarlGrey selectElementWithMatcher:OmniboxText("chrome://version")]
      assertWithMatcher:grey_nil()];
  [ChromeEarlGrey waitForWebStateContainingText:"Hello world!"];

  // Verify that no new tabs were open which could load chrome://version.
  [ChromeEarlGrey waitForMainTabCount:1];
}

// Tests that loading WebUI URL via an iframe on a http:// page blocks loading
// the content.
- (void)testLoadWebUIURLWithIFrame {
  // Set up the test HTML server.
  std::map<GURL, std::string> responses;
  const GURL URL = self.testServer->GetURL("/pageWithWebUILink");
  const char kPageHTML[] =
      "<p>Hello world!</p>"
      "<iframe src='chrome://chrome-urls' width='400' height='300'>";
  responses[URL] = kPageHTML;

  for (const auto& [url, content] : responses) {
    _expectationHandler->OnRequest(url.path())
        .RespondWith("text/html", content);
  }

  // Assert that test is starting with one tab.
  [ChromeEarlGrey waitForMainTabCount:1];
  [ChromeEarlGrey waitForIncognitoTabCount:0];

  // Load the page.
  [ChromeEarlGrey loadURL:URL];
  [ChromeEarlGrey waitForPageToFinishLoading];

  // Wait until the page content is rendered.
  [ChromeEarlGrey waitForWebStateContainingText:"Hello world!"];

  // Verify that the page does not show the content of the chrome:// page.
  [ChromeEarlGrey waitForWebStateNotContainingText:"List of Chrome URLs"];
}

// Tests that evaluating user JavaScript that causes navigation correctly
// modifies history.
// TODO(crbug.com/362621166): Test is flaky.
- (void)DISABLED_testBrowsingUserJavaScriptNavigation {
  // TODO(crbug.com/40511873): Keyboard entry inside the omnibox fails only on
  // iPad.
  if ([ChromeEarlGrey isIPadIdiom]) {
    return;
  }

  // Create map of canned responses and set up the test HTML server.
  std::map<GURL, std::string> responses;
  const GURL startURL = self.testServer->GetURL("/startpage");
  const GURL targetURL = self.testServer->GetURL("/targetpage");
  responses[startURL] = "<html><body><p>Ready to begin.</p></body></html>";
  responses[targetURL] = "<html><body><p>You've arrived!</p></body></html>";

  for (const auto& [url, content] : responses) {
    _expectationHandler->OnRequest(url.path())
        .RespondWith("text/html", content);
  }

  // Load the first page and run JS (using the codepath that user-entered JS in
  // the omnibox would take, not page-triggered) that should navigate.
  [ChromeEarlGrey loadURL:startURL];

  NSString* script =
      [NSString stringWithFormat:@"javascript:window.location='%s'",
                                 targetURL.spec().c_str()];

  [ChromeEarlGreyUI focusOmniboxAndReplaceText:script];

  // There's currently no EG API to tap 'go' on the keyboard.
  XCUIApplication* app = [[XCUIApplication alloc] init];
  [[[app keyboards] buttons][@"go"] tap];

  [ChromeEarlGrey waitForPageToFinishLoading];

  [[EarlGrey selectElementWithMatcher:OmniboxText(targetURL.GetContent())]
      assertWithMatcher:grey_notNil()];

  [ChromeEarlGrey goBack];
  [[EarlGrey selectElementWithMatcher:OmniboxText(startURL.GetContent())]
      assertWithMatcher:grey_notNil()];
}

// Tests that evaluating non-navigation user JavaScript doesn't affect history.
// TODO(crbug.com/362621166): Test is flaky.
- (void)DISABLED_testBrowsingUserJavaScriptWithoutNavigation {
  // TODO(crbug.com/40511873): Keyboard entry inside the omnibox fails only on
  // iPad.
  if ([ChromeEarlGrey isIPadIdiom]) {
    return;
  }

  // Create map of canned responses and set up the test HTML server.
  std::map<GURL, std::string> responses;
  const GURL firstURL = self.testServer->GetURL("/firstURL");
  const GURL secondURL = self.testServer->GetURL("/secondURL");
  const std::string firstResponse = "Test Page 1";
  const std::string secondResponse = "Test Page 2";
  responses[firstURL] = firstResponse;
  responses[secondURL] = secondResponse;

  for (const auto& [url, content] : responses) {
    _expectationHandler->OnRequest(url.path())
        .RespondWith("text/html", content);
  }

  [ChromeEarlGrey loadURL:firstURL];
  [ChromeEarlGrey loadURL:secondURL];

  // Execute some JavaScript in the omnibox.
  [ChromeEarlGreyUI
      focusOmniboxAndReplaceText:@"javascript:document.write('foo')"];
  // TODO(crbug.com/40916974): Use simulatePhysicalKeyboardEvent until
  // replaceText can properly handle \n.
  [ChromeEarlGrey simulatePhysicalKeyboardEvent:@"\n" flags:0];
  [ChromeEarlGrey waitForWebStateContainingText:"foo"];

  // Verify that the JavaScript did not affect history by going back and then
  // forward again.
  [ChromeEarlGrey goBack];
  [[EarlGrey selectElementWithMatcher:OmniboxText(firstURL.GetContent())]
      assertWithMatcher:grey_notNil()];
  [ChromeEarlGrey goForward];
  [[EarlGrey selectElementWithMatcher:OmniboxText(secondURL.GetContent())]
      assertWithMatcher:grey_notNil()];
}

@end
