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

#include <string>

#include "ash/constants/ash_login_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/json/json_file_value_serializer.h"
#include "base/location.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/ash/login/startup_utils.h"
#include "chrome/browser/ash/login/test/js_checker.h"
#include "chrome/browser/ash/login/test/oobe_base_test.h"
#include "chrome/browser/ash/login/test/oobe_screen_waiter.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/ui/ash/login/login_display_host.h"
#include "chrome/browser/ui/ash/login/webui_login_view.h"
#include "chrome/browser/ui/webui/ash/login/enable_debugging_screen_handler.h"
#include "chrome/browser/ui/webui/ash/login/oobe_ui.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chromeos/ash/components/dbus/dbus_thread_manager.h"
#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
#include "chromeos/ash/components/dbus/debug_daemon/fake_debug_daemon_client.h"
#include "chromeos/ash/components/dbus/update_engine/fake_update_engine_client.h"
#include "chromeos/dbus/constants/dbus_switches.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash {
namespace {

constexpr char kDebuggingScreenId[] = "debugging";

const test::UIPath kRemoveProtectionDialog = {kDebuggingScreenId,
                                              "removeProtectionDialog"};
const test::UIPath kSetupDialog = {kDebuggingScreenId, "setupDialog"};
const test::UIPath kWaitDialog = {kDebuggingScreenId, "waitDialog"};
const test::UIPath kDoneDialog = {kDebuggingScreenId, "doneDialog"};
const test::UIPath kErrorDialog = {kDebuggingScreenId, "errorDialog"};

const test::UIPath kHelpLink = {kDebuggingScreenId, "help-link"};
const test::UIPath kPasswordInput = {kDebuggingScreenId, "password"};
const test::UIPath kPassword2Input = {kDebuggingScreenId, "passwordRepeat"};
const test::UIPath kPasswordNote = {kDebuggingScreenId, "password-note"};

const test::UIPath kCancelButton = {kDebuggingScreenId,
                                    "removeProtectionCancelButton"};
const test::UIPath kEnableButton = {kDebuggingScreenId, "enableButton"};
const test::UIPath kRemoveProtectionButton = {kDebuggingScreenId,
                                              "removeProtectionProceedButton"};

class TestDebugDaemonClient : public FakeDebugDaemonClient {
 public:
  TestDebugDaemonClient() = default;

  ~TestDebugDaemonClient() override = default;

  // FakeDebugDaemonClient overrides:
  void SetDebuggingFeaturesStatus(int featues_mask) override {
    ResetWait();
    FakeDebugDaemonClient::SetDebuggingFeaturesStatus(featues_mask);
  }

  void EnableDebuggingFeatures(const std::string& password,
                               EnableDebuggingCallback callback) override {
    FakeDebugDaemonClient::EnableDebuggingFeatures(
        password,
        base::BindOnce(&TestDebugDaemonClient::OnEnableDebuggingFeatures,
                       base::Unretained(this), std::move(callback)));
  }

  void RemoveRootfsVerification(EnableDebuggingCallback callback) override {
    FakeDebugDaemonClient::RemoveRootfsVerification(
        base::BindOnce(&TestDebugDaemonClient::OnRemoveRootfsVerification,
                       base::Unretained(this), std::move(callback)));
  }

  void QueryDebuggingFeatures(QueryDevFeaturesCallback callback) override {
    LOG(WARNING) << "QueryDebuggingFeatures";
    FakeDebugDaemonClient::QueryDebuggingFeatures(
        base::BindOnce(&TestDebugDaemonClient::OnQueryDebuggingFeatures,
                       base::Unretained(this), std::move(callback)));
  }

  void OnRemoveRootfsVerification(EnableDebuggingCallback original_callback,
                                  bool succeeded) {
    LOG(WARNING) << "OnRemoveRootfsVerification: succeeded = " << succeeded;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(original_callback), succeeded));
    if (runner_.get())
      runner_->Quit();
    else
      got_reply_ = true;

