// Copyright 2020 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/callback.h"
#import "base/scoped_observation.h"
#import "base/test/ios/wait_util.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/test_future.h"
#import "ios/web/common/features.h"
#import "ios/web/navigation/crw_wk_navigation_handler.h"
#import "ios/web/public/navigation/https_upgrade_type.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/test/fakes/fake_web_client.h"
#import "ios/web/public/test/navigation_test_util.h"
#import "ios/web/public/test/web_view_content_test_util.h"
#import "ios/web/public/web_state_observer.h"
#import "ios/web/security/wk_web_view_security_util.h"
#import "ios/web/test/web_int_test.h"
#import "ios/web/util/error_translation_util.h"
#import "net/base/apple/url_conversions.h"
#import "net/test/embedded_test_server/default_handlers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "url/url_constants.h"

using base::test::ios::kWaitForPageLoadTimeout;
using web::HttpsUpgradeType;

namespace {

constexpr char kEchoURL[] = "/echo";
constexpr char kServerRedirectToJavaScriptURL[] =
    "/server-redirect?javascript:window.testExecuted=true;";
constexpr char kServerRedirectToDataURL[] =
    "/server-redirect?data:text/html,Blocked";
constexpr char kBlockedDataContent[] = "Blocked";

// A WebStateObserver that observes that the navigation is finished and keeps
// track of the error type (SSL or net error).
class FailedWebStateObserver : public web::WebStateObserver {
 public:
  // Type of the error that caused the navigation to fail.
  enum class ErrorType {
    kNone,
    // The navigation failed due to an SSL error such as an invalid certificate.
    kSSLError,
    // The navigation failed due to a net error such as an invalid hostname.
    kNetError
  };

  FailedWebStateObserver() = default;
  FailedWebStateObserver(const FailedWebStateObserver&) = delete;
  FailedWebStateObserver& operator=(const FailedWebStateObserver&) = delete;

  void SetOnDidFinishNavigation(base::OnceClosure closure) {
    on_did_finish_navigation_ = std::move(closure);
  }

  void DidFinishNavigation(
      web::WebState* web_state,
      web::NavigationContext* navigation_context) override {
    did_finish_ = true;
    failed_https_upgrade_type_ =
        navigation_context->GetFailedHttpsUpgradeType();

    DCHECK_EQ(ErrorType::kNone, error_type_);
    NSError* error = navigation_context->GetError();
    if (web::IsWKWebViewSSLCertError(error)) {
      error_type_ = ErrorType::kSSLError;
    } else {
      int error_code = 0;
      if (!web::GetNetErrorFromIOSErrorCode(
              error.code, &error_code,
              net::NSURLWithGURL(navigation_context->GetUrl()))) {
        error_code = net::ERR_FAILED;
      }
      if (error_code != net::OK) {
        error_type_ = ErrorType::kNetError;
      }
    }
    if (on_did_finish_navigation_) {
      std::move(on_did_finish_navigation_).Run();
    }
  }

  void WebStateDestroyed(web::WebState* web_state) override { NOTREACHED(); }

  bool did_finish() const { return did_finish_; }
  web::HttpsUpgradeType failed_https_upgrade_type() const {
    return failed_https_upgrade_type_;
  }
  ErrorType error_type() const { return error_type_; }

 private:
  base::OnceClosure on_did_finish_navigation_;
  bool did_finish_ = false;
  web::HttpsUpgradeType failed_https_upgrade_type_ =
      web::HttpsUpgradeType::kNone;
  ErrorType error_type_ = ErrorType::kNone;
};

}  // namespace

namespace web {

class CRWKNavigationHandlerIntTest : public WebIntTest {
 protected:
  CRWKNavigationHandlerIntTest()
      : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
    net::test_server::RegisterDefaultHandlers(&server_);
    net::test_server::RegisterDefaultHandlers(&https_server_);
  }

  FakeWebClient* GetWebClient() override {
    return static_cast<FakeWebClient*>(WebIntTest::GetWebClient());
  }

