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

#include "extensions/browser/api/feedback_private/feedback_service.h"

#include "base/check.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/mock_callback.h"
#include "build/build_config.h"
#include "components/feedback/feedback_data.h"
#include "components/feedback/feedback_report.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/api/feedback_private/feedback_private_delegate.h"
#include "extensions/browser/api/feedback_private/mock_feedback_service.h"
#include "extensions/browser/api_unittest.h"
#include "extensions/common/api/feedback_private.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/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "base/base64.h"
#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
#include "components/user_manager/fake_user_manager.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/user_manager.h"
#include "crypto/hpke.h"
#include "crypto/keypair.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

namespace extensions {

using feedback::FeedbackData;
using feedback::FeedbackUploader;
using testing::_;
using testing::StrictMock;

namespace {

const std::string kFakeKey = "fake key";
const std::string kFakeValue = "fake value";
const std::string kTabTitleValue = "some sensitive info";
#if BUILDFLAG(IS_CHROMEOS)
constexpr char kVariationsFetchHpkeKey[] =
    "https://www.gstatic.com/chromeos-feedback-variations-encryption-key/"
    "public_keyset.json";
const std::string kTestPublicKeyResponseBody =
    "{\"primaryKeyId\":123,\"key\":[{\"keyData\":{\"typeUrl\":\"type."
    "googleapis.com/"
    "google.crypto.tink.HpkePublicKey\",\"value\":"
    "\"EgYIARABGAIaIKeVK4N3icUhM5YF+Pp5S6PWAg9OlY8zP9oLL9qv4IYS\","
    "\"keyMaterialType\":\"ASYMMETRIC_PUBLIC\"},\"status\":\"ENABLED\","
    "\"keyId\":456,\"outputPrefixType\":\"RAW\"}]}";
const std::string kTestBase64HpkePrivateKey =
    "IHbZB+CCrEXra2WGQx/jFQ+a0NSpVCauqy2uC9NH8Hs=";
// Command line variation string returned during testing.
const std::string kTestCommandLineVariations =
    " --enable-features=\"*TestBlinkFeatureDefault,"
    "TestFeatureForBrowserTest1\" "
    "--disable-features=\"TestFeatureForBrowserTest2\" "
    "--disable-field-trial-config";
#endif  // BUILDFLAG(IS_CHROMEOS)

class MockFeedbackUploader : public FeedbackUploader {
 public:
  MockFeedbackUploader(
      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) {}

  MOCK_METHOD(void,
              QueueReport,
              (std::unique_ptr<std::string>, bool, int),
              (override));

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

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

class MockFeedbackPrivateDelegate : public FeedbackPrivateDelegate {
 public:
  MockFeedbackPrivateDelegate() {
    ON_CALL(*this, FetchSystemInformation)
        .WillByDefault([](content::BrowserContext* context,
                          system_logs::SysLogsFetcherCallback callback) {
          auto sys_info = std::make_unique<system_logs::SystemLogsResponse>();
          sys_info->emplace(kFakeKey, kFakeValue);
          sys_info->emplace(feedback::FeedbackReport::kMemUsageWithTabTitlesKey,
                            kTabTitleValue);
          std::move(callback).Run(std::move(sys_info));
        });
#if BUILDFLAG(IS_CHROMEOS)
    ON_CALL(*this, FetchExtraLogs)
        .WillByDefault([](scoped_refptr<FeedbackData> feedback_data,
                          FetchExtraLogsCallback callback) {
          std::move(callback).Run(feedback_data);
        });
#endif  // BUILDFLAG(IS_CHROMEOS)
  }

  ~MockFeedbackPrivateDelegate() override = default;

