chromium/chrome/browser/apps/app_discovery_service/recommended_arc_apps/recommend_apps_fetcher_impl_unittest.cc

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

#include "chrome/browser/apps/app_discovery_service/recommended_arc_apps/recommend_apps_fetcher_impl.h"

#include <memory>
#include <utility>
#include <vector>

#include "ash/components/arc/arc_features_parser.h"
#include "base/base64url.h"
#include "base/files/file_path.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "chrome/browser/apps/app_discovery_service/recommended_arc_apps/fake_recommend_apps_fetcher_delegate.h"
#include "chrome/browser/apps/app_discovery_service/recommended_arc_apps/recommend_apps_fetcher.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/crosapi/mojom/cros_display_config.mojom.h"
#include "components/user_manager/scoped_user_manager.h"
#include "content/public/test/browser_task_environment.h"
#include "gpu/config/gpu_info.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/zlib/google/compression_utils.h"
#include "ui/display/display.h"
#include "ui/display/test/test_screen.h"
#include "ui/display/util/display_util.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/devices/device_data_manager_test_api.h"
#include "ui/events/devices/input_device.h"
#include "ui/events/devices/touchscreen_device.h"

namespace apps {
namespace {

// Values set in ArcFeatures created by CreateArcFeaturesForTest.
constexpr char kTestArcSdkVersion[] = "25";
constexpr char kTestArcPlayStoreVersion[] = "81010860";
constexpr char kTestDeviceFingerprint[] =
    "google/product/device:1/R105-14925.0/1234567:user/release-keys";
const char* const kTestArcAbiList[] = {"x86", "x86_64"};
const char* const kTestArcFeatures[] = {"android.hardware.faketouch",
                                        "android.software.home_screen"};

constexpr char kOneAppResponse[] = R"({"recommendedApp": [{
    "androidApp": {
      "packageName": "com.game.name",
      "title": "NameOfFunGame",
      "icon": {
        "imageUri": "https://play-lh.googleusercontent.com/1234IDECLAREATHUMBWAR",
        "dimensions": {
          "width": 512,
          "height": 512
        }
      }
    }
  }]})";

// Creates a fake ARC features to be used for these tests.
arc::ArcFeatures CreateArcFeaturesForTest() {
  arc::ArcFeatures arc_features;
  arc_features.build_props.sdk_version = kTestArcSdkVersion;
  arc_features.build_props.fingerprint = kTestDeviceFingerprint;
  arc_features.play_store_version = kTestArcPlayStoreVersion;

  std::vector<std::string> abi_list(std::begin(kTestArcAbiList),
                                    std::end(kTestArcAbiList));
  arc_features.build_props.abi_list = base::JoinString(abi_list, ",");

  for (const char* feature : kTestArcFeatures) {
    arc_features.feature_map[feature] = 1;
  }
  return arc_features;
}

class TestCrosDisplayConfig
    : public crosapi::mojom::CrosDisplayConfigController {
 public:
  explicit TestCrosDisplayConfig(
      mojo::PendingReceiver<crosapi::mojom::CrosDisplayConfigController>
          receiver)
      : receiver_(this, std::move(receiver)) {}

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

  ~TestCrosDisplayConfig() override = default;

  void Flush() { receiver_.FlushForTesting(); }

  bool RunGetDisplayUnitInfoListCallback(
      std::vector<crosapi::mojom::DisplayUnitInfoPtr> unit_info_list) {
    if (!get_display_unit_info_list_callback_) {
      return false;
    }
    std::move(get_display_unit_info_list_callback_)
        .Run(std::move(unit_info_list));
    return true;
  }

  // crosapi::mojom::CrosDisplayConfigController:
  void AddObserver(
      mojo::PendingAssociatedRemote<crosapi::mojom::CrosDisplayConfigObserver>
          observer) override {}
  void GetDisplayLayoutInfo(GetDisplayLayoutInfoCallback callback) override {}
  void SetDisplayLayoutInfo(crosapi::mojom::DisplayLayoutInfoPtr info,
                            SetDisplayLayoutInfoCallback callback) override {}
  void GetDisplayUnitInfoList(
      bool single_unified,
      GetDisplayUnitInfoListCallback callback) override {
    get_display_unit_info_list_callback_ = std::move(callback);
  }
  void SetDisplayProperties(
      const std::string& id,
      crosapi::mojom::DisplayConfigPropertiesPtr properties,
      crosapi::mojom::DisplayConfigSource source,
      SetDisplayPropertiesCallback callback) override {}
  void SetUnifiedDesktopEnabled(bool enabled) override {}
  void OverscanCalibration(const std::string& display_id,
                           crosapi::mojom::DisplayConfigOperation op,
                           const std::optional<gfx::Insets>& delta,
                           OverscanCalibrationCallback callback) override {}
  void TouchCalibration(const std::string& display_id,
                        crosapi::mojom::DisplayConfigOperation op,
                        crosapi::mojom::TouchCalibrationPtr calibration,
                        TouchCalibrationCallback callback) override {}
  void HighlightDisplay(int64_t id) override {}
  void DragDisplayDelta(int64_t display_id,
                        int32_t delta_x,
                        int32_t delta_y) override {}

 private:
  mojo::Receiver<crosapi::mojom::CrosDisplayConfigController> receiver_;

  GetDisplayUnitInfoListCallback get_display_unit_info_list_callback_;
};

