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

#include "content/browser/renderer_host/navigation_throttle_runner.h"

#include <optional>
#include <set>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/metrics_hashes.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/renderer_host/navigation_throttle_registry_impl.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/test_navigation_throttle.h"
#include "content/public/test/test_renderer_host.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace content {

// Test version of a NavigationThrottle that will execute a callback when
// called.
class DeletingNavigationThrottle : public NavigationThrottle {
 public:
  DeletingNavigationThrottle(NavigationThrottleRegistry& registry,
                             const base::RepeatingClosure& deletion_callback)
      : NavigationThrottle(registry), deletion_callback_(deletion_callback) {}
  ~DeletingNavigationThrottle() override {}

  NavigationThrottle::ThrottleCheckResult WillStartRequest() override {
    deletion_callback_.Run();
    return NavigationThrottle::PROCEED;
  }

  NavigationThrottle::ThrottleCheckResult WillRedirectRequest() override {
    deletion_callback_.Run();
    return NavigationThrottle::PROCEED;
  }

  NavigationThrottle::ThrottleCheckResult WillFailRequest() override {
    deletion_callback_.Run();
    return NavigationThrottle::PROCEED;
  }

  NavigationThrottle::ThrottleCheckResult WillProcessResponse() override {
    deletion_callback_.Run();
    return NavigationThrottle::PROCEED;
  }

  NavigationThrottle::ThrottleCheckResult WillCommitWithoutUrlLoader()
      override {
    deletion_callback_.Run();
    return NavigationThrottle::PROCEED;
  }

  const char* GetNameForLogging() override {
    return "DeletingNavigationThrottle";
  }

 private:
  base::RepeatingClosure deletion_callback_;
};