  MOCK_METHOD(base::DictValue,
              GetStrings,
              (content::BrowserContext*, bool),
              (const, override));
  MOCK_METHOD(void,
              FetchSystemInformation,
              (content::BrowserContext*, system_logs::SysLogsFetcherCallback),
              (const, override));
#if BUILDFLAG(IS_CHROMEOS)
  MOCK_METHOD(std::unique_ptr<system_logs::SystemLogsSource>,
              CreateSingleLogSource,
              (api::feedback_private::LogSource),
              (const, override));
  MOCK_METHOD(void,
              FetchExtraLogs,
              (scoped_refptr<feedback::FeedbackData>, FetchExtraLogsCallback),
              (const, override));
  MOCK_METHOD(api::feedback_private::LandingPageType,
              GetLandingPageType,
              (const feedback::FeedbackData&),
              (const, override));
#endif  // BUILDFLAG(IS_CHROMEOS)
  MOCK_METHOD(std::string,
              GetSignedInUserEmail,
              (content::BrowserContext*),
              (const, override));
  MOCK_METHOD(void, NotifyFeedbackDelayed, (), (const, override));
  MOCK_METHOD(feedback::FeedbackUploader*,
              GetFeedbackUploaderForContext,
              (content::BrowserContext*),
              (const, override));
  MOCK_METHOD(void,
              OpenFeedback,
              (content::BrowserContext*, api::feedback_private::FeedbackSource),
              (const, override));
};

#if BUILDFLAG(IS_CHROMEOS)
const FeedbackCommon::AttachedFile* FindAttachment(
    std::string_view name,
    const scoped_refptr<FeedbackData>& feedback_data) {
  size_t num_attachments = feedback_data->attachments();
  for (size_t i = 0; i < num_attachments; i++) {
    const FeedbackCommon::AttachedFile* file = feedback_data->attachment(i);
    if (file->name == name) {
      return file;
    }
  }
  return nullptr;
}

void VerifyAttachment(std::string_view name,
                      std::string_view data,
                      const scoped_refptr<FeedbackData>& feedback_data) {
  const auto* attachment = FindAttachment(name, feedback_data);
  ASSERT_TRUE(attachment);
  EXPECT_EQ(name, attachment->name);

  base::span<const uint8_t> byte_span = base::as_byte_span(data);
  std::vector<uint8_t> expected_data =
      std::vector<uint8_t>(byte_span.begin(), byte_span.end());

  EXPECT_EQ(expected_data, attachment->data);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace

class FeedbackServiceTest : public ApiUnitTest {
 protected:
  FeedbackServiceTest() {
#if BUILDFLAG(IS_CHROMEOS)
    test_url_loader_factory_.AddResponse(
        kVariationsFetchHpkeKey, kTestPublicKeyResponseBody, net::HTTP_OK);
#endif  // BUILDFLAG(IS_CHROMEOS)
    test_shared_loader_factory_ =
        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
            &test_url_loader_factory_);
    CHECK(scoped_temp_dir_.CreateUniqueTempDir());
    mock_uploader_ = std::make_unique<StrictMock<MockFeedbackUploader>>(
        /*is_off_the_record=*/false, scoped_temp_dir_.GetPath(),
        test_shared_loader_factory_);
    feedback_data_ = base::MakeRefCounted<FeedbackData>(
        mock_uploader_->AsWeakPtr(), nullptr);
#if BUILDFLAG(IS_CHROMEOS)
    auto fake_user_manager = std::make_unique<user_manager::FakeUserManager>();
    scoped_user_manager_ = std::make_unique<user_manager::ScopedUserManager>(
        std::move(fake_user_manager));
#endif  // BUILDFLAG(IS_CHROMEOS)
  }

  ~FeedbackServiceTest() override = default;

  void TestSendFeedbackConcerningTabTitles(bool send_tab_titles) {
    feedback_data_->AddLog(kFakeKey, kFakeValue);
    feedback_data_->AddLog(feedback::FeedbackReport::kMemUsageWithTabTitlesKey,
                           kTabTitleValue);
    const FeedbackParams params{/*is_internal_email=*/false,
                                /*load_system_info=*/false,
                                /*send_tab_titles=*/send_tab_titles,
                                /*send_histograms=*/true,
                                /*send_bluetooth_logs=*/false,
                                /*send_wifi_debug_logs=*/false,
                                /*send_autofill_metadata=*/false};

    EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
    base::MockCallback<SendFeedbackCallback> mock_callback;
    EXPECT_CALL(mock_callback, Run(true));

    auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
#if BUILDFLAG(IS_CHROMEOS)
    EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);
#endif  // BUILDFLAG(IS_CHROMEOS)

    auto feedback_service = base::MakeRefCounted<FeedbackService>(
        browser_context(), mock_delegate.get());
    RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
    EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));
  }

#if BUILDFLAG(IS_CHROMEOS)
  void TestSendFeedbackConcerningWifiDebugLogs(bool send_wifi_debug_logs) {
    const FeedbackParams params{/*is_internal_email=*/false,
                                /*load_system_info=*/true,
                                /*send_tab_titles=*/false,
                                /*send_histograms=*/false,
                                /*send_bluetooth_logs=*/false,
                                /*send_wifi_debug_logs=*/send_wifi_debug_logs,
                                /*send_autofill_metadata=*/false};

    // Create a test file in sub directory "wifi".
    const base::FilePath test_file_dir =
        scoped_temp_dir_.GetPath().Append("wifi");
    ASSERT_TRUE(base::CreateDirectory(test_file_dir));

    const base::FilePath test_file =
        test_file_dir.Append("wifi_firmware_dumps.tar.zst");
    ASSERT_TRUE(base::WriteFile(test_file, "Test file content"));

    EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
    base::MockCallback<SendFeedbackCallback> mock_callback;
    EXPECT_CALL(mock_callback, Run(true));

    auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
    EXPECT_CALL(*mock_delegate, FetchSystemInformation(_, _)).Times(1);
    EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);

