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

#import <TargetConditionals.h>

#import <functional>
#import <string>

#import "base/functional/bind.h"
#import "base/test/ios/wait_util.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h"
#import "ios/testing/earl_grey/earl_grey_test.h"
#import "ios/testing/embedded_test_server_handlers.h"
#import "ios/web/common/features.h"
#import "net/test/embedded_test_server/default_handlers.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"

namespace {
// Returns ERR_CONNECTION_CLOSED error message.
std::string GetErrorMessage() {
  return net::ErrorToShortString(net::ERR_CONNECTION_CLOSED);
}

const std::string kRedirectPage = "/redirect-page.html";

// Provides responses for the different pages.
std::unique_ptr<net::test_server::HttpResponse> StandardResponse(
    const net::test_server::HttpRequest& request) {
  if (request.GetURL().GetPath() == kRedirectPage) {
    auto result = std::make_unique<net::test_server::BasicHttpResponse>();
    result->set_code(net::HTTP_MOVED_PERMANENTLY);
    result->AddCustomHeader("Location", "data:text/plain,Hello World");
    return std::move(result);
  }

  return nullptr;
}

}  // namespace

// Tests critical user journeys reloated to page load errors.
@interface ErrorPageTestCase : ChromeTestCase
// YES if test server is replying with valid HTML content (URL query). NO if
// test server closes the socket.
@property(atomic) bool serverRespondsWithContent;
@end

@implementation ErrorPageTestCase
@synthesize serverRespondsWithContent = _serverRespondsWithContent;

- (void)setUp {
  [super setUp];

  RegisterDefaultHandlers(self.testServer);
  self.testServer->RegisterRequestHandler(base::BindRepeating(
      &net::test_server::HandlePrefixedRequest, "/echo-query",
      base::BindRepeating(&testing::HandleEchoQueryOrCloseSocket,
                          std::cref(_serverRespondsWithContent))));
  self.testServer->RegisterDefaultHandler(
      base::BindRepeating(&StandardResponse));

  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
}

// Tests that the error page is correctly displayed after navigating back to it
// multiple times. See http://crbug.com/944037 .
// TODO:(crbug.com/443199230): Fails on simulator and device.
- (void)DISABLED_testBackForwardErrorPage {
  std::string errorText = net::ErrorToShortString(net::ERR_INVALID_URL);
  self.serverRespondsWithContent = YES;

  [ChromeEarlGrey loadURL:GURL("chrome://invalid")];
  [ChromeEarlGrey waitForWebStateContainingText:errorText];
  // Add some delay otherwise the back/forward navigations are occurring too
  // fast.
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.2));

  // Navigate to a page which responds.
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/echo-query?bar")];
  [ChromeEarlGrey waitForWebStateContainingText:"bar"];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.2));

  [ChromeEarlGrey goBack];
  [ChromeEarlGrey waitForWebStateContainingText:errorText];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.2));

  [ChromeEarlGrey goForward];
  [ChromeEarlGrey waitForWebStateContainingText:"bar"];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.2));

  [ChromeEarlGrey goBack];
  [ChromeEarlGrey waitForWebStateContainingText:errorText];
  base::test::ios::SpinRunLoopWithMinDelay(base::Seconds(0.2));

  [ChromeEarlGrey goForward];
  [ChromeEarlGrey waitForWebStateContainingText:"bar"];
}

// Loads the URL which fails to load, then sucessfully navigates back/forward to
// the page.
// TODO:(crbug.com/1185639): Re-enable this test on simulator.
#if TARGET_OS_SIMULATOR
#define MAYBE_testNavigateForwardToErrorPage \
  FLAKY_testNavigateForwardToErrorPage
#else
#define MAYBE_testNavigateForwardToErrorPage testNavigateForwardToErrorPage
#endif
- (void)MAYBE_testNavigateForwardToErrorPage {
  self.serverRespondsWithContent = YES;
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/echo-query?bar")];
  [ChromeEarlGrey waitForWebStateContainingText:"bar"];

  // No response leads to ERR_CONNECTION_CLOSED error.
  self.serverRespondsWithContent = NO;
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/echo-query?foo")];
  [ChromeEarlGrey waitForWebStateContainingText:GetErrorMessage()];

  self.serverRespondsWithContent = YES;
  [ChromeEarlGrey goBack];
  [ChromeEarlGrey waitForWebStateContainingText:"bar"];

  // Navigate forward to the error page, which should load without errors.
  [ChromeEarlGrey goForward];
  [ChromeEarlGrey waitForWebStateContainingText:"foo"];
}

// Loads the URL which fails to load, then sucessfully reloads the page.
- (void)testReloadErrorPage {
  // No response leads to ERR_CONNECTION_CLOSED error.
  self.serverRespondsWithContent = NO;
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/echo-query?foo")];
  [ChromeEarlGrey waitForWebStateContainingText:GetErrorMessage()];

  // Reload the page, which should load without errors.
  self.serverRespondsWithContent = YES;
  [ChromeEarlGrey reload];
  [ChromeEarlGrey waitForWebStateContainingText:"foo"];
}

// Sucessfully loads the page, stops the server and reloads the page.
- (void)testReloadPageAfterServerIsDown {
  // Sucessfully load the page.
  self.serverRespondsWithContent = YES;
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/echo-query?foo")];
  [ChromeEarlGrey waitForWebStateContainingText:"foo"];

  // Reload the page, no response leads to ERR_CONNECTION_CLOSED error.
  self.serverRespondsWithContent = NO;
  [ChromeEarlGrey reload];
  [ChromeEarlGrey waitForWebStateContainingText:GetErrorMessage()];
}

// Loads a URL then restore the session and fail during the reload
- (void)testRestoreErrorPage {
  // Load the page.
  self.serverRespondsWithContent = YES;
  [ChromeEarlGrey loadURL:self.testServer->GetURL("/echo-query?foo")];
  [ChromeEarlGrey waitForWebStateContainingText:"foo"];
  GREYAssertEqual(1, [ChromeEarlGrey navigationBackListItemsCount],
                  @"The navigation back list should have only 1 entries before "
                  @"the restoration.");

  // Restore the session but with the page no longer loading.
  self.serverRespondsWithContent = NO;
  [self triggerRestoreByRestartingApplication];
  [ChromeEarlGrey waitForWebStateContainingText:GetErrorMessage()];

  GREYAssertEqual(1, [ChromeEarlGrey navigationBackListItemsCount],
                  @"The navigation back list should still have only 1 entries "
                  @"after the restoration.");
}

// Loads a URL which redirect to a data URL and check that the navigation is
// blocked on the first URL.
- (void)testRedirectToData {
  // Disable the test on iOS 16.4 as WKWebView handles this internally now as of
  // https://bugs.webkit.org/show_bug.cgi?id=230158
  // TODO(crbug.com/40267045): Remove redirect logic completely when dropping
  // iOS 16.
  if (@available(iOS 16.4, *)) {
    EARL_GREY_TEST_DISABLED(@"Test disabled on iOS 16.4.");
  }
  self.serverRespondsWithContent = YES;
  [ChromeEarlGrey loadURL:self.testServer->GetURL(kRedirectPage)];
  [ChromeEarlGrey waitForWebStateContainingText:net::ErrorToShortString(
                                                    net::ERR_UNSAFE_REDIRECT)];
  [ChromeEarlGrey waitForWebStateContainingText:kRedirectPage];
  [[EarlGrey selectElementWithMatcher:chrome_test_util::Omnibox()]
      assertWithMatcher:chrome_test_util::OmniboxContainingText(kRedirectPage)];
}

@end