    num_remove_protection_++;
  }

  void OnQueryDebuggingFeatures(QueryDevFeaturesCallback original_callback,
                                bool succeeded,
                                int feature_mask) {
    LOG(WARNING) << "OnQueryDebuggingFeatures: succeeded = " << succeeded
                 << ", feature_mask = " << feature_mask;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(original_callback), succeeded, feature_mask));
    if (runner_.get())
      runner_->Quit();
    else
      got_reply_ = true;

    num_query_debugging_features_++;
  }

  void OnEnableDebuggingFeatures(EnableDebuggingCallback original_callback,
                                 bool succeeded) {
    LOG(WARNING) << "OnEnableDebuggingFeatures: succeeded = " << succeeded
                 << ", feature_mask = ";
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(original_callback), succeeded));
    if (runner_.get())
      runner_->Quit();
    else
      got_reply_ = true;

    num_enable_debugging_features_++;
  }

  void ResetWait() {
    got_reply_ = false;
    num_query_debugging_features_ = 0;
    num_enable_debugging_features_ = 0;
    num_remove_protection_ = 0;
  }

  int num_query_debugging_features() const {
    return num_query_debugging_features_;
  }

  int num_enable_debugging_features() const {
    return num_enable_debugging_features_;
  }

  int num_remove_protection() const { return num_remove_protection_; }

  void WaitUntilCalled() {
    if (got_reply_)
      return;

    runner_ = new content::MessageLoopRunner;
    runner_->Run();
  }

 private:
  scoped_refptr<content::MessageLoopRunner> runner_;
  bool got_reply_ = false;
  int num_query_debugging_features_ = 0;
  int num_enable_debugging_features_ = 0;
  int num_remove_protection_ = 0;
};

}  // namespace

class EnableDebuggingTestBase : public OobeBaseTest {
 public:
  EnableDebuggingTestBase() = default;

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

  ~EnableDebuggingTestBase() override = default;

  // OobeBaseTest:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    OobeBaseTest::SetUpCommandLine(command_line);
    // Disable HID detection because it takes precedence and could block
    // enable-debugging UI.
    command_line->AppendSwitch(switches::kDisableHIDDetectionOnOOBEForTesting);
  }

  void SetUpInProcessBrowserTestFixture() override {
    OobeBaseTest::SetUpInProcessBrowserTestFixture();
    debug_daemon_client_ = std::make_unique<TestDebugDaemonClient>();
    DebugDaemonClient::SetInstanceForTest(debug_daemon_client_.get());
  }

  void TearDownInProcessBrowserTestFixture() override {
    DebugDaemonClient::SetInstanceForTest(nullptr);
    debug_daemon_client_.reset();
    OobeBaseTest::TearDownInProcessBrowserTestFixture();
  }

  void InvokeEnableDebuggingScreen() {
    LoginDisplayHost::default_host()->HandleAccelerator(
        LoginAcceleratorAction::kEnableDebugging);

    OobeScreenWaiter(EnableDebuggingScreenView::kScreenId).Wait();
  }

  void CloseEnableDebuggingScreen() { test::OobeJS().TapOnPath(kCancelButton); }

  void ClickEnableButton() { test::OobeJS().TapOnPath(kEnableButton); }

  void ShowRemoveProtectionScreen() {
    debug_daemon_client_->SetDebuggingFeaturesStatus(
        DebugDaemonClient::DEV_FEATURE_NONE);
    WaitForOobeUI();
    test::OobeJS().ExpectHidden(kDebuggingScreenId);
    InvokeEnableDebuggingScreen();
    test::OobeJS()
        .CreateVisibilityWaiter(true, kRemoveProtectionDialog)
        ->Wait();
    test::OobeJS().ExpectVisiblePath(kRemoveProtectionButton);
    test::OobeJS().ExpectVisiblePath(kHelpLink);
    debug_daemon_client_->WaitUntilCalled();
    base::RunLoop().RunUntilIdle();
  }

  void ShowSetupScreen() {
    debug_daemon_client_->SetDebuggingFeaturesStatus(
        debugd::DevFeatureFlag::DEV_FEATURE_ROOTFS_VERIFICATION_REMOVED);
    WaitForOobeUI();
    test::OobeJS().ExpectHidden(kDebuggingScreenId);
    InvokeEnableDebuggingScreen();
    test::OobeJS().CreateVisibilityWaiter(true, kSetupDialog)->Wait();
    debug_daemon_client_->WaitUntilCalled();
    base::RunLoop().RunUntilIdle();

    test::OobeJS().ExpectVisiblePath(kPasswordInput);
    test::OobeJS().ExpectVisiblePath(kPassword2Input);
    test::OobeJS().ExpectVisiblePath(kPasswordNote);
  }

  std::unique_ptr<TestDebugDaemonClient> debug_daemon_client_;
};

class EnableDebuggingDevTest : public EnableDebuggingTestBase {
 public:
  EnableDebuggingDevTest() = default;
  ~EnableDebuggingDevTest() override = default;