    if (send_wifi_debug_logs) {
      ash::DebugDaemonClient::InitializeFake();
    }
    auto feedback_service = base::MakeRefCounted<FeedbackService>(
        browser_context(), mock_delegate.get());
    feedback_service->SetLogFilesRootPathForTesting(scoped_temp_dir_.GetPath());

    RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
    if (ash::DebugDaemonClient::Get()) {
      ash::DebugDaemonClient::Shutdown();
    }
    EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));

    // Verify the attachment is added if and only if send_wifi_debug_logs is
    // true.
    constexpr char kWifiDumpName[] = "wifi_firmware_dumps.tar.zst";
    if (send_wifi_debug_logs) {
      VerifyAttachment(kWifiDumpName, "TestData", feedback_data_);
    } else {
      EXPECT_FALSE(FindAttachment(kWifiDumpName, feedback_data_));
    }
  }

  void TestSendFeedbackConcerningBluetoothDebugLogs(bool send_bluetooth_logs) {
    const FeedbackParams params{/*is_internal_email=*/false,
                                /*load_system_info=*/true,
                                /*send_tab_titles=*/false,
                                /*send_histograms=*/false,
                                /*send_bluetooth_logs=*/send_bluetooth_logs,
                                /*send_wifi_debug_logs=*/false,
                                /*send_autofill_metadata=*/false};

    EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
    base::MockCallback<SendFeedbackCallback> mock_callback;
    EXPECT_CALL(mock_callback, Run(true));

    auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
    EXPECT_CALL(*mock_delegate, FetchSystemInformation(_, _)).Times(1);
    EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);

    if (send_bluetooth_logs) {
      ash::DebugDaemonClient::InitializeFake();
    }
    auto feedback_service = base::MakeRefCounted<FeedbackService>(
        browser_context(), mock_delegate.get());
    feedback_service->SetLogFilesRootPathForTesting(scoped_temp_dir_.GetPath());

    RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
    if (ash::DebugDaemonClient::Get()) {
      ash::DebugDaemonClient::Shutdown();
    }
    EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));

    // Verify the attachment is added if and only if send_bluetooth_logs is
    // true.
    constexpr char kBluetoothDumpName[] = "bluetooth_firmware_dumps.tar.zst";
    if (send_bluetooth_logs) {
      VerifyAttachment(kBluetoothDumpName, "TestData", feedback_data_);
    } else {
      EXPECT_FALSE(FindAttachment(kBluetoothDumpName, feedback_data_));
    }
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  void RunUntilFeedbackIsSent(scoped_refptr<FeedbackService> feedback_service,
                              const FeedbackParams& params,
                              SendFeedbackCallback mock_callback) {
    feedback_service->RedactThenSendFeedback(params, feedback_data_,
                                             std::move(mock_callback));
    base::ThreadPoolInstance::Get()->FlushForTesting();
    task_environment()->RunUntilIdle();
  }