class NavigationThrottleRunnerTest : public RenderViewHostTestHarness,
                                     public NavigationThrottleRegistryBase {
 public:
  NavigationThrottleRunnerTest()
      : delegate_result_(NavigationThrottle::DEFER) {}
  ~NavigationThrottleRunnerTest() override = default;

  void SetUp() override {
    RenderViewHostTestHarness::SetUp();
    runner_ = std::make_unique<NavigationThrottleRunner>(this, 1, true);
  }

  void Resume() {
    ASSERT_EQ(1u, deferring_throttles_.size());
    runner_->ResumeProcessingNavigationEvent(*deferring_throttles_.begin());
    deferring_throttles_.clear();
  }

  void SimulateEvent(NavigationThrottleEvent event) {
    was_delegate_notified_ = false;
    delegate_result_ = NavigationThrottle::DEFER;
    observer_last_event_ = NavigationThrottleEvent::kNoEvent;
    runner_->ProcessNavigationEvent(event);
  }

  // Whether the callback was called.
  bool was_delegate_notified() const { return was_delegate_notified_; }

  // Returns the delegate_result.
  NavigationThrottle::ThrottleCheckResult delegate_result() const {
    return delegate_result_;
  }

  NavigationThrottleEvent observer_last_event() const {
    return observer_last_event_;
  }

  bool is_deferring() { return !deferring_throttles_.empty(); }

  NavigationThrottleRunner* runner() { return runner_.get(); }

  void CheckNotNotified(TestNavigationThrottle* throttle) {
    CHECK_EQ(
        0, throttle->GetCallCount(TestNavigationThrottle::WILL_START_REQUEST));
    CHECK_EQ(0, throttle->GetCallCount(
                    TestNavigationThrottle::WILL_REDIRECT_REQUEST));
    CHECK_EQ(0,
             throttle->GetCallCount(TestNavigationThrottle::WILL_FAIL_REQUEST));
    CHECK_EQ(0, throttle->GetCallCount(
                    TestNavigationThrottle::WILL_PROCESS_RESPONSE));
  }

  void CheckNotifiedOfEvent(TestNavigationThrottle* throttle,
                            NavigationThrottleEvent event) {
    if (event == NavigationThrottleEvent::kWillStartRequest) {
      CHECK_EQ(1, throttle->GetCallCount(
                      TestNavigationThrottle::WILL_START_REQUEST));
    } else {
      CHECK_EQ(0, throttle->GetCallCount(
                      TestNavigationThrottle::WILL_START_REQUEST));
    }
    if (event == NavigationThrottleEvent::kWillRedirectRequest) {
      CHECK_EQ(1, throttle->GetCallCount(
                      TestNavigationThrottle::WILL_REDIRECT_REQUEST));
    } else {
      CHECK_EQ(0, throttle->GetCallCount(
                      TestNavigationThrottle::WILL_REDIRECT_REQUEST));
    }
    if (event == NavigationThrottleEvent::kWillFailRequest) {
      CHECK_EQ(
          1, throttle->GetCallCount(TestNavigationThrottle::WILL_FAIL_REQUEST));
    } else {
      CHECK_EQ(
          0, throttle->GetCallCount(TestNavigationThrottle::WILL_FAIL_REQUEST));
    }
    if (event == NavigationThrottleEvent::kWillProcessResponse) {
      CHECK_EQ(1, throttle->GetCallCount(
                      TestNavigationThrottle::WILL_PROCESS_RESPONSE));
    } else {
      CHECK_EQ(0, throttle->GetCallCount(
                      TestNavigationThrottle::WILL_PROCESS_RESPONSE));
    }
    if (event == NavigationThrottleEvent::kWillCommitWithoutUrlLoader) {
      CHECK_EQ(1, throttle->GetCallCount(
                      TestNavigationThrottle::WILL_COMMIT_WITHOUT_URL_LOADER));
    } else {
      CHECK_EQ(0, throttle->GetCallCount(
                      TestNavigationThrottle::WILL_COMMIT_WITHOUT_URL_LOADER));
    }
  }

  // Creates, register and returns a TestNavigationThrottle that will
  // synchronously return |result| on checks by default.
  TestNavigationThrottle* CreateTestNavigationThrottle(
      NavigationThrottle::ThrottleCheckResult result) {
    TestNavigationThrottle* test_throttle = new TestNavigationThrottle(*this);
    test_throttle->SetResponseForAllMethods(TestNavigationThrottle::SYNCHRONOUS,
                                            result);
    AddThrottle(std::unique_ptr<TestNavigationThrottle>(test_throttle));
    return test_throttle;
  }

  // Creates, register and returns a TestNavigationThrottle that will
  // synchronously return |result| on check for the given |method|, and
  // NavigationThrottle::PROCEED otherwise.
  TestNavigationThrottle* CreateTestNavigationThrottle(
      TestNavigationThrottle::ThrottleMethod method,
      NavigationThrottle::ThrottleCheckResult result) {
    TestNavigationThrottle* test_throttle =
        CreateTestNavigationThrottle(NavigationThrottle::PROCEED);
    test_throttle->SetResponse(method, TestNavigationThrottle::SYNCHRONOUS,
                               result);
    return test_throttle;
  }

  // Creates and register a NavigationThrottle that will delete the
  // NavigationHandle in checks.
  void AddDeletingNavigationThrottle() {
    AddThrottle(std::make_unique<DeletingNavigationThrottle>(
        *this, base::BindRepeating(
                   &NavigationThrottleRunnerTest::ResetNavigationThrottleRunner,
                   base::Unretained(this))));
  }

  ukm::TestUkmRecorder& test_ukm_recorder() { return test_ukm_recorder_; }

 private:
  // NavigationThrottleRegistry:
  NavigationHandle& GetNavigationHandle() override { return handle_; }
  void AddThrottle(
      std::unique_ptr<NavigationThrottle> navigation_throttle) override {
    throttles_.push_back(std::move(navigation_throttle));
  }

  MOCK_METHOD(bool, HasThrottle, (const std::string& name), (override));
  MOCK_METHOD(bool, EraseThrottleForTesting, (const std::string& name),
              (override));

  // NavigationThrottleRegistryBase:
  void OnEventProcessed(
      NavigationThrottleEvent event,
      NavigationThrottle::ThrottleCheckResult result) override {
    DCHECK(!was_delegate_notified_);
    delegate_result_ = result;
    was_delegate_notified_ = true;
    observer_last_event_ = event;
  }
  void OnDeferProcessingNavigationEvent(
      NavigationThrottle* deferring_throttle) override {
    deferring_throttles_.insert(deferring_throttle);
  }
  std::vector<std::unique_ptr<NavigationThrottle>>& GetThrottles() override {
    return throttles_;
  }
  NavigationThrottle& GetThrottleAtIndex(size_t index) override {
    EXPECT_LT(index, throttles_.size());
    return *throttles_[index];
  }
  const std::set<raw_ptr<NavigationThrottle>>& GetDeferringThrottles()
      const override {
    return deferring_throttles_;
  }

  void ResetNavigationThrottleRunner() { runner_.reset(); }

  MockNavigationHandle handle_;
  std::vector<std::unique_ptr<NavigationThrottle>> throttles_;
  std::set<raw_ptr<NavigationThrottle>> deferring_throttles_;
  std::unique_ptr<NavigationThrottleRunner> runner_;
  NavigationThrottleEvent observer_last_event_ =
      NavigationThrottleEvent::kNoEvent;
  bool was_delegate_notified_ = false;
  NavigationThrottle::ThrottleCheckResult delegate_result_;
  ukm::TestAutoSetUkmRecorder test_ukm_recorder_;
};