  // EnableDebuggingTestBase:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    EnableDebuggingTestBase::SetUpCommandLine(command_line);
    command_line->AppendSwitch(chromeos::switches::kSystemDevMode);
  }
};

// Show remove protection screen, click on [Cancel] button.
IN_PROC_BROWSER_TEST_F(EnableDebuggingDevTest, ShowAndCancelRemoveProtection) {
  ShowRemoveProtectionScreen();
  CloseEnableDebuggingScreen();
  test::OobeJS().ExpectHidden(kDebuggingScreenId);

  EXPECT_EQ(debug_daemon_client_->num_query_debugging_features(), 1);
  EXPECT_EQ(debug_daemon_client_->num_enable_debugging_features(), 0);
  EXPECT_EQ(debug_daemon_client_->num_remove_protection(), 0);
}

// Show remove protection, click on [Remove protection] button and wait for
// reboot.
IN_PROC_BROWSER_TEST_F(EnableDebuggingDevTest, ShowAndRemoveProtection) {
  // Disarm faked reboot, otherwise Chrome just stops and there's nothing to
  // verify.
  chromeos::FakePowerManagerClient* fake_power_manager_client =
      chromeos::FakePowerManagerClient::Get();
  ASSERT_NE(fake_power_manager_client, nullptr);
  fake_power_manager_client->set_restart_callback(base::DoNothing());

  ShowRemoveProtectionScreen();
  debug_daemon_client_->ResetWait();
  test::OobeJS().TapOnPath(kRemoveProtectionButton);
  debug_daemon_client_->WaitUntilCalled();
  test::OobeJS().ExpectVisiblePath(kWaitDialog);

  // Check if we have rebooted after enabling.
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(debug_daemon_client_->num_remove_protection(), 1);
  EXPECT_EQ(debug_daemon_client_->num_enable_debugging_features(), 0);
  EXPECT_EQ(fake_power_manager_client->num_request_restart_calls(), 1);
}

// Show setup screen. Click on [Enable] button. Wait until done screen is shown.
IN_PROC_BROWSER_TEST_F(EnableDebuggingDevTest, ShowSetup) {
  ShowSetupScreen();
  debug_daemon_client_->ResetWait();
  ClickEnableButton();
  debug_daemon_client_->WaitUntilCalled();
  test::OobeJS().CreateVisibilityWaiter(true, kDoneDialog)->Wait();

  EXPECT_EQ(debug_daemon_client_->num_enable_debugging_features(), 1);
  EXPECT_EQ(debug_daemon_client_->num_remove_protection(), 0);
}

// Show setup screen. Type in matching passwords.
// Click on [Enable] button. Wait until done screen is shown.
IN_PROC_BROWSER_TEST_F(EnableDebuggingDevTest, SetupMatchingPasswords) {
  ShowSetupScreen();
  debug_daemon_client_->ResetWait();
  test::OobeJS().TypeIntoPath("test0000", kPasswordInput);
  test::OobeJS().TypeIntoPath("test0000", kPassword2Input);
  ClickEnableButton();
  debug_daemon_client_->WaitUntilCalled();
  test::OobeJS().CreateVisibilityWaiter(true, kDoneDialog)->Wait();

  EXPECT_EQ(debug_daemon_client_->num_enable_debugging_features(), 1);
  EXPECT_EQ(debug_daemon_client_->num_remove_protection(), 0);
}

// Show setup screen. Type in different passwords.
// Click on [Enable] button. Assert done screen is not shown.
// Then confirm that typing in matching passwords enables debugging features.
IN_PROC_BROWSER_TEST_F(EnableDebuggingDevTest, SetupNotMatchingPasswords) {
  ShowSetupScreen();
  debug_daemon_client_->ResetWait();
  test::OobeJS().TypeIntoPath("test0000", kPasswordInput);
  test::OobeJS().TypeIntoPath("test9999", kPassword2Input);
  test::OobeJS().ExpectDisabledPath(kEnableButton);

  EXPECT_EQ(debug_daemon_client_->num_enable_debugging_features(), 0);
  EXPECT_EQ(debug_daemon_client_->num_remove_protection(), 0);

  test::OobeJS().TypeIntoPath("test0000", kPassword2Input);
  ClickEnableButton();
  debug_daemon_client_->WaitUntilCalled();
  test::OobeJS().CreateVisibilityWaiter(true, kDoneDialog)->Wait();

  EXPECT_EQ(debug_daemon_client_->num_enable_debugging_features(), 1);
  EXPECT_EQ(debug_daemon_client_->num_remove_protection(), 0);
}

