// Copyright (c) 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/system/sys_info.h"
#include "chrome/browser/chromeos/login/login_wizard.h"
#include "chrome/browser/chromeos/login/screens/base_screen.h"
#include "chrome/browser/chromeos/login/screens/hid_detection_screen.h"
#include "chrome/browser/chromeos/login/test/js_checker.h"
#include "chrome/browser/chromeos/login/test/oobe_base_test.h"
#include "chrome/browser/chromeos/login/test/oobe_screen_waiter.h"
#include "chrome/browser/chromeos/login/wizard_controller.h"
#include "chrome/browser/ui/webui/chromeos/login/hid_detection_screen_handler.h"
#include "chrome/browser/ui/webui/chromeos/login/welcome_screen_handler.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/constants/chromeos_switches.h"
#include "content/public/test/browser_test.h"
#include "services/device/public/cpp/hid/fake_input_service_linux.h"
#include "services/device/public/mojom/input_service.mojom.h"

namespace chromeos {

class HIDDetectionScreenTest : public InProcessBrowserTest {
 public:
  enum class DeviceType { kKeyboard, kMouse };

  HIDDetectionScreenTest() {
    // HID detection screen only appears for Chromebases, Chromebits, and
    // Chromeboxes.
    base::SysInfo::SetChromeOSVersionInfoForTest("DEVICETYPE=CHROMEBOX",
                                                 base::Time::Now());

    fake_input_service_manager_ =
        std::make_unique<device::FakeInputServiceLinux>();

    HIDDetectionScreen::OverrideInputDeviceManagerBinderForTesting(
        base::Bind(&device::FakeInputServiceLinux::Bind,
                   base::Unretained(fake_input_service_manager_.get())));
  }

  ~HIDDetectionScreenTest() override {
    HIDDetectionScreen::OverrideInputDeviceManagerBinderForTesting(
        base::NullCallback());
  }

  // InProcessBrowserTest:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    InProcessBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendArg(switches::kLoginManager);
  }

  void SetUpOnMainThread() override {
    ShowLoginWizard(HIDDetectionView::kScreenId);
    ASSERT_TRUE(WizardController::default_controller());

    hid_detection_screen_ = static_cast<HIDDetectionScreen*>(
        WizardController::default_controller()->GetScreen(
            HIDDetectionView::kScreenId));
    ASSERT_TRUE(hid_detection_screen_);
    ASSERT_EQ(WizardController::default_controller()->current_screen(),
              hid_detection_screen_);
    ASSERT_TRUE(hid_detection_screen_->view_);

    hid_detection_screen()->SetAdapterInitialPoweredForTesting(false);
  }

  HIDDetectionScreen* hid_detection_screen() { return hid_detection_screen_; }
  HIDDetectionScreenHandler* handler() {
    return static_cast<HIDDetectionScreenHandler*>(
        hid_detection_screen()->view_);
  }

  scoped_refptr<device::BluetoothAdapter> adapter() {
    return hid_detection_screen_->GetAdapterForTesting();
  }

  void AddDeviceToService(DeviceType dev_type,
                          device::mojom::InputDeviceType type) {
    const bool is_mouse = dev_type == DeviceType::kMouse;
    auto device = device::mojom::InputDeviceInfo::New();
    device->id = is_mouse ? "mouse" : "keyboard";
    device->subsystem = device::mojom::InputDeviceSubsystem::SUBSYSTEM_INPUT;
    device->type = type;
    device->is_mouse = is_mouse;
    device->is_keyboard = !is_mouse;
    fake_input_service_manager_->AddDevice(std::move(device));
    base::RunLoop().RunUntilIdle();
  }

  void RemoveDeviceFromService(DeviceType dev_type) {
    const bool is_mouse = dev_type == DeviceType::kMouse;
    std::string id = is_mouse ? "mouse" : "keyboard";
    fake_input_service_manager_->RemoveDevice(std::move(id));
    base::RunLoop().RunUntilIdle();
  }

  void ContinueToWelcomeScreen() {
    // Simulate the user's click on "Continue" button.
    test::OobeJS()
        .CreateVisibilityWaiter(true, {"hid-detection", "hid-continue-button"})
        ->Wait();
    test::OobeJS().TapOnPath({"hid-detection", "hid-continue-button"});
    OobeScreenWaiter(WelcomeView::kScreenId).Wait();
  }

 private:
  HIDDetectionScreen* hid_detection_screen_;
  std::unique_ptr<device::FakeInputServiceLinux> fake_input_service_manager_;

  DISALLOW_COPY_AND_ASSIGN(HIDDetectionScreenTest);
};

class HIDDetectionScreenChromebookTest : public OobeBaseTest {
 public:
  HIDDetectionScreenChromebookTest() {
    // Set device type to one that should not invoke HIDDetectionScreen logic.
    base::SysInfo::SetChromeOSVersionInfoForTest("DEVICETYPE=CHROMEBOOK",
                                                 base::Time::Now());
  }
};

IN_PROC_BROWSER_TEST_F(HIDDetectionScreenChromebookTest,
                       HIDDetectionScreenNotAllowed) {
  OobeScreenWaiter(WelcomeView::kScreenId).Wait();
  ASSERT_TRUE(WizardController::default_controller());

  EXPECT_FALSE(WizardController::default_controller()->HasScreen(
      HIDDetectionView::kScreenId));
}