class NavigationThrottleRunnerTestWithEvent
    : public NavigationThrottleRunnerTest,
      public testing::WithParamInterface<NavigationThrottleEvent> {
 public:
  NavigationThrottleRunnerTestWithEvent() = default;
  ~NavigationThrottleRunnerTestWithEvent() override = default;
  void SetUp() override {
    NavigationThrottleRunnerTest::SetUp();
    event_ = GetParam();
  }

  NavigationThrottleEvent event() const { return event_; }

  void CheckNotified(TestNavigationThrottle* throttle) {
    CheckNotifiedOfEvent(throttle, event());
  }

 private:
  NavigationThrottleEvent event_;
};

// Checks that a navigation deferred by a NavigationThrottle can be properly
// resumed.
TEST_P(NavigationThrottleRunnerTestWithEvent, ResumeDeferred) {
  TestNavigationThrottle* test_throttle =
      CreateTestNavigationThrottle(NavigationThrottle::DEFER);
  CheckNotNotified(test_throttle);

  // Simulate the event. The request should be deferred. The observer
  // should not have been notified.
  SimulateEvent(event());
  CheckNotified(test_throttle);
  EXPECT_TRUE(is_deferring());
  EXPECT_FALSE(was_delegate_notified());

  // Resume the request. It should no longer be deferred and the observer
  // should have been notified.
  Resume();
  CheckNotified(test_throttle);
  EXPECT_FALSE(is_deferring());
  EXPECT_TRUE(was_delegate_notified());
  EXPECT_EQ(NavigationThrottle::PROCEED, delegate_result());
  EXPECT_EQ(event(), observer_last_event());
}

// Checks that a NavigationThrottleRunner can be safely deleted by the execution
// of one of its NavigationThrottle.
TEST_P(NavigationThrottleRunnerTestWithEvent, DeletionByNavigationThrottle) {
  AddDeletingNavigationThrottle();
  SimulateEvent(event());
  EXPECT_EQ(nullptr, runner());
  EXPECT_FALSE(was_delegate_notified());
}

// Checks that a NavigationThrottleRunner can be safely deleted by the execution
// of one of its NavigationThrottle following a call to
// ResumeProcessingNavigationEvent.
TEST_P(NavigationThrottleRunnerTestWithEvent,
       DeletionByNavigationThrottleAfterResume) {
  CreateTestNavigationThrottle(NavigationThrottle::DEFER);
  AddDeletingNavigationThrottle();
  SimulateEvent(event());
  EXPECT_NE(nullptr, runner());
  Resume();
  EXPECT_EQ(nullptr, runner());
  EXPECT_FALSE(was_delegate_notified());
}

INSTANTIATE_TEST_SUITE_P(
    AllEvents,
    NavigationThrottleRunnerTestWithEvent,
    ::testing::Values(NavigationThrottleEvent::kWillStartRequest,
                      NavigationThrottleEvent::kWillRedirectRequest,
                      NavigationThrottleEvent::kWillFailRequest,
                      NavigationThrottleEvent::kWillProcessResponse,
                      NavigationThrottleEvent::kWillCommitWithoutUrlLoader));

class NavigationThrottleRunnerTestWithEventAndAction
    : public NavigationThrottleRunnerTest,
      public testing::WithParamInterface<
          std::tuple<NavigationThrottleEvent,
                     NavigationThrottle::ThrottleAction>> {
 public:
  NavigationThrottleRunnerTestWithEventAndAction() = default;
  ~NavigationThrottleRunnerTestWithEventAndAction() override = default;
  void SetUp() override {
    NavigationThrottleRunnerTest::SetUp();
    std::tie(event_, action_) = GetParam();
  }
  NavigationThrottleEvent event() const { return event_; }
  NavigationThrottle::ThrottleAction action() const { return action_; }

  void CheckNotified(TestNavigationThrottle* throttle) {
    CheckNotifiedOfEvent(throttle, event());
  }

 private:
  NavigationThrottleEvent event_;
  NavigationThrottle::ThrottleAction action_;
};