// Test images come with some features enabled but still has rootfs protection.
// Invoking debug screen should show remove protection screen.
IN_PROC_BROWSER_TEST_F(EnableDebuggingDevTest, ShowOnTestImages) {
  debug_daemon_client_->SetDebuggingFeaturesStatus(
      debugd::DevFeatureFlag::DEV_FEATURE_SSH_SERVER_CONFIGURED |
      debugd::DevFeatureFlag::DEV_FEATURE_SYSTEM_ROOT_PASSWORD_SET);
  WaitForOobeUI();
  test::OobeJS().ExpectHidden(kDebuggingScreenId);
  InvokeEnableDebuggingScreen();
  test::OobeJS().CreateVisibilityWaiter(true, kRemoveProtectionDialog)->Wait();
  debug_daemon_client_->WaitUntilCalled();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(debug_daemon_client_->num_query_debugging_features(), 1);
  EXPECT_EQ(debug_daemon_client_->num_enable_debugging_features(), 0);
  EXPECT_EQ(debug_daemon_client_->num_remove_protection(), 0);
}

IN_PROC_BROWSER_TEST_F(EnableDebuggingDevTest, WaitForDebugDaemon) {
  // Stat with service not ready.
  debug_daemon_client_->SetServiceIsAvailable(false);
  debug_daemon_client_->SetDebuggingFeaturesStatus(
      DebugDaemonClient::DEV_FEATURE_NONE);
  WaitForOobeUI();

  // Invoking UI and it should land on wait-view.
  test::OobeJS().ExpectHidden(kDebuggingScreenId);
  InvokeEnableDebuggingScreen();
  test::OobeJS().ExpectVisiblePath(kWaitDialog);

  // Mark service ready and it should proceed to remove protection view.
  debug_daemon_client_->SetServiceIsAvailable(true);
  debug_daemon_client_->WaitUntilCalled();
  base::RunLoop().RunUntilIdle();
  test::OobeJS().ExpectVisiblePath(kRemoveProtectionDialog);
}

// Uses the base class setup, with a TestDebugDaemonClient.
using EnableDebuggingTest = EnableDebuggingTestBase;

// Try to show enable debugging dialog, we should see error screen here.
IN_PROC_BROWSER_TEST_F(EnableDebuggingTest, NoShowInNonDevMode) {
  WaitForOobeUI();
  test::OobeJS().ExpectHidden(kDebuggingScreenId);
  InvokeEnableDebuggingScreen();
  test::OobeJS().CreateVisibilityWaiter(true, kErrorDialog)->Wait();
}

class EnableDebuggingRequestedTest : public EnableDebuggingDevTest {
 public:
  EnableDebuggingRequestedTest() = default;

  // EnableDebuggingDevTest overrides:
  bool SetUpUserDataDirectory() override {
    base::DictValue local_state_dict;
    local_state_dict.Set(ash::prefs::kDebuggingFeaturesRequested, true);

    base::FilePath user_data_dir;
    CHECK(base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir));
    base::FilePath local_state_path =
        user_data_dir.Append(chrome::kLocalStateFilename);
    CHECK(
        JSONFileValueSerializer(local_state_path).Serialize(local_state_dict));

    return EnableDebuggingDevTest::SetUpUserDataDirectory();
  }
  void SetUpInProcessBrowserTestFixture() override {
    EnableDebuggingDevTest::SetUpInProcessBrowserTestFixture();

    debug_daemon_client_->SetDebuggingFeaturesStatus(
        debugd::DevFeatureFlag::DEV_FEATURE_ROOTFS_VERIFICATION_REMOVED);
  }
};

// Setup screen is automatically shown when the feature is requested.
IN_PROC_BROWSER_TEST_F(EnableDebuggingRequestedTest, AutoShowSetup) {
  OobeScreenWaiter(EnableDebuggingScreenView::kScreenId).Wait();
}

// Canceling auto shown setup screen should close it.
IN_PROC_BROWSER_TEST_F(EnableDebuggingRequestedTest, CancelAutoShowSetup) {
  OobeScreenWaiter(EnableDebuggingScreenView::kScreenId).Wait();
  CloseEnableDebuggingScreen();
  test::OobeJS().ExpectHidden(kDebuggingScreenId);
}

}  // namespace ash