// Helper class to extract relevant information from the app list request
// headers.
class AppListRequestHeaderReader {
 public:
  explicit AppListRequestHeaderReader(network::ResourceRequest* request) {
    sdk_version_ =
        request->headers.GetHeader("X-DFE-Sdk-Version").value_or(std::string());
    device_fingerprint_ = request->headers.GetHeader("X-DFE-Device-Fingerprint")
                              .value_or(std::string());
    play_store_version_ =
        request->headers.GetHeader("X-DFE-Chromesky-Client-Version")
            .value_or(std::string());
    DecodeDeviceConfigHeader(request);
  }
  ~AppListRequestHeaderReader() = default;

  AppListRequestHeaderReader(const AppListRequestHeaderReader& other) = delete;
  AppListRequestHeaderReader& operator=(
      const AppListRequestHeaderReader& other) = delete;

  const std::string& sdk_version() const { return sdk_version_; }

  const std::string& device_fingerprint() const { return device_fingerprint_; }

  const std::string& play_store_version() const { return play_store_version_; }

  device_configuration::DeviceConfigurationProto_TouchScreen touch_screen()
      const {
    return device_config_.touch_screen();
  }

  int screen_density() const { return device_config_.screen_density(); }

  int screen_height() const { return device_config_.screen_height(); }

  int screen_width() const { return device_config_.screen_width(); }

  device_configuration::DeviceConfigurationProto_ScreenLayout screen_layout()
      const {
    return device_config_.screen_layout();
  }

  device_configuration::DeviceConfigurationProto_Keyboard keyboard() const {
    return device_config_.keyboard();
  }

  bool has_hard_keyboard() const { return device_config_.has_hard_keyboard(); }

  std::vector<std::string> GetNativePlatforms() const {
    std::vector<std::string> result;
    for (int i = 0; i < device_config_.native_platform_size(); ++i) {
      result.push_back(device_config_.native_platform(i));
    }
    std::sort(result.begin(), result.end());
    return result;
  }

  std::vector<std::string> GetSystemAvailableFeatures() const {
    std::vector<std::string> result;
    for (int i = 0; i < device_config_.system_available_feature_size(); ++i) {
      result.push_back(device_config_.system_available_feature(i));
    }
    std::sort(result.begin(), result.end());
    return result;
  }

 private:
  void DecodeDeviceConfigHeader(network::ResourceRequest* request) {
    std::optional<std::string> device_config_header =
        request->headers.GetHeader("X-DFE-Device-Config");
    ASSERT_TRUE(device_config_header);

    std::string decoded;
    ASSERT_TRUE(Base64UrlDecode(*device_config_header,
                                base::Base64UrlDecodePolicy::DISALLOW_PADDING,
                                &decoded));
    std::string decompressed;
    ASSERT_TRUE(compression::GzipUncompress(decoded, &decompressed));

    ASSERT_TRUE(device_config_.ParseFromString(decompressed));
  }

  std::string sdk_version_;
  std::string device_fingerprint_;
  std::string play_store_version_;
  device_configuration::DeviceConfigurationProto device_config_;
};

}  // namespace

class RecommendAppsFetcherImplTest : public testing::Test {
 public:
  RecommendAppsFetcherImplTest() = default;
  ~RecommendAppsFetcherImplTest() override = default;

