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

#include "components/feedback/feedback_uploader.h"

#include <string>

#include "base/files/scoped_temp_dir.h"
#include "base/metrics/field_trial.h"
#include "base/run_loop.h"
#include "base/task/task_traits.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "components/feedback/feedback_common.h"
#include "components/feedback/feedback_constants.h"
#include "components/variations/net/variations_http_headers.h"
#include "components/variations/scoped_variations_ids_provider.h"
#include "components/variations/variations_associated_data.h"
#include "components/variations/variations_ids_provider.h"
#include "net/http/http_util.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace feedback {

namespace {

constexpr base::TimeDelta kTestRetryDelay = base::Milliseconds(1);

constexpr char kFeedbackPostUrl[] =
    "https://www.google.com/tools/feedback/chrome/__submit";

void QueueReport(FeedbackUploader* uploader,
                 const std::string& report_data,
                 bool has_email = true,
                 int product_id = 0) {
  uploader->QueueReport(std::make_unique<std::string>(report_data), has_email,
                        product_id);
}

class TestFeedbackUploader final : public FeedbackUploader {
 public:
  TestFeedbackUploader(
      bool is_off_the_record,
      const base::FilePath& state_path,
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
      : FeedbackUploader(is_off_the_record,
                         state_path,
                         std::move(url_loader_factory)) {
    SetFeedbackGURLForTesting(GURL(kFeedbackPostUrl));
  }
  TestFeedbackUploader(const TestFeedbackUploader&) = delete;
  TestFeedbackUploader& operator=(const TestFeedbackUploader&) = delete;

  ~TestFeedbackUploader() override = default;

  base::WeakPtr<FeedbackUploader> AsWeakPtr() override {
    return weak_ptr_factory_.GetWeakPtr();
  }

 private:
  base::WeakPtrFactory<TestFeedbackUploader> weak_ptr_factory_{this};
};

// Stand-in for the FeedbackUploaderChrome class that adds a fake bearer token
// to the request.
class MockFeedbackUploaderChrome final : public FeedbackUploader {
 public:
  MockFeedbackUploaderChrome(
      bool is_off_the_record,
      const base::FilePath& state_path,
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
      : FeedbackUploader(is_off_the_record, state_path, url_loader_factory) {}

  void AppendExtraHeadersToUploadRequest(
      network::ResourceRequest* resource_request) override {
    resource_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
                                        "Bearer abcdefg");
  }

  base::WeakPtr<FeedbackUploader> AsWeakPtr() override {
    return weak_ptr_factory_.GetWeakPtr();
  }

 private:
  base::WeakPtrFactory<MockFeedbackUploaderChrome> weak_ptr_factory_{this};
};

}  // namespace

class FeedbackUploaderDispatchTest : public ::testing::Test {
 protected:
  FeedbackUploaderDispatchTest()
      : shared_url_loader_factory_(
            base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
                &test_url_loader_factory_)) {
    EXPECT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
  }

  FeedbackUploaderDispatchTest(const FeedbackUploaderDispatchTest&) = delete;
  FeedbackUploaderDispatchTest& operator=(const FeedbackUploaderDispatchTest&) =
      delete;

  ~FeedbackUploaderDispatchTest() override {
    // Clean up registered ids.
    variations::test::ClearAllVariationIDs();
  }

  // Registers a field trial with the specified name and group and an associated
  // google web property variation id.
  void CreateFieldTrialWithId(const std::string& trial_name,
                              const std::string& group_name,
                              int variation_id) {
    variations::AssociateGoogleVariationIDForTesting(
        variations::GOOGLE_WEB_PROPERTIES_ANY_CONTEXT, trial_name, group_name,
        static_cast<variations::VariationID>(variation_id));
    base::FieldTrialList::CreateFieldTrial(trial_name, group_name)->Activate();
  }

  scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory() {
    return shared_url_loader_factory_;
  }

  network::TestURLLoaderFactory* test_url_loader_factory() {
    return &test_url_loader_factory_;
  }

  const base::FilePath& state_path() { return scoped_temp_dir_.GetPath(); }

 private:
  base::test::TaskEnvironment task_environment_;
  base::ScopedTempDir scoped_temp_dir_;
  variations::test::ScopedVariationsIdsProvider scoped_variations_ids_provider_{
      variations::VariationsIdsProvider::Mode::kUseSignedInState};
  network::TestURLLoaderFactory test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_;
};

TEST_F(FeedbackUploaderDispatchTest, VariationHeaders) {
  // Register a trial and variation id, so that there is data in variations
  // headers.
  CreateFieldTrialWithId("Test", "Group1", 123);

  TestFeedbackUploader uploader(/*is_off_the_record=*/false, state_path(),
                                shared_url_loader_factory());

  net::HttpRequestHeaders headers;
  test_url_loader_factory()->SetInterceptor(
      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
        EXPECT_TRUE(variations::HasVariationsHeader(request));
      }));

  QueueReport(&uploader, "test");
  base::RunLoop().RunUntilIdle();
}