#if BUILDFLAG(IS_CHROMEOS)
  std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_;
#endif  // BUILDFLAG(IS_CHROMEOS)

  base::ScopedTempDir scoped_temp_dir_;
  network::TestURLLoaderFactory test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
  std::unique_ptr<StrictMock<MockFeedbackUploader>> mock_uploader_;
  scoped_refptr<FeedbackData> feedback_data_;
};

TEST_F(FeedbackServiceTest, SendFeedbackWithoutSysInfo) {
  const FeedbackParams params{/*is_internal_email=*/false,
                              /*load_system_info=*/false,
                              /*send_tab_titles=*/true,
                              /*send_histograms=*/true,
                              /*send_bluetooth_logs=*/false,
                              /*send_wifi_debug_logs=*/false,
                              /*send_autofill_metadata=*/false};

  EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
  base::MockCallback<SendFeedbackCallback> mock_callback;
  EXPECT_CALL(mock_callback, Run(true));

  auto feedback_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
  auto feedback_service = base::MakeRefCounted<FeedbackService>(
      browser_context(), feedback_delegate.get());

  RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
}

TEST_F(FeedbackServiceTest, SendFeedbackLoadSysInfo) {
  const FeedbackParams params{/*is_internal_email=*/false,
                              /*load_system_info=*/true,
                              /*send_tab_titles=*/true,
                              /*send_histograms=*/true,
                              /*send_bluetooth_logs=*/false,
                              /*send_wifi_debug_logs=*/false,
                              /*send_autofill_metadata=*/false};

  EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
  base::MockCallback<SendFeedbackCallback> mock_callback;
  EXPECT_CALL(mock_callback, Run(true));

  auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
  EXPECT_CALL(*mock_delegate, FetchSystemInformation(_, _)).Times(1);
#if BUILDFLAG(IS_CHROMEOS)
  EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);
#endif  // BUILDFLAG(IS_CHROMEOS)

  auto feedback_service = base::MakeRefCounted<FeedbackService>(
      browser_context(), mock_delegate.get());

  RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
  EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));
  EXPECT_EQ(1u, feedback_data_->sys_info()->count(
                    feedback::FeedbackReport::kMemUsageWithTabTitlesKey));
}

// TODO(crbug.com/40908623): Re-enable this test
TEST_F(FeedbackServiceTest, DISABLED_SendFeedbackDoNotSendTabTitles) {
  TestSendFeedbackConcerningTabTitles(false);
  EXPECT_EQ(0u, feedback_data_->sys_info()->count(
                    feedback::FeedbackReport::kMemUsageWithTabTitlesKey));
}

TEST_F(FeedbackServiceTest, SendFeedbackDoSendTabTitles) {
  TestSendFeedbackConcerningTabTitles(true);
  EXPECT_EQ(1u, feedback_data_->sys_info()->count(
                    feedback::FeedbackReport::kMemUsageWithTabTitlesKey));
}

TEST_F(FeedbackServiceTest, SendFeedbackAutofillMetadata) {
  const FeedbackParams params{/*is_internal_email=*/true,
                              /*load_system_info=*/false,
                              /*send_tab_titles=*/false,
                              /*send_histograms=*/true,
                              /*send_bluetooth_logs=*/false,
                              /*send_wifi_debug_logs=*/false,
                              /*send_autofill_metadata=*/true};
  feedback_data_->set_autofill_metadata("Autofill Metadata");
  EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
  base::MockCallback<SendFeedbackCallback> mock_callback;
  EXPECT_CALL(mock_callback, Run(true));

  auto feedback_service =
      base::MakeRefCounted<FeedbackService>(browser_context(), nullptr);

  RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
}