  void SetUp() override {
    display::Screen::SetScreenInstance(&test_screen_);
    display::SetInternalDisplayIds({test_screen_.GetPrimaryDisplay().id()});

    gpu_info_.gl_version = "OpenGL ES 3.2 Mesa 21.2.3";
    gpu_info_.gl_renderer = "Mesa DRI";
    gpu_info_.gl_extensions =
        "GL_EXT_texture_format_BGRA8888 GL_EXT_read_format_bgra";

    mojo::PendingRemote<crosapi::mojom::CrosDisplayConfigController>
        remote_display_config;
    cros_display_config_ = std::make_unique<TestCrosDisplayConfig>(
        remote_display_config.InitWithNewPipeAndPassReceiver());

    test_url_loader_factory_.SetInterceptor(
        base::BindRepeating(&RecommendAppsFetcherImplTest::InterceptRequest,
                            base::Unretained(this)));

    recommend_apps_fetcher_ = std::make_unique<RecommendAppsFetcherImpl>(
        &delegate_, std::move(remote_display_config),
        &test_url_loader_factory_);

    static_cast<RecommendAppsFetcherImpl*>(recommend_apps_fetcher_.get())
        ->set_arc_features_getter_for_testing(base::BindRepeating(
            &RecommendAppsFetcherImplTest::HandleArcFeaturesRequest,
            base::Unretained(this)));
  }

  void TearDown() override {
    recommend_apps_fetcher_.reset();
    cros_display_config_.reset();
    display::Screen::SetScreenInstance(nullptr);
    device_data_manager_test_api_.SetKeyboardDevices({});
    device_data_manager_test_api_.SetTouchscreenDevices({});
  }

 protected:
  struct Dpi {
    Dpi(float x, float y) : x(x), y(y) {}
    ~Dpi() = default;

    const float x;
    const float y;
  };

  std::vector<crosapi::mojom::DisplayUnitInfoPtr> CreateDisplayUnitInfo(
      const Dpi& internal_dpi,
      std::optional<Dpi> external_dpi) {
    std::vector<crosapi::mojom::DisplayUnitInfoPtr> info_list;

    if (external_dpi.has_value()) {
      auto external_info = crosapi::mojom::DisplayUnitInfo::New();
      external_info->id =
          base::NumberToString(test_screen_.GetPrimaryDisplay().id() + 1);
      external_info->is_internal = false;
      external_info->dpi_x = external_dpi->x;
      external_info->dpi_y = external_dpi->y;
      info_list.emplace_back(std::move(external_info));
    }

    auto info = crosapi::mojom::DisplayUnitInfo::New();
    info->id = base::NumberToString(test_screen_.GetPrimaryDisplay().id());
    info->is_internal = true;
    info->dpi_x = internal_dpi.x;
    info->dpi_y = internal_dpi.y;
    info_list.emplace_back(std::move(info));

    return info_list;
  }

  network::ResourceRequest* WaitForAppListRequest() {
    if (test_url_loader_factory_.pending_requests()->size() == 0) {
      request_waiter_ = std::make_unique<base::RunLoop>();
      request_waiter_->Run();
      request_waiter_.reset();
    }
    return &test_url_loader_factory_.GetPendingRequest(0)->request;
  }

  void SetDisplaySize(const gfx::Size& size) {
    display::Display display = test_screen_.GetPrimaryDisplay();
    display.SetSize(size);
    test_screen_.display_list().RemoveDisplay(display.id());
    test_screen_.display_list().AddDisplay(display,
                                           display::DisplayList::Type::PRIMARY);
  }

  void VerifyArcRequestHeaders(
      const AppListRequestHeaderReader& header_reader) {
    EXPECT_EQ(kTestArcSdkVersion, header_reader.sdk_version());
    // TODO(crbug.com/40232048): Verify that fingerprint is only set when
    // kAppDiscoveryForOobe is enabled.
    EXPECT_EQ(kTestDeviceFingerprint, header_reader.device_fingerprint());
    EXPECT_EQ(kTestArcPlayStoreVersion, header_reader.play_store_version());
    EXPECT_EQ(std::vector<std::string>(std::begin(kTestArcAbiList),
                                       std::end(kTestArcAbiList)),
              header_reader.GetNativePlatforms());
    EXPECT_EQ(std::vector<std::string>(std::begin(kTestArcFeatures),
                                       std::end(kTestArcFeatures)),
              header_reader.GetSystemAvailableFeatures());
  }