// Checks that a NavigationThrottle asking during to defer
// followed by another NavigationThrottle behave correctly.
TEST_P(NavigationThrottleRunnerTestWithEventAndAction, DeferThenAction) {
  TestNavigationThrottle* defer_throttle =
      CreateTestNavigationThrottle(NavigationThrottle::DEFER);
  TestNavigationThrottle* test_throttle =
      CreateTestNavigationThrottle(action());
  CheckNotNotified(defer_throttle);
  CheckNotNotified(test_throttle);

  // Simulate the event. The request should be deferred. The observer
  // should not have been notified. The second throttle should not have been
  // notified.
  SimulateEvent(event());
  CheckNotified(defer_throttle);
  CheckNotNotified(test_throttle);
  EXPECT_TRUE(is_deferring());
  EXPECT_FALSE(was_delegate_notified());

  // Resume the request. It should no longer be deferred and the observer
  // should have been notified. The second throttle should have been notified.
  Resume();
  CheckNotified(defer_throttle);
  CheckNotified(test_throttle);
  EXPECT_FALSE(is_deferring());
  EXPECT_TRUE(was_delegate_notified());
  EXPECT_EQ(action(), delegate_result().action());
  EXPECT_EQ(event(), observer_last_event());
}

// Checks that a NavigationThrottle asking to cancel followed by a
// NavigationThrottle asking to proceed behave correctly. The navigation will
// be stopped directly, and the second throttle will not be called.
TEST_P(NavigationThrottleRunnerTestWithEventAndAction, CancelThenProceed) {
  if (action() == NavigationThrottle::PROCEED) {
    return;
  }
  TestNavigationThrottle* test_throttle =
      CreateTestNavigationThrottle(action());
  TestNavigationThrottle* proceed_throttle =
      CreateTestNavigationThrottle(NavigationThrottle::PROCEED);
  CheckNotNotified(test_throttle);
  CheckNotNotified(proceed_throttle);

  // Simulate the event. The request should be not be deferred. The observer
  // should have been notified. The second throttle should not have been
  // notified.
  SimulateEvent(event());
  CheckNotified(test_throttle);
  CheckNotNotified(proceed_throttle);
  EXPECT_FALSE(is_deferring());
  EXPECT_TRUE(was_delegate_notified());
  EXPECT_EQ(action(), delegate_result().action());
  EXPECT_EQ(event(), observer_last_event());
}

// Checks that a NavigationThrottle asking to proceed followed by a
// NavigationThrottle asking to cancel behave correctly.
// Both throttles will be called, and the request will be cancelled.
TEST_P(NavigationThrottleRunnerTestWithEventAndAction, ProceedThenCancel) {
  if (action() == NavigationThrottle::PROCEED) {
    return;
  }
  TestNavigationThrottle* proceed_throttle =
      CreateTestNavigationThrottle(NavigationThrottle::PROCEED);
  TestNavigationThrottle* test_throttle =
      CreateTestNavigationThrottle(action());
  CheckNotNotified(test_throttle);
  CheckNotNotified(proceed_throttle);

  // Simulate the event. The request should be not be deferred. The observer
  // should have been notified.
  SimulateEvent(event());
  CheckNotified(proceed_throttle);
  CheckNotified(test_throttle);
  EXPECT_FALSE(is_deferring());
  EXPECT_TRUE(was_delegate_notified());
  EXPECT_EQ(action(), delegate_result().action());
  EXPECT_EQ(event(), observer_last_event());
}

// Checks that a NavigationThrottle being deferred and resumed records UKM about
// the deferral.
TEST_P(NavigationThrottleRunnerTestWithEventAndAction, DeferRecordsUKM) {
  TestNavigationThrottle* defer_throttle =
      CreateTestNavigationThrottle(NavigationThrottle::DEFER);
  CheckNotNotified(defer_throttle);

  // Simulate the event. The request should be deferred.
  SimulateEvent(event());
  CheckNotified(defer_throttle);
  EXPECT_TRUE(is_deferring());

  // Resume the request. This should record UKM.
  Resume();

  // There should be one entry with name hash matching the logging name and
  // event that is being run. Ignore the time for testing as it is variable, and
  // even possibly 0.
  const auto& entries = test_ukm_recorder().GetEntriesByName(
      ukm::builders::NavigationThrottleDeferredTime::kEntryName);
  EXPECT_EQ(1u, entries.size());
  for (const ukm::mojom::UkmEntry* entry : entries) {
    EXPECT_EQ(*ukm::TestUkmRecorder::GetEntryMetric(
                  entry, ukm::builders::NavigationThrottleDeferredTime::
                             kNavigationThrottleEventTypeName),
              static_cast<int64_t>(event()));
    EXPECT_EQ(*ukm::TestUkmRecorder::GetEntryMetric(
                  entry, ukm::builders::NavigationThrottleDeferredTime::
                             kNavigationThrottleNameHashName),
              static_cast<int64_t>(
                  base::HashMetricName(defer_throttle->GetNameForLogging())));
  }
}