#if BUILDFLAG(IS_CHROMEOS)
TEST_F(FeedbackServiceTest, SendFeedbackWithWifiDebugLogs) {
  TestSendFeedbackConcerningWifiDebugLogs(/*send_wifi_debug_logs=*/true);
}

TEST_F(FeedbackServiceTest, SendFeedbackWithoutWifiDebugLogs) {
  TestSendFeedbackConcerningWifiDebugLogs(/*send_wifi_debug_logs=*/false);
}

TEST_F(FeedbackServiceTest, SendFeedbackWithBluetoothDebugLogs) {
  TestSendFeedbackConcerningBluetoothDebugLogs(/*send_bluetooth_logs=*/true);
}

TEST_F(FeedbackServiceTest, SendFeedbackWithoutBluetoothDebugLogs) {
  TestSendFeedbackConcerningBluetoothDebugLogs(/*send_bluetooth_logs=*/false);
}

// Test that the feedback report contains the variations.binary encrypted
// properly.
TEST_F(FeedbackServiceTest, TestSendFeedbackWithVariationsBinary) {
  const FeedbackParams params{/*is_internal_email=*/false,
                              /*load_system_info=*/true,
                              /*send_tab_titles=*/false,
                              /*send_histograms=*/false,
                              /*send_bluetooth_logs=*/false,
                              /*send_wifi_debug_logs=*/false,
                              /*send_autofill_metadata=*/false};

  EXPECT_CALL(*mock_uploader_, QueueReport).Times(1);
  base::MockCallback<SendFeedbackCallback> mock_callback;
  EXPECT_CALL(mock_callback, Run(true));

  auto mock_delegate = std::make_unique<MockFeedbackPrivateDelegate>();
  EXPECT_CALL(*mock_delegate, FetchSystemInformation(_, _)).Times(1);
  EXPECT_CALL(*mock_delegate, FetchExtraLogs(_, _)).Times(1);

  auto feedback_service = base::MakeRefCounted<FeedbackService>(
      browser_context(), mock_delegate.get());
  feedback_service->SetUrlLoaderFactory(test_shared_loader_factory_);

  RunUntilFeedbackIsSent(feedback_service, params, mock_callback.Get());
  EXPECT_EQ(1u, feedback_data_->sys_info()->count(kFakeKey));

  // Initialize Hpke private key.
  std::optional<std::vector<uint8_t>> hpke_private_key =
      base::Base64Decode(kTestBase64HpkePrivateKey);
  const auto key = crypto::keypair::PrivateKey::FromX25519PrivateKey(
      base::span<const uint8_t, 32>(*hpke_private_key));

  // Get the encrypted file.
  constexpr char kVariationsBinary[] = "variations.binary";
  const FeedbackCommon::AttachedFile* variations_binary =
      FindAttachment(kVariationsBinary, feedback_data_);
  ASSERT_TRUE(variations_binary);
  std::vector<uint8_t> ciphertext(variations_binary->data.begin(),
                                  variations_binary->data.end());

  const crypto::hpke::HpkeParams kParams = {
      .kem = crypto::hpke::KemType::kX25519HkdfSha256,
      .kdf = crypto::hpke::KdfType::kHkdfSha256,
      .aead = crypto::hpke::AeadType::kAes256Gcm,
  };

  std::optional<std::vector<uint8_t>> plaintext =
      crypto::hpke::Open(kParams, key, ciphertext, /*info=*/{}, /*ad=*/{});
  std::string decrypted_string(plaintext->begin(), plaintext->end());

  // Final check.
  EXPECT_EQ(kTestCommandLineVariations, decrypted_string);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace extensions