  FakeRecommendAppsFetcherDelegate delegate_;
  network::TestURLLoaderFactory test_url_loader_factory_;
  std::unique_ptr<RecommendAppsFetcher> recommend_apps_fetcher_;
  std::unique_ptr<TestCrosDisplayConfig> cros_display_config_;

  ui::DeviceDataManagerTestApi device_data_manager_test_api_;
  display::test::TestScreen test_screen_;
  base::OnceCallback<void(std::optional<arc::ArcFeatures>)>
      arc_features_callback_;
  gpu::GPUInfo gpu_info_;

 private:
  void InterceptRequest(const network::ResourceRequest& request) {
    ASSERT_EQ(
        "https://android.clients.google.com/fdfe/chrome/"
        "getSetupAppRecommendations",
        request.url.spec());
    if (request_waiter_) {
      request_waiter_->Quit();
    }
  }

  void HandleArcFeaturesRequest(
      base::OnceCallback<void(std::optional<arc::ArcFeatures>)> callback) {
    arc_features_callback_ = std::move(callback);
  }

  content::BrowserTaskEnvironment task_environment_;
  data_decoder::test::InProcessDataDecoder in_process_data_decoder;

  std::unique_ptr<base::RunLoop> request_waiter_;
};

TEST_F(RecommendAppsFetcherImplTest, ExtraLargeScreenWithTouch) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  device_data_manager_test_api_.SetTouchscreenDevices({ui::TouchscreenDevice(
      123, ui::InputDeviceType::INPUT_DEVICE_USB,
      std::string("test external touch device"), gfx::Size(1920, 1200), 1)});
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{
          {1, ui::INPUT_DEVICE_INTERNAL, "internal keyboard"}});
  SetDisplaySize(gfx::Size(1920, 1200));

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(110, 120), Dpi(117.23, 117.23))));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  AppListRequestHeaderReader header_reader(request);

  VerifyArcRequestHeaders(header_reader);

  EXPECT_EQ(device_configuration::DeviceConfigurationProto_TouchScreen_FINGER,
            header_reader.touch_screen());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_Keyboard_QWERTY,
            header_reader.keyboard());
  EXPECT_FALSE(header_reader.has_hard_keyboard());
  EXPECT_EQ(230, header_reader.screen_density());
  EXPECT_EQ(1920, header_reader.screen_width());
  EXPECT_EQ(1200, header_reader.screen_height());
  EXPECT_EQ(
      device_configuration::DeviceConfigurationProto_ScreenLayout_EXTRA_LARGE,
      header_reader.screen_layout());
}

TEST_F(RecommendAppsFetcherImplTest, NoArcFeatures) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  device_data_manager_test_api_.SetTouchscreenDevices({ui::TouchscreenDevice(
      123, ui::InputDeviceType::INPUT_DEVICE_USB,
      std::string("test external touch device"), gfx::Size(1920, 1200), 1)});
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{
          {1, ui::INPUT_DEVICE_INTERNAL, "internal keyboard"}});
  SetDisplaySize(gfx::Size(1920, 1200));

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(110, 120), Dpi(117.23, 117.23))));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(std::nullopt);

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  AppListRequestHeaderReader header_reader(request);

  EXPECT_EQ("", header_reader.sdk_version());
  EXPECT_EQ("", header_reader.device_fingerprint());
  EXPECT_EQ("", header_reader.play_store_version());
  EXPECT_EQ(std::vector<std::string>(), header_reader.GetNativePlatforms());
  EXPECT_EQ(std::vector<std::string>(),
            header_reader.GetSystemAvailableFeatures());

  EXPECT_EQ(device_configuration::DeviceConfigurationProto_TouchScreen_FINGER,
            header_reader.touch_screen());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_Keyboard_QWERTY,
            header_reader.keyboard());
  EXPECT_FALSE(header_reader.has_hard_keyboard());
  EXPECT_EQ(230, header_reader.screen_density());
  EXPECT_EQ(1920, header_reader.screen_width());
  EXPECT_EQ(1200, header_reader.screen_height());
  EXPECT_EQ(
      device_configuration::DeviceConfigurationProto_ScreenLayout_EXTRA_LARGE,
      header_reader.screen_layout());
}