  // Tests the failed HTTPS upgradestatus of a navigation. Navigates to `url`
  // using `https_upgrade_type` as the HTTPS upgrade type. Expects
  // GetFailedHTTPSUpgradeType() to be equal to `https_upgrade_type`.
  // Expects the navigation error to be of type `expected_error_type`.
  void TestFailedHttpsUpgrade(
      const GURL& url,
      HttpsUpgradeType https_upgrade_type,
      HttpsUpgradeType expected_failed_upgrade_type,
      FailedWebStateObserver::ErrorType expected_error_type) {
    web::NavigationManager::WebLoadParams params(url);
    params.transition_type = ui::PAGE_TRANSITION_TYPED;
    params.https_upgrade_type = https_upgrade_type;

    FailedWebStateObserver observer;
    base::ScopedObservation<WebState, WebStateObserver> scoped_observer(
        &observer);
    scoped_observer.Observe(web_state());
    web_state()->GetNavigationManager()->LoadURLWithParams(params);

    // Need to use a pointer to `observer` as the block wants to capture it by
    // value (even if marked with __block) which would not work.
    FailedWebStateObserver* observer_ptr = &observer;
    EXPECT_TRUE(
        base::test::ios::WaitUntilConditionOrTimeout(kWaitForPageLoadTimeout, ^{
          // Run the event loop, otherwise the HTTPS connection times out
          // instead of failing with an SSL error.
          base::RunLoop().RunUntilIdle();
          return observer_ptr->did_finish() &&
                 (observer_ptr->failed_https_upgrade_type() ==
                  expected_failed_upgrade_type);
        }));
    EXPECT_EQ(expected_error_type, observer.error_type());
  }

  base::test::ScopedFeatureList scoped_feature_list_;
  net::test_server::EmbeddedTestServer server_;
  net::EmbeddedTestServer https_server_;
};

// Tests that reloading a page with a different default User Agent updates the
// item.
TEST_F(CRWKNavigationHandlerIntTest, ReloadWithDifferentUserAgent) {
  FakeWebClient* web_client = GetWebClient();
  web_client->SetDefaultUserAgent(UserAgentType::MOBILE);

  ASSERT_TRUE(server_.Start());
  GURL url(server_.GetURL("/echo"));
  ASSERT_TRUE(LoadUrl(url));

  NavigationItem* item = web_state()->GetNavigationManager()->GetVisibleItem();
  EXPECT_EQ(UserAgentType::MOBILE, item->GetUserAgentType());

  web_client->SetDefaultUserAgent(UserAgentType::DESKTOP);

  web_state()->GetNavigationManager()->Reload(ReloadType::NORMAL,
                                              /* check_for_repost = */ true);

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForPageLoadTimeout, ^{
        NavigationItem* item_after_reload =
            web_state()->GetNavigationManager()->GetVisibleItem();
        return item_after_reload->GetUserAgentType() == UserAgentType::DESKTOP;
      }));
}

// Tests that reloading a failed page that should not have a User Agent doesn't
// trigger a DCHECK (preventing crbug.com/1360567).
TEST_F(CRWKNavigationHandlerIntTest, ReloadNONEUserAgentErrorPage) {
  FakeWebClient* web_client = GetWebClient();
  web_client->SetDefaultUserAgent(UserAgentType::MOBILE);

  GURL url("testwebui://extensions");
  ASSERT_TRUE(LoadUrl(url));

  NavigationItem* item = web_state()->GetNavigationManager()->GetVisibleItem();
  EXPECT_EQ(UserAgentType::NONE, item->GetUserAgentType());

  web_state()->GetNavigationManager()->Reload(ReloadType::NORMAL,
                                              /* check_for_repost = */ true);

  // Make sure the load has time to start.
  base::test::ios::SpinRunLoopWithMinDelay(base::Milliseconds(10));

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::test::ios::kWaitForPageLoadTimeout, ^{
        return !web_state()->IsLoading();
      }));
}

// Tests that an SSL or net error on a navigation that wasn't upgraded to HTTPS
// doesn't set the IsFailedHTTPSUpgrade() bit on the navigation context.
TEST_F(CRWKNavigationHandlerIntTest, FailedHTTPSUpgrade_NotUpgraded_SSLError) {
  ASSERT_TRUE(https_server_.Start());
  GURL url(https_server_.GetURL("/"));
  TestFailedHttpsUpgrade(url, HttpsUpgradeType::kNone, HttpsUpgradeType::kNone,
                         FailedWebStateObserver::ErrorType::kSSLError);
}

