// Copyright 2023 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/web_applications/commands/install_app_locally_command.h"

#include <memory>
#include <utility>

#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"
#include "chrome/browser/web_applications/proto/web_app_os_integration_state.pb.h"
#include "chrome/browser/web_applications/test/fake_web_app_origin_association_manager.h"
#include "chrome/browser/web_applications/test/fake_web_app_provider.h"
#include "chrome/browser/web_applications/test/os_integration_test_override_impl.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_icon_generator.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_install_params.h"
#include "chrome/browser/web_applications/web_app_management_type.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "components/sync/base/time.h"
#include "components/webapps/browser/install_result_code.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "url/gurl.h"

namespace web_app {
namespace {

class InstallAppLocallyCommandTest : public WebAppTest {
 public:
  const GURL kWebAppUrl = GURL("https://example.com/path/index.html");
  InstallAppLocallyCommandTest() = default;
  ~InstallAppLocallyCommandTest() override = default;

  void SetUp() override {
    WebAppTest::SetUp();
    {
      base::ScopedAllowBlockingForTesting allow_blocking;
      test_override_ = OsIntegrationTestOverrideImpl::OverrideForTesting();
    }

    auto file_handler_manager =
        std::make_unique<WebAppFileHandlerManager>(profile());
    auto protocol_handler_manager =
        std::make_unique<WebAppProtocolHandlerManager>(profile());
    auto os_integration_manager = std::make_unique<OsIntegrationManager>(
        profile(), std::move(file_handler_manager),
        std::move(protocol_handler_manager));
    fake_provider().SetOsIntegrationManager(std::move(os_integration_manager));

    auto origin_association_manager =
        std::make_unique<FakeWebAppOriginAssociationManager>(*profile());
    origin_association_manager->set_pass_through(true);
    fake_provider().SetOriginAssociationManager(
        std::move(origin_association_manager));

    test::AwaitStartWebAppProviderAndSubsystems(profile());
  }

  void TearDown() override {
    // Blocking required due to file operations in the shortcut override
    // destructor.
    test::UninstallAllWebApps(profile());
    {
      base::ScopedAllowBlockingForTesting allow_blocking;
      test_override_.reset();
    }
    WebAppTest::TearDown();
  }

  webapps::AppId InstallNonLocallyInstalledAppWithIcons(
      OrderedSizeToBitmap icon_map,
      proto::InstallState install_state =
          proto::InstallState::SUGGESTED_FROM_ANOTHER_DEVICE) {
    std::unique_ptr<WebAppInstallInfo> info =
        WebAppInstallInfo::CreateWithStartUrlForTesting(kWebAppUrl);
    info->title = u"Test App";
    info->user_display_mode = mojom::UserDisplayMode::kStandalone;
    info->icon_bitmaps.any = std::move(icon_map);
    if (install_state == proto::InstallState::SUGGESTED_FROM_MIGRATION) {
      info->migration_sources.emplace_back(
          webapps::ManifestId(GURL("https://migration.example.com/start.html")),
          MigrationBehavior::kSuggest);
    }
    base::test::TestFuture<const webapps::AppId&, webapps::InstallResultCode>
        result;

    web_app::WebAppInstallParams params;
    params.install_state = install_state;
    params.add_to_applications_menu = false;
    params.add_to_desktop = false;
    params.add_to_quick_launch_bar = false;
    params.add_to_search = false;
    // InstallFromInfo does not trigger OS integration.
    fake_provider().scheduler().InstallFromInfoWithParams(
        std::move(info), /*overwrite_existing_manifest_fields=*/true,
        webapps::WebappInstallSource::OMNIBOX_INSTALL_ICON,
        result.GetCallback(), params);
    bool success = result.Wait();
    EXPECT_TRUE(success);
    if (!success) {
      return webapps::AppId();
    }
    EXPECT_EQ(result.Get<webapps::InstallResultCode>(),
              webapps::InstallResultCode::kSuccessNewInstall);
    const webapps::AppId app_id = result.Get<webapps::AppId>();
    return app_id;
  }

  bool HasShortcutsOsIntegration() {
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
    return true;
#else
    return false;
#endif
  }

 protected:
  SkBitmap CreateSolidColorIcon(int size, SkColor color) {
    SkBitmap bitmap;
    bitmap.allocN32Pixels(size, size);
    bitmap.eraseColor(color);
    return bitmap;
  }