TEST_F(RecommendAppsFetcherImplTest, HasHardKeyboard) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  device_data_manager_test_api_.SetTouchscreenDevices({ui::TouchscreenDevice(
      123, ui::InputDeviceType::INPUT_DEVICE_USB,
      std::string("test external touch device"), gfx::Size(1920, 1200), 1)});
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{{1, ui::INPUT_DEVICE_INTERNAL,
                                       "internal keyboard", "phys",
                                       base::FilePath("sys_path"), 0, 0, 0}});
  SetDisplaySize(gfx::Size(1920, 1200));

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(110, 120), Dpi(117.23, 117.23))));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  AppListRequestHeaderReader header_reader(request);

  VerifyArcRequestHeaders(header_reader);

  EXPECT_EQ(device_configuration::DeviceConfigurationProto_TouchScreen_FINGER,
            header_reader.touch_screen());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_Keyboard_QWERTY,
            header_reader.keyboard());
  EXPECT_TRUE(header_reader.has_hard_keyboard());
  EXPECT_EQ(230, header_reader.screen_density());
  EXPECT_EQ(1920, header_reader.screen_width());
  EXPECT_EQ(1200, header_reader.screen_height());
  EXPECT_EQ(
      device_configuration::DeviceConfigurationProto_ScreenLayout_EXTRA_LARGE,
      header_reader.screen_layout());
}

TEST_F(RecommendAppsFetcherImplTest, NoKeyboard) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  SetDisplaySize(gfx::Size(1920, 1200));

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(110, 120), Dpi(117.23, 117.23))));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  AppListRequestHeaderReader header_reader(request);

  VerifyArcRequestHeaders(header_reader);

  EXPECT_EQ(device_configuration::DeviceConfigurationProto_TouchScreen_NOTOUCH,
            header_reader.touch_screen());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_Keyboard_NOKEYS,
            header_reader.keyboard());
  EXPECT_FALSE(header_reader.has_hard_keyboard());
  EXPECT_EQ(230, header_reader.screen_density());
  EXPECT_EQ(1920, header_reader.screen_width());
  EXPECT_EQ(1200, header_reader.screen_height());
  EXPECT_EQ(
      device_configuration::DeviceConfigurationProto_ScreenLayout_EXTRA_LARGE,
      header_reader.screen_layout());
}

TEST_F(RecommendAppsFetcherImplTest, ExtraLargeScreenWithStylus) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  device_data_manager_test_api_.SetTouchscreenDevices(
      {ui::TouchscreenDevice(123, ui::InputDeviceType::INPUT_DEVICE_INTERNAL,
                             std::string("test external touch device"),
                             gfx::Size(1200, 1920), 1, true)});
  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{
          {1, ui::INPUT_DEVICE_INTERNAL, "internal keyboard"}});

  SetDisplaySize(gfx::Size(1200, 1920));

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117.23, 117.23), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  AppListRequestHeaderReader header_reader(request);

  VerifyArcRequestHeaders(header_reader);

  EXPECT_EQ(device_configuration::DeviceConfigurationProto_TouchScreen_STYLUS,
            header_reader.touch_screen());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_Keyboard_QWERTY,
            header_reader.keyboard());
  EXPECT_FALSE(header_reader.has_hard_keyboard());
  EXPECT_EQ(234, header_reader.screen_density());
  EXPECT_EQ(1200, header_reader.screen_width());
  EXPECT_EQ(1920, header_reader.screen_height());
  EXPECT_EQ(
      device_configuration::DeviceConfigurationProto_ScreenLayout_EXTRA_LARGE,
      header_reader.screen_layout());
}

TEST_F(RecommendAppsFetcherImplTest, LargeScreenWithoutTouchScreen) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{
          {1, ui::INPUT_DEVICE_INTERNAL, "internal keyboard"}});

  SetDisplaySize(gfx::Size(1200, 1200));

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  AppListRequestHeaderReader header_reader(request);

  VerifyArcRequestHeaders(header_reader);

  EXPECT_EQ(device_configuration::DeviceConfigurationProto_TouchScreen_NOTOUCH,
            header_reader.touch_screen());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_Keyboard_QWERTY,
            header_reader.keyboard());
  EXPECT_FALSE(header_reader.has_hard_keyboard());
  EXPECT_EQ(234, header_reader.screen_density());
  EXPECT_EQ(1200, header_reader.screen_width());
  EXPECT_EQ(1200, header_reader.screen_height());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_ScreenLayout_LARGE,
            header_reader.screen_layout());
}