// Tests that an SSL error on a navigation that was upgraded to HTTPS
// sets the IsFailedHTTPSUpgrade() bit on the navigation context.
TEST_F(CRWKNavigationHandlerIntTest, FailedHTTPSUpgrade_Upgraded_SSLError) {
  ASSERT_TRUE(https_server_.Start());
  GURL url(https_server_.GetURL("/"));
  TestFailedHttpsUpgrade(url, HttpsUpgradeType::kHttpsOnlyMode,
                         HttpsUpgradeType::kHttpsOnlyMode,
                         FailedWebStateObserver::ErrorType::kSSLError);
}

// Tests that a net error on a navigation that wasn't upgraded to HTTPS
// doesn't set the IsFailedHTTPSUpgrade() bit on the navigation context.
// TODO(crbug.com/433316885): Re-enable this test.
TEST_F(CRWKNavigationHandlerIntTest,
       DISABLED_FailedHTTPSUpgrade_NotUpgraded_NetError) {
  GURL url("https://site.test");
  TestFailedHttpsUpgrade(url, HttpsUpgradeType::kNone, HttpsUpgradeType::kNone,
                         FailedWebStateObserver::ErrorType::kNetError);
}

// Tests that a net error on a navigation that was upgraded to HTTPS
// sets the IsFailedHTTPSUpgrade() bit on the navigation context.
// TODO(crbug.com/433316885): Re-enable this test.
TEST_F(CRWKNavigationHandlerIntTest,
       DISABLED_FailedHTTPSUpgrade_Upgraded_NetError) {
  GURL url("https://site.test");
  TestFailedHttpsUpgrade(url, HttpsUpgradeType::kHttpsOnlyMode,
                         HttpsUpgradeType::kHttpsOnlyMode,
                         FailedWebStateObserver::ErrorType::kNetError);
}

// Tests that a server redirect to a JavaScript URL does not proceed.
TEST_F(CRWKNavigationHandlerIntTest, ServerRedirectToJavaScriptURL) {
  ASSERT_TRUE(server_.Start());
  GURL initial_url = server_.GetURL(kEchoURL);
  ASSERT_TRUE(LoadUrl(initial_url));

  GURL redirect_url = server_.GetURL(kServerRedirectToJavaScriptURL);

  FailedWebStateObserver observer;
  base::ScopedObservation<WebState, WebStateObserver> scoped_observer(
      &observer);
  scoped_observer.Observe(web_state());

  base::test::TestFuture<void> future;
  observer.SetOnDidFinishNavigation(future.GetCallback());

  NavigationManager::WebLoadParams params(redirect_url);
  web_state()->GetNavigationManager()->LoadURLWithParams(params);

  EXPECT_TRUE(future.Wait());

  EXPECT_EQ(FailedWebStateObserver::ErrorType::kNetError,
            observer.error_type());
  EXPECT_NE(url::kJavaScriptScheme,
            web_state()->GetLastCommittedURL().scheme());
  EXPECT_NE(url::kJavaScriptScheme, web_state()->GetVisibleURL().scheme());
}

// Tests that a server redirect to a data URL does not proceed.
TEST_F(CRWKNavigationHandlerIntTest, ServerRedirectToDataURL) {
  ASSERT_TRUE(server_.Start());
  GURL initial_url = server_.GetURL(kEchoURL);
  ASSERT_TRUE(LoadUrl(initial_url));

  GURL redirect_url = server_.GetURL(kServerRedirectToDataURL);

  FailedWebStateObserver observer;
  base::ScopedObservation<WebState, WebStateObserver> scoped_observer(
      &observer);
  scoped_observer.Observe(web_state());

  base::test::TestFuture<void> future;
  observer.SetOnDidFinishNavigation(future.GetCallback());

  NavigationManager::WebLoadParams params(redirect_url);
  web_state()->GetNavigationManager()->LoadURLWithParams(params);

  EXPECT_TRUE(future.Wait());

  EXPECT_EQ(FailedWebStateObserver::ErrorType::kNetError,
            observer.error_type());
  EXPECT_NE(url::kDataScheme, web_state()->GetLastCommittedURL().scheme());
  EXPECT_NE(url::kDataScheme, web_state()->GetVisibleURL().scheme());
  EXPECT_FALSE(test::IsWebViewContainingText(web_state(), kBlockedDataContent));
}

}  // namespace web