INSTANTIATE_TEST_SUITE_P(
    AllEvents,
    NavigationThrottleRunnerTestWithEventAndAction,
    ::testing::Combine(
        ::testing::Values(NavigationThrottleEvent::kWillStartRequest,
                          NavigationThrottleEvent::kWillRedirectRequest,
                          NavigationThrottleEvent::kWillFailRequest,
                          NavigationThrottleEvent::kWillProcessResponse,
                          NavigationThrottleEvent::kWillCommitWithoutUrlLoader),
        ::testing::Values(NavigationThrottle::PROCEED,
                          NavigationThrottle::CANCEL,
                          NavigationThrottle::CANCEL_AND_IGNORE,
                          NavigationThrottle::BLOCK_REQUEST,
                          NavigationThrottle::BLOCK_REQUEST_AND_COLLAPSE,
                          NavigationThrottle::BLOCK_RESPONSE)));

class NavigationThrottleRunnerTestWithEventAndError
    : public NavigationThrottleRunnerTest,
      public testing::WithParamInterface<
          std::tuple<NavigationThrottleEvent,
                     net::Error,
                     std::optional<std::string>>> {
 public:
  NavigationThrottleRunnerTestWithEventAndError() = default;
  ~NavigationThrottleRunnerTestWithEventAndError() override = default;
  void SetUp() override {
    NavigationThrottleRunnerTest::SetUp();
    std::tie(event_, error_, custom_error_page_) = GetParam();
  }
  NavigationThrottleEvent event() const { return event_; }
  net::Error error() const { return error_; }
  const std::optional<std::string>& custom_error_page() const {
    return custom_error_page_;
  }

  void CheckNotified(TestNavigationThrottle* throttle) {
    CheckNotifiedOfEvent(throttle, event());
  }

 private:
  NavigationThrottleEvent event_;
  net::Error error_;
  std::optional<std::string> custom_error_page_ = std::nullopt;
};

// Checks that the NavigationThrottleRunner correctly propagates a
// ThrottleCheckResult with a custom error page and/or error code to its
// delegate.
TEST_P(NavigationThrottleRunnerTestWithEventAndError, CustomNetError) {
  SCOPED_TRACE(::testing::Message()
               << "Event: " << static_cast<int>(event())
               << " Error: " << error() << " Custom error page: "
               << (custom_error_page().has_value() ? custom_error_page().value()
                                                   : ""));
  NavigationThrottle::ThrottleCheckResult result(NavigationThrottle::CANCEL,
                                                 error());
  if (custom_error_page().has_value()) {
    result = NavigationThrottle::ThrottleCheckResult(
        NavigationThrottle::CANCEL, error(), custom_error_page().value());
  }
  TestNavigationThrottle* test_throttle = CreateTestNavigationThrottle(result);
  CheckNotNotified(test_throttle);

  // Simulate the event. The request should not be deferred. The
  // callback should have been called.
  SimulateEvent(event());
  EXPECT_FALSE(is_deferring());
  EXPECT_TRUE(was_delegate_notified());
  EXPECT_EQ(NavigationThrottle::CANCEL, delegate_result().action());
  EXPECT_EQ(error(), delegate_result().net_error_code());
  EXPECT_EQ(custom_error_page().has_value(),
            delegate_result().error_page_content().has_value());
  if (custom_error_page().has_value()) {
    EXPECT_EQ(custom_error_page().value(),
              delegate_result().error_page_content().value());
  }
  CheckNotified(test_throttle);
}

INSTANTIATE_TEST_SUITE_P(
    AllEvents,
    NavigationThrottleRunnerTestWithEventAndError,
    ::testing::Combine(
        ::testing::Values(NavigationThrottleEvent::kWillStartRequest,
                          NavigationThrottleEvent::kWillRedirectRequest,
                          NavigationThrottleEvent::kWillFailRequest,
                          NavigationThrottleEvent::kWillProcessResponse,
                          NavigationThrottleEvent::kWillCommitWithoutUrlLoader),
        ::testing::Values(net::ERR_BLOCKED_BY_ADMINISTRATOR, net::ERR_ABORTED),
        ::testing::Values(std::nullopt, "<html><body>test</body></html>")));

}  // namespace content