TEST_F(RecommendAppsFetcherImplTest, NormalScreenWithoutTouchScreen) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{
          {1, ui::INPUT_DEVICE_INTERNAL, "internal keyboard"}});

  SetDisplaySize(gfx::Size(1200, 512));

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  AppListRequestHeaderReader header_reader(request);

  VerifyArcRequestHeaders(header_reader);

  EXPECT_EQ(device_configuration::DeviceConfigurationProto_TouchScreen_NOTOUCH,
            header_reader.touch_screen());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_Keyboard_QWERTY,
            header_reader.keyboard());
  EXPECT_FALSE(header_reader.has_hard_keyboard());
  EXPECT_EQ(234, header_reader.screen_density());
  EXPECT_EQ(1200, header_reader.screen_width());
  EXPECT_EQ(512, header_reader.screen_height());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_ScreenLayout_NORMAL,
            header_reader.screen_layout());
}

TEST_F(RecommendAppsFetcherImplTest, SmallScreenWithoutTouchScreen) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{
          {1, ui::INPUT_DEVICE_INTERNAL, "internal keyboard"},
          {2, ui::INPUT_DEVICE_USB, "external keyboard"}});

  SetDisplaySize(gfx::Size(512, 456));

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  AppListRequestHeaderReader header_reader(request);

  VerifyArcRequestHeaders(header_reader);

  EXPECT_EQ(device_configuration::DeviceConfigurationProto_TouchScreen_NOTOUCH,
            header_reader.touch_screen());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_Keyboard_QWERTY,
            header_reader.keyboard());
  EXPECT_FALSE(header_reader.has_hard_keyboard());
  EXPECT_EQ(234, header_reader.screen_density());
  EXPECT_EQ(512, header_reader.screen_width());
  EXPECT_EQ(456, header_reader.screen_height());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_ScreenLayout_SMALL,
            header_reader.screen_layout());
}

TEST_F(RecommendAppsFetcherImplTest, ArcFeaturesReadyBeforeAsh) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{
          {1, ui::INPUT_DEVICE_INTERNAL, "internal keyboard"},
          {2, ui::INPUT_DEVICE_USB, "external keyboard"}});

  SetDisplaySize(gfx::Size(512, 456));

  recommend_apps_fetcher_->Start();

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  AppListRequestHeaderReader header_reader(request);

  VerifyArcRequestHeaders(header_reader);

  EXPECT_EQ(device_configuration::DeviceConfigurationProto_TouchScreen_NOTOUCH,
            header_reader.touch_screen());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_Keyboard_QWERTY,
            header_reader.keyboard());
  EXPECT_FALSE(header_reader.has_hard_keyboard());
  EXPECT_EQ(234, header_reader.screen_density());
  EXPECT_EQ(512, header_reader.screen_width());
  EXPECT_EQ(456, header_reader.screen_height());
  EXPECT_EQ(device_configuration::DeviceConfigurationProto_ScreenLayout_SMALL,
            header_reader.screen_layout());
}

TEST_F(RecommendAppsFetcherImplTest, RetryCalledBeforeFirstRequest) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  device_data_manager_test_api_.SetKeyboardDevices(
      std::vector<ui::KeyboardDevice>{
          {1, ui::INPUT_DEVICE_INTERNAL, "internal keyboard"},
          {2, ui::INPUT_DEVICE_USB, "external keyboard"}});

  SetDisplaySize(gfx::Size(512, 456));

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  recommend_apps_fetcher_->Retry();
  EXPECT_TRUE(test_url_loader_factory_.pending_requests()->empty());

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);
}

TEST_F(RecommendAppsFetcherImplTest, EmptyResponse) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  SetDisplaySize(gfx::Size(512, 456));

  recommend_apps_fetcher_->Start();

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);
  test_url_loader_factory_.AddResponse(request->url.spec(), "");

  EXPECT_EQ(FakeRecommendAppsFetcherDelegate::Result::LOAD_ERROR,
            delegate_.WaitForResult());
}

