// 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 <memory>
#include <string>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/extensions/api/file_system/consent_provider_impl.h"
#include "chrome/test/base/testing_browser_process.h"
#include "components/prefs/testing_pref_service.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/user.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/manifest.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/mojom/dialog_button.mojom.h"

using extensions::file_system_api::ConsentProviderImpl;
using extensions::mojom::ManifestLocation;

namespace extensions {
namespace {

// Configurations and results for TestingConsentProviderDelegate, with directly
// accessible fields.
struct TestDelegateState {
  // Used to assign a fake dialog response.
  ui::mojom::DialogButton dialog_button = ui::mojom::DialogButton::kNone;

  // Used to assign fake result of detection the auto launch kiosk mode.
  bool is_auto_launched = false;

  // Used to set allowlisted components list with a single id.
  std::string allowlisted_component_id;

  // Counters to record calls.
  int show_dialog_counter = 0;
  int show_notification_counter = 0;
};

// Test implementation of ConsentProviderImpl::DelegateInterface that exposes
// states to a TestDelegateState instance.
class TestingConsentProviderDelegate
    : public ConsentProviderImpl::DelegateInterface {
 public:
  explicit TestingConsentProviderDelegate(TestDelegateState* state)
      : state_(state) {}

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

  ~TestingConsentProviderDelegate() override = default;

 private:
  // ConsentProviderImpl::DelegateInterface:
  void ShowDialog(content::RenderFrameHost* host,
                  const extensions::ExtensionId& extension_id,
                  const std::string& extension_name,
                  const std::string& volume_id,
                  const std::string& volume_label,
                  bool writable,
                  ConsentProviderImpl::ShowDialogCallback callback) override {
    ++state_->show_dialog_counter;
    std::move(callback).Run(state_->dialog_button);
  }

  // ConsentProviderImpl::DelegateInterface:
  void ShowNotification(const extensions::ExtensionId& extension_id,
                        const std::string& extension_name,
                        const std::string& volume_id,
                        const std::string& volume_label,
                        bool writable) override {
    ++state_->show_notification_counter;
  }

  // ConsentProviderImpl::DelegateInterface:
  bool IsAutoLaunched(const extensions::Extension& extension) override {
    return state_->is_auto_launched;
  }

  // ConsentProviderImpl::DelegateInterface:
  bool IsAllowlistedComponent(const extensions::Extension& extension) override {
    return state_->allowlisted_component_id.compare(extension.id()) == 0;
  }

  // Use raw_ptr since |state| is owned by owner.
  raw_ptr<TestDelegateState> state_;
};

// Rewrites result of a consent request from |result| to |log|.
void OnConsentReceived(ConsentProviderImpl::Consent* log,
                       const ConsentProviderImpl::Consent result) {
  *log = result;
}

}  // namespace

class FileSystemApiConsentProviderTest : public testing::Test {
 public:
  FileSystemApiConsentProviderTest() = default;

  void SetUp() override {
    user_manager_ = new ash::FakeChromeUserManager;
    scoped_user_manager_enabler_ =
        std::make_unique<user_manager::ScopedUserManager>(
            base::WrapUnique(user_manager_.get()));
  }

  void TearDown() override {
    scoped_user_manager_enabler_.reset();
    user_manager_ = nullptr;
  }