IN_PROC_BROWSER_TEST_F(HIDDetectionScreenTest, MouseKeyboardStates) {
  // NOTE: State strings match those in hid_detection_screen.cc.
  // No devices added yet
  EXPECT_EQ("searching", handler()->mouse_state_for_test());
  EXPECT_EQ("searching", handler()->keyboard_state_for_test());
  EXPECT_FALSE(handler()->continue_button_enabled_for_test());

  // Generic connection types. Unlike the pointing device, which may be a tablet
  // or touchscreen, the keyboard only reports usb and bluetooth states.
  AddDeviceToService(DeviceType::kMouse,
                     device::mojom::InputDeviceType::TYPE_SERIO);
  EXPECT_TRUE(handler()->continue_button_enabled_for_test());

  AddDeviceToService(DeviceType::kKeyboard,
                     device::mojom::InputDeviceType::TYPE_SERIO);
  EXPECT_EQ("connected", handler()->mouse_state_for_test());
  EXPECT_EQ("usb", handler()->keyboard_state_for_test());
  EXPECT_TRUE(handler()->continue_button_enabled_for_test());

  // Remove generic devices, add usb devices.
  RemoveDeviceFromService(DeviceType::kMouse);
  RemoveDeviceFromService(DeviceType::kKeyboard);
  EXPECT_FALSE(handler()->continue_button_enabled_for_test());

  AddDeviceToService(DeviceType::kMouse,
                     device::mojom::InputDeviceType::TYPE_USB);
  AddDeviceToService(DeviceType::kKeyboard,
                     device::mojom::InputDeviceType::TYPE_USB);
  EXPECT_EQ("usb", handler()->mouse_state_for_test());
  EXPECT_EQ("usb", handler()->keyboard_state_for_test());
  EXPECT_TRUE(handler()->continue_button_enabled_for_test());

  // Remove usb devices, add bluetooth devices.
  RemoveDeviceFromService(DeviceType::kMouse);
  RemoveDeviceFromService(DeviceType::kKeyboard);
  EXPECT_FALSE(handler()->continue_button_enabled_for_test());

  AddDeviceToService(DeviceType::kMouse,
                     device::mojom::InputDeviceType::TYPE_BLUETOOTH);
  AddDeviceToService(DeviceType::kKeyboard,
                     device::mojom::InputDeviceType::TYPE_BLUETOOTH);
  EXPECT_EQ("paired", handler()->mouse_state_for_test());
  EXPECT_EQ("paired", handler()->keyboard_state_for_test());
  EXPECT_TRUE(handler()->continue_button_enabled_for_test());
}

// Test that if there is any Bluetooth device connected on HID screen, the
// Bluetooth adapter should not be disabled after advancing to the next screen.
IN_PROC_BROWSER_TEST_F(HIDDetectionScreenTest, BluetoothDeviceConnected) {
  OobeScreenWaiter(HIDDetectionView::kScreenId).Wait();
  EXPECT_TRUE(adapter()->IsPowered());

  // Add a pair of USB mouse/keyboard so that |pointing_device_type_|
  // and |keyboard_type_| are
  // device::mojom::InputDeviceType::TYPE_USB.
  AddDeviceToService(DeviceType::kMouse,
                     device::mojom::InputDeviceType::TYPE_USB);
  AddDeviceToService(DeviceType::kKeyboard,
                     device::mojom::InputDeviceType::TYPE_USB);

  // Add another pair of Bluetooth mouse/keyboard.
  AddDeviceToService(DeviceType::kMouse,
                     device::mojom::InputDeviceType::TYPE_BLUETOOTH);
  AddDeviceToService(DeviceType::kKeyboard,
                     device::mojom::InputDeviceType::TYPE_BLUETOOTH);

  ContinueToWelcomeScreen();

  // The adapter should not be powered off at this moment.
  EXPECT_TRUE(adapter()->IsPowered());
}

// Test that if there is no Bluetooth device connected on HID screen, the
// Bluetooth adapter should be disabled after advancing to the next screen.
IN_PROC_BROWSER_TEST_F(HIDDetectionScreenTest, NoBluetoothDeviceConnected) {
  OobeScreenWaiter(HIDDetectionView::kScreenId).Wait();
  EXPECT_TRUE(adapter()->IsPowered());

  AddDeviceToService(DeviceType::kMouse,
                     device::mojom::InputDeviceType::TYPE_USB);
  AddDeviceToService(DeviceType::kKeyboard,
                     device::mojom::InputDeviceType::TYPE_USB);

  ContinueToWelcomeScreen();

  // The adapter should be powered off at this moment.
  EXPECT_FALSE(adapter()->IsPowered());
}

// Tests that the connected 'ticks' are shown when the devices are connected.
IN_PROC_BROWSER_TEST_F(HIDDetectionScreenTest, TestTicks) {
  OobeScreenWaiter(HIDDetectionView::kScreenId).Wait();
  test::OobeJS()
      .CreateVisibilityWaiter(false, {"hid-detection", "mouse-tick"})
      ->Wait();
  test::OobeJS()
      .CreateVisibilityWaiter(false, {"hid-detection", "keyboard-tick"})
      ->Wait();

  AddDeviceToService(DeviceType::kMouse,
                     device::mojom::InputDeviceType::TYPE_USB);
  AddDeviceToService(DeviceType::kKeyboard,
                     device::mojom::InputDeviceType::TYPE_USB);

  test::OobeJS()
      .CreateVisibilityWaiter(true, {"hid-detection", "mouse-tick"})
      ->Wait();
  test::OobeJS()
      .CreateVisibilityWaiter(true, {"hid-detection", "keyboard-tick"})
      ->Wait();
  ContinueToWelcomeScreen();
}

}  // namespace chromeos