TEST_F(FeedbackUploaderDispatchTest, VariationHeadersForProductID) {
  // Register a trial and variation id, so that there is data in variations
  // headers.
  CreateFieldTrialWithId("Test", "Group1", 123);

  TestFeedbackUploader uploader(/*is_off_the_record=*/false, state_path(),
                                shared_url_loader_factory());

  net::HttpRequestHeaders headers;
  test_url_loader_factory()->SetInterceptor(
      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
        EXPECT_TRUE(variations::HasVariationsHeader(request));
      }));

  QueueReport(&uploader, "test", /*has_email=*/false, /*product_id=*/208);
  base::RunLoop().RunUntilIdle();
}

TEST_F(FeedbackUploaderDispatchTest, NoVariationHeadersForOrcaProductID) {
  // Register a trial and variation id, so that there is data in variations
  // headers.
  CreateFieldTrialWithId("Test", "Group1", 123);

  TestFeedbackUploader uploader(/*is_off_the_record=*/false, state_path(),
                                shared_url_loader_factory());

  net::HttpRequestHeaders headers;
  test_url_loader_factory()->SetInterceptor(
      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
        EXPECT_FALSE(variations::HasVariationsHeader(request));
      }));

  QueueReport(&uploader, "test", /*has_email=*/false,
              /*product_id=*/feedback::kOrcaFeedbackProductId);
  base::RunLoop().RunUntilIdle();
}

// Test that the bearer token is present if there is an email address present in
// the report.
TEST_F(FeedbackUploaderDispatchTest, BearerTokenWithEmail) {
  MockFeedbackUploaderChrome uploader(/*is_off_the_record=*/false, state_path(),
                                      shared_url_loader_factory());

  test_url_loader_factory()->SetInterceptor(
      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
        EXPECT_TRUE(request.headers.HasHeader("Authorization"));
      }));

  QueueReport(&uploader, "test", /*has_email=*/true);
  base::RunLoop().RunUntilIdle();
}

TEST_F(FeedbackUploaderDispatchTest, BearerTokenNoEmail) {
  MockFeedbackUploaderChrome uploader(/*is_off_the_record=*/false, state_path(),
                                      shared_url_loader_factory());

  test_url_loader_factory()->SetInterceptor(
      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
        EXPECT_FALSE(request.headers.HasHeader("Authorization"));
      }));

  QueueReport(&uploader, "test", /*has_email=*/false);
  base::RunLoop().RunUntilIdle();
}

TEST_F(FeedbackUploaderDispatchTest, 204Response) {
  FeedbackUploader::SetMinimumRetryDelayForTesting(kTestRetryDelay);
  TestFeedbackUploader uploader(/*is_off_the_record=*/false, state_path(),
                                shared_url_loader_factory());

  EXPECT_EQ(kTestRetryDelay, uploader.retry_delay());
  // Successful reports should not introduce any retries, and should not
  // increase the backoff delay.
  auto head = network::mojom::URLResponseHead::New();
  std::string headers("HTTP/1.1 204 No Content\n\n");
  head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
      net::HttpUtil::AssembleRawHeaders(headers));
  network::URLLoaderCompletionStatus status;
  test_url_loader_factory()->AddResponse(GURL(kFeedbackPostUrl),
                                         std::move(head), "", status);
  QueueReport(&uploader, "Successful report");
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(kTestRetryDelay, uploader.retry_delay());
  EXPECT_TRUE(uploader.QueueEmpty());
}

TEST_F(FeedbackUploaderDispatchTest, 400Response) {
  FeedbackUploader::SetMinimumRetryDelayForTesting(kTestRetryDelay);
  TestFeedbackUploader uploader(/*is_off_the_record=*/false, state_path(),
                                shared_url_loader_factory());

  EXPECT_EQ(kTestRetryDelay, uploader.retry_delay());
  // Failed reports due to client errors are not retried. No backoff delay
  // should be doubled.
  auto head = network::mojom::URLResponseHead::New();
  std::string headers("HTTP/1.1 400 Bad Request\n\n");
  head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
      net::HttpUtil::AssembleRawHeaders(headers));
  network::URLLoaderCompletionStatus status;
  test_url_loader_factory()->AddResponse(GURL(kFeedbackPostUrl),
                                         std::move(head), "", status);
  QueueReport(&uploader, "Client error failed report");
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(kTestRetryDelay, uploader.retry_delay());
  EXPECT_TRUE(uploader.QueueEmpty());
}

TEST_F(FeedbackUploaderDispatchTest, 500Response) {
  FeedbackUploader::SetMinimumRetryDelayForTesting(kTestRetryDelay);
  TestFeedbackUploader uploader(/*is_off_the_record=*/false, state_path(),
                                shared_url_loader_factory());

  EXPECT_EQ(kTestRetryDelay, uploader.retry_delay());
  // Failed reports due to server errors are retried.
  auto head = network::mojom::URLResponseHead::New();
  std::string headers("HTTP/1.1 500 Server Error\n\n");
  head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
      net::HttpUtil::AssembleRawHeaders(headers));
  network::URLLoaderCompletionStatus status;
  test_url_loader_factory()->AddResponse(GURL(kFeedbackPostUrl),
                                         std::move(head), "", status);
  QueueReport(&uploader, "Server error failed report");
  base::RunLoop().RunUntilIdle();

  EXPECT_LT(kTestRetryDelay, uploader.retry_delay());
  EXPECT_FALSE(uploader.QueueEmpty());
}

}  // namespace feedback