 protected:
  raw_ptr<ash::FakeChromeUserManager, DanglingUntriaged>
      user_manager_;  // Owned by the scope enabler.
  std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_enabler_;
  content::BrowserTaskEnvironment task_environment_;
};

TEST_F(FileSystemApiConsentProviderTest, ForNonKioskApps) {
  // Component apps are not granted unless they are allowlisted.
  {
    scoped_refptr<const Extension> component_extension(
        ExtensionBuilder("Test", ExtensionBuilder::Type::PLATFORM_APP)
            .SetLocation(ManifestLocation::kComponent)
            .Build());
    TestDelegateState state;
    ConsentProviderImpl provider(
        std::make_unique<TestingConsentProviderDelegate>(&state));
    EXPECT_FALSE(provider.IsGrantable(*component_extension));
  }

  // Allowlisted component apps are instantly granted access without asking
  // user.
  {
    scoped_refptr<const Extension> allowlisted_component_extension(
        ExtensionBuilder("Test", ExtensionBuilder::Type::PLATFORM_APP)
            .SetLocation(ManifestLocation::kComponent)
            .Build());
    TestDelegateState state;
    state.allowlisted_component_id = allowlisted_component_extension->id();
    ConsentProviderImpl provider(
        std::make_unique<TestingConsentProviderDelegate>(&state));
    EXPECT_TRUE(provider.IsGrantable(*allowlisted_component_extension));

    ConsentProviderImpl::Consent result = ConsentProvider::CONSENT_IMPOSSIBLE;
    provider.RequestConsent(nullptr, *allowlisted_component_extension.get(),
                            "Volume ID 1", "Volume Label 1",
                            true /* writable */,
                            base::BindOnce(&OnConsentReceived, &result));
    base::RunLoop().RunUntilIdle();

    EXPECT_EQ(0, state.show_dialog_counter);
    EXPECT_EQ(0, state.show_notification_counter);
    EXPECT_EQ(ConsentProvider::CONSENT_GRANTED, result);
  }

  // Non-component apps in non-kiosk mode will be rejected instantly, without
  // asking for user consent.
  {
    scoped_refptr<const Extension> non_component_extension(
        ExtensionBuilder("Test").Build());
    TestDelegateState state;
    ConsentProviderImpl provider(
        std::make_unique<TestingConsentProviderDelegate>(&state));
    EXPECT_FALSE(provider.IsGrantable(*non_component_extension));
  }
}

TEST_F(FileSystemApiConsentProviderTest, ForKioskApps) {
  // Non-component apps in auto-launch kiosk mode will be granted access
  // instantly without asking for user consent, but with a notification.
  {
    scoped_refptr<const Extension> auto_launch_kiosk_app(
        ExtensionBuilder("Test", ExtensionBuilder::Type::PLATFORM_APP)
            .SetManifestKey("kiosk_enabled", true)
            .SetManifestKey("kiosk_only", true)
            .Build());
    auto* auto_user = user_manager_->AddKioskChromeAppUser(
        AccountId::FromUserEmail(auto_launch_kiosk_app->id()));
    user_manager_->LoginUser(auto_user->GetAccountId());

    TestDelegateState state;
    state.is_auto_launched = true;
    ConsentProviderImpl provider(
        std::make_unique<TestingConsentProviderDelegate>(&state));
    EXPECT_TRUE(provider.IsGrantable(*auto_launch_kiosk_app));

    ConsentProviderImpl::Consent result = ConsentProvider::CONSENT_IMPOSSIBLE;
    provider.RequestConsent(
        nullptr, *auto_launch_kiosk_app.get(), "Volume ID 2", "Volume Label 2",
        true /* writable */, base::BindOnce(&OnConsentReceived, &result));
    base::RunLoop().RunUntilIdle();

    EXPECT_EQ(0, state.show_dialog_counter);
    EXPECT_EQ(1, state.show_notification_counter);
    EXPECT_EQ(ConsentProvider::CONSENT_GRANTED, result);
  }

  // Non-component apps in manual-launch kiosk mode will be granted access after
  // receiving approval from the user.
  scoped_refptr<const Extension> manual_launch_kiosk_app(
      ExtensionBuilder("Test", ExtensionBuilder::Type::PLATFORM_APP)
          .SetManifestKey("kiosk_enabled", true)
          .SetManifestKey("kiosk_only", true)
          .Build());
  auto* manual_user = user_manager_->AddKioskChromeAppUser(
      AccountId::FromUserEmail(manual_launch_kiosk_app->id()));
  user_manager_->LoginUser(manual_user->GetAccountId());
  {
    TestDelegateState state;
    state.dialog_button = ui::mojom::DialogButton::kOk;
    ConsentProviderImpl provider(
        std::make_unique<TestingConsentProviderDelegate>(&state));
    EXPECT_TRUE(provider.IsGrantable(*manual_launch_kiosk_app));

    ConsentProviderImpl::Consent result = ConsentProvider::CONSENT_IMPOSSIBLE;
    provider.RequestConsent(nullptr, *manual_launch_kiosk_app.get(),
                            "Volume ID 3", "Volume Label 3",
                            true /* writable */,
                            base::BindOnce(&OnConsentReceived, &result));
    base::RunLoop().RunUntilIdle();

    EXPECT_EQ(1, state.show_dialog_counter);
    EXPECT_EQ(0, state.show_notification_counter);
    EXPECT_EQ(ConsentProvider::CONSENT_GRANTED, result);
  }

  // Non-component apps in manual-launch kiosk mode will be rejected access
  // after rejecting by a user.
  {
    TestDelegateState state;
    state.dialog_button = ui::mojom::DialogButton::kCancel;
    ConsentProviderImpl provider(
        std::make_unique<TestingConsentProviderDelegate>(&state));
    EXPECT_TRUE(provider.IsGrantable(*manual_launch_kiosk_app));

    ConsentProviderImpl::Consent result = ConsentProvider::CONSENT_IMPOSSIBLE;
    provider.RequestConsent(nullptr, *manual_launch_kiosk_app.get(),
                            "Volume ID 4", "Volume Label 4",
                            true /* writable */,
                            base::BindOnce(&OnConsentReceived, &result));
    base::RunLoop().RunUntilIdle();

    EXPECT_EQ(1, state.show_dialog_counter);
    EXPECT_EQ(0, state.show_notification_counter);
    EXPECT_EQ(ConsentProvider::CONSENT_REJECTED, result);
  }
}

}  // namespace extensions