  SkColor GetShortcutColor(const webapps::AppId& app_id,
                           const std::string& app_name) {
    if (!HasShortcutsOsIntegration()) {
      return SK_ColorTRANSPARENT;
    }

    scoped_refptr<OsIntegrationTestOverrideImpl> test_override =
        OsIntegrationTestOverrideImpl::Get();

#if BUILDFLAG(IS_WIN)
    std::optional<SkColor> desktop_color =
        test_override->GetShortcutIconTopLeftColor(
            profile(), test_override->desktop(), app_id, app_name);
    std::optional<SkColor> application_menu_icon_color =
        test_override->GetShortcutIconTopLeftColor(
            profile(), test_override->application_menu(), app_id, app_name);
    EXPECT_EQ(desktop_color.value(), application_menu_icon_color.value());
    return desktop_color.value();
#elif BUILDFLAG(IS_MAC)
    std::optional<SkColor> icon_color =
        test_override->GetShortcutIconTopLeftColor(
            profile(), test_override->chrome_apps_folder(), app_id, app_name);
    EXPECT_TRUE(icon_color.has_value());
    return icon_color.value();
#elif BUILDFLAG(IS_LINUX)
    std::optional<SkColor> icon_color =
        test_override->GetShortcutIconTopLeftColor(
            profile(), test_override->desktop(), app_id, app_name,
            kLauncherIconSize);
    EXPECT_TRUE(icon_color.has_value());
    return icon_color.value();
#else
    NOTREACHED() << "Shortcuts not supported for other OS";
#endif
  }

 private:
  std::unique_ptr<OsIntegrationTestOverrideImpl::BlockingRegistration>
      test_override_;

  base::test::ScopedFeatureList scoped_feature_list_{
      blink::features::kWebAppMigrationApi};
};

TEST_F(InstallAppLocallyCommandTest, BasicBehavior) {
  // Create an app that is not locally installed, i.e. has the
  // is_locally_installed bit set to false and there is no OS integration
  // defined for it.
  OrderedSizeToBitmap icon_map;
  icon_map[icon_size::k16] = CreateSolidColorIcon(icon_size::k16, SK_ColorBLUE);
  icon_map[icon_size::k24] = CreateSolidColorIcon(icon_size::k24, SK_ColorRED);
  icon_map[icon_size::k128] =
      CreateSolidColorIcon(icon_size::k128, SK_ColorGREEN);
  const webapps::AppId& app_id =
      InstallNonLocallyInstalledAppWithIcons(std::move(icon_map));

  auto state =
      fake_provider().registrar_unsafe().GetAppCurrentOsIntegrationState(
          app_id);
  ASSERT_TRUE(state.has_value());
  const proto::os_state::WebAppOsIntegration& os_integration_state =
      state.value();

  if (HasShortcutsOsIntegration()) {
    ASSERT_FALSE(os_integration_state.has_shortcut());
  }

  // Install app locally.
  base::test::TestFuture<void> test_future;
  fake_provider().scheduler().InstallAppLocally(app_id,
                                                test_future.GetCallback());
  EXPECT_TRUE(test_future.Wait());

  auto updated_state =
      fake_provider().registrar_unsafe().GetAppCurrentOsIntegrationState(
          app_id);
  ASSERT_TRUE(updated_state.has_value());
  const proto::os_state::WebAppOsIntegration& updated_os_states =
      updated_state.value();
  ASSERT_TRUE(updated_os_states.has_shortcut());

  EXPECT_TRUE(
      fake_provider().registrar_unsafe().GetAppById(app_id)->GetSources().Has(
          WebAppManagement::kUserInstalled));

  // OS integration should be triggered now.
  if (HasShortcutsOsIntegration()) {
    ASSERT_TRUE(OsIntegrationTestOverrideImpl::Get()->IsShortcutCreated(
        profile(), app_id,
        fake_provider().registrar_unsafe().GetAppShortName(app_id)));

    // On all desktop platforms, the shortcut icon that is used for the
    // launcher is icon_size::k128, which should be GREEN as per the icon_map
    // being used above.
    ASSERT_THAT(
        GetShortcutColor(
            app_id, fake_provider().registrar_unsafe().GetAppShortName(app_id)),
        testing::Eq(SK_ColorGREEN));
  }
}

TEST_F(InstallAppLocallyCommandTest, AppNotInRegistrar) {
  const webapps::AppId app_id = "abcde";

  base::test::TestFuture<void> test_future;
  fake_provider().scheduler().InstallAppLocally(app_id,
                                                test_future.GetCallback());
  EXPECT_TRUE(test_future.Wait());
  EXPECT_FALSE(
      fake_provider().registrar_unsafe().GetInstallState(app_id).has_value());
}

TEST_F(InstallAppLocallyCommandTest, MigrationPWAsNotAllowed) {
  OrderedSizeToBitmap icon_map;
  icon_map[icon_size::k128] =
      CreateSolidColorIcon(icon_size::k128, SK_ColorGREEN);
  const webapps::AppId& app_id = InstallNonLocallyInstalledAppWithIcons(
      std::move(icon_map), proto::InstallState::SUGGESTED_FROM_MIGRATION);

  base::test::TestFuture<void> test_future;
  fake_provider().scheduler().InstallAppLocally(app_id,
                                                test_future.GetCallback());
  EXPECT_TRUE(test_future.Wait());

  // Install state not migrated, no OS integration done.
  auto state =
      fake_provider().registrar_unsafe().GetAppCurrentOsIntegrationState(
          app_id);
  EXPECT_TRUE(state.has_value());
  EXPECT_FALSE(state.value().has_shortcut());
  EXPECT_TRUE(fake_provider().registrar_unsafe().AppMatches(
      app_id, WebAppFilter::IsAppSuggestedForMigration()));
}

}  // namespace
}  // namespace web_app