TEST_F(RecommendAppsFetcherImplTest, EmptyAppList) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  SetDisplaySize(gfx::Size(512, 456));

  recommend_apps_fetcher_->Start();

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);
  test_url_loader_factory_.AddResponse(request->url.spec(), "[]");
}

TEST_F(RecommendAppsFetcherImplTest, ResponseWithLeadingBrackets) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);
}

TEST_F(RecommendAppsFetcherImplTest, MalformedJsonResponse) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(), ")}]'!2%^$");

  EXPECT_EQ(FakeRecommendAppsFetcherDelegate::Result::PARSE_ERROR,
            delegate_.WaitForResult());
}

TEST_F(RecommendAppsFetcherImplTest, UnexpectedResponseType) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(), "\"abcd\"");
}

TEST_F(RecommendAppsFetcherImplTest, ResponseWithMultipleApps) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  const std::string response =
      R"([{
           "title_": {"name_": "Test app 1"},
           "id_": {"id_": "test.app1"},
           "icon_": {
             "url_": {
               "privateDoNotAccessOrElseSafeUrlWrappedValue_": "http://test.app"
              }
            }
         }, {
           "id_": {"id_": "test.app2"}
         }])";

  test_url_loader_factory_.AddResponse(request->url.spec(), response);
}

TEST_F(RecommendAppsFetcherImplTest, InvalidAppItemsIgnored) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  const std::string response =
      R"([{
           "title_": {"name_": "Test app 1"},
           "id_": {"id_": "test.app1"},
           "icon_": {
             "url_": {
               "privateDoNotAccessOrElseSafeUrlWrappedValue_": "http://test.app"
              }
            }
         }, [], 2, {"id_": {"id_": "test.app2"}}, {"a": "b"}])";

  test_url_loader_factory_.AddResponse(request->url.spec(), response);
}

TEST_F(RecommendAppsFetcherImplTest, DictionaryResponse) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(), "{}");
}

TEST_F(RecommendAppsFetcherImplTest, InvalidErrorCodeType) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(),
                                       R"({"Error code": ""})");
}

TEST_F(RecommendAppsFetcherImplTest, ResponseWithErrorCode) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(),
                                       R"({"Error code": "6"})");
}

TEST_F(RecommendAppsFetcherImplTest, NotEnoughAppsError) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(),
                                       R"({"Error code": "5"})");
}

TEST_F(RecommendAppsFetcherImplTest, AppListRequestFailure) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(), "",
                                       net::HTTP_BAD_REQUEST);

  EXPECT_EQ(FakeRecommendAppsFetcherDelegate::Result::LOAD_ERROR,
            delegate_.WaitForResult());
}

TEST_F(RecommendAppsFetcherImplTest, SuccessOnRetry) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(),
                                       R"({"Error code": "5"})");
}

TEST_F(RecommendAppsFetcherImplTest, FailureOnRetry) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);
  test_url_loader_factory_.AddResponse(request->url.spec(),
                                       R"({"Error code": "5"})");
}

TEST_F(RecommendAppsFetcherImplTest, AppDiscoveryValidResponse) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(), kOneAppResponse);

  EXPECT_EQ(FakeRecommendAppsFetcherDelegate::Result::SUCCESS,
            delegate_.WaitForResult());
}

TEST_F(RecommendAppsFetcherImplTest, AppDiscoveryParseErrorResponse) {
  ASSERT_TRUE(recommend_apps_fetcher_);
  RecommendAppsFetcherImpl::ScopedGpuInfoForTest scoped(&gpu_info_);

  recommend_apps_fetcher_->Start();

  cros_display_config_->Flush();
  ASSERT_TRUE(cros_display_config_->RunGetDisplayUnitInfoListCallback(
      CreateDisplayUnitInfo(Dpi(117, 117), std::nullopt)));

  ASSERT_TRUE(arc_features_callback_);
  std::move(arc_features_callback_).Run(CreateArcFeaturesForTest());

  network::ResourceRequest* request = WaitForAppListRequest();
  ASSERT_TRUE(request);

  test_url_loader_factory_.AddResponse(request->url.spec(), ")}]'!2%^$");

  EXPECT_EQ(FakeRecommendAppsFetcherDelegate::Result::PARSE_ERROR,
            delegate_.WaitForResult());
}

}  // namespace apps