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

#include <initializer_list>
#include <optional>
#include <string>
#include <utility>

#include "base/check.h"
#include "base/files/file_path.h"
#include "base/memory/scoped_refptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/test_future.h"
#include "base/test/with_feature_override.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/scoped_test_mv2_enabler.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/shortcuts/shortcut_icon_generator.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/ui/web_applications/web_app_browsertest_base.h"
#include "chrome/browser/ui/web_applications/web_app_dialogs.h"
#include "chrome/browser/web_applications/model/web_app_icon_types.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom-shared.h"
#include "chrome/browser/web_applications/test/web_app_test_observers.h"
#include "chrome/browser/web_applications/test/web_app_test_utils.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_filter.h"
#include "chrome/browser/web_applications/web_app_icon_generator.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "components/webapps/browser/features.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/common/extension.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-shared.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/skia_util.h"
#include "url/gurl.h"

namespace {

std::string LoadExtension(Profile* profile, const base::FilePath& path) {
  extensions::ChromeTestExtensionLoader loader(profile);
  scoped_refptr<const extensions::Extension> extension =
      loader.LoadExtension(path);
  EXPECT_TRUE(extension);
  return extension->id();
}

}  // namespace

namespace web_app {

// This test currently only works on ChromeOS, and will be removed once the
// behavior of shortcuts on ChromeOS is made similar to Windows, Mac and Linux
// platforms.
// On ChromeOS, the Create Shortcut dialog creates DIY apps.
class CreateShortcutBrowserTest : public base::test::WithFeatureOverride,
                                  public WebAppBrowserTestBase {
 public:
  CreateShortcutBrowserTest()
      : base::test::WithFeatureOverride(::features::kWebAppInstallDialog) {}
  webapps::AppId InstallDiyAppForCurrentUrl(bool open_as_window = false) {
    WebAppTestInstallObserver observer(profile());
    observer.BeginListening();
    {
      std::optional<base::AutoReset<CreateShortcutDialogCheckState>> auto_check;
      if (open_as_window) {
        auto_check.emplace(
            SetCreateShortcutDialogCheckStateForTesting(  // IN-TEST
                CreateShortcutDialogCheckState::kChecked));
      }
      base::AutoReset<InstallDialogTestResponse> auto_accept =
          SetPwaInstallationAutoRespondForTesting(  // IN-TEST
              InstallDialogTestResponse::kAcceptAndLaunch);
      CHECK(chrome::ExecuteCommand(browser(), IDC_CREATE_SHORTCUT));
      webapps::AppId app_id = observer.Wait();
      return app_id;
    }
  }

  // Start URL points to `PageWithDifferentStartUrlManifestStartUrl`.
  GURL PageWithDifferentStartUrl() {
    return embedded_https_test_server().GetURL(
        "/web_apps/different_start_url.html");
  }

  GURL PageWithDifferentStartUrlManifestStartUrl() {
    return embedded_https_test_server().GetURL("/web_apps/basic.html");
  }

  WebAppRegistrar& registrar() {
    auto* provider = WebAppProvider::GetForTest(profile());
    CHECK(provider);
    return provider->registrar_unsafe();
  }

  WebAppSyncBridge& sync_bridge() {
    auto* provider = WebAppProvider::GetForTest(profile());
    CHECK(provider);
    return provider->sync_bridge_unsafe();
  }
};

IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest,
                       CreateShortcutForInstallableSite) {
  base::UserActionTester user_action_tester;
  NavigateViaLinkClickToURLAndWait(browser(), GetInstallableAppURL());

  webapps::AppId app_id = InstallDiyAppForCurrentUrl();
  EXPECT_EQ(registrar().GetAppShortName(app_id), GetInstallableAppName());
  // Shortcut apps to PWAs should launch in a tab.
  EXPECT_EQ(registrar().GetAppUserDisplayMode(app_id),
            mojom::UserDisplayMode::kBrowser);

  EXPECT_EQ(0, user_action_tester.GetActionCount("InstallWebAppFromMenu"));
  EXPECT_EQ(1, user_action_tester.GetActionCount("CreateShortcut"));
}

// TODO(crbug.com/40269598): flaky on Mac11 Tests builder.
#if BUILDFLAG(IS_MAC)
#define MAYBE_InstallSourceRecorded DISABLED_InstallSourceRecorded
#else
#define MAYBE_InstallSourceRecorded InstallSourceRecorded
#endif
IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest, MAYBE_InstallSourceRecorded) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // LatestWebAppInstallSource should be correctly set and reported to UMA for
  // both installable and non-installable (shortcut) sites.
  for (const GURL& url :
       {GetInstallableAppURL(),
        embedded_test_server()->GetURL(
            "/web_apps/get_manifest.html?theme_color_only.json")}) {
    base::HistogramTester histogram_tester;
    NavigateViaLinkClickToURLAndWait(browser(), url);
    webapps::AppId app_id = InstallDiyAppForCurrentUrl();

    EXPECT_EQ(webapps::WebappInstallSource::MENU_CREATE_SHORTCUT,
              *registrar().GetLatestAppInstallSource(app_id));
    histogram_tester.ExpectUniqueSample(
        "Webapp.Install.InstallEvent",
        static_cast<int>(webapps::WebappInstallSource::MENU_CREATE_SHORTCUT),
        1);
  }
}

IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest,
                       CanInstallOverTabShortcutApp) {
  NavigateViaLinkClickToURLAndWait(browser(), GetInstallableAppURL());
  InstallDiyAppForCurrentUrl();

  BrowserWindowInterface* new_browser =
      NavigateInNewWindowAndAwaitInstallabilityCheck(GetInstallableAppURL());

  EXPECT_EQ(GetAppMenuCommandState(IDC_CREATE_SHORTCUT, new_browser), kEnabled);
  EXPECT_EQ(GetAppMenuCommandState(IDC_INSTALL_PWA, new_browser), kEnabled);
  EXPECT_EQ(GetAppMenuCommandState(IDC_OPEN_IN_PWA_WINDOW, new_browser),
            kNotPresent);
}

IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest,
                       CannotInstallOverWindowShortcutApp) {
  NavigateViaLinkClickToURLAndWait(browser(), GetInstallableAppURL());
  webapps::AppId app_id = InstallDiyAppForCurrentUrl();
  // Change launch container to open in window.
  sync_bridge().SetAppUserDisplayModeForTesting(
      app_id, mojom::UserDisplayMode::kStandalone);

  BrowserWindowInterface* new_browser =
      NavigateInNewWindowAndAwaitInstallabilityCheck(GetInstallableAppURL());

  EXPECT_EQ(GetAppMenuCommandState(IDC_CREATE_SHORTCUT, new_browser), kEnabled);
  EXPECT_EQ(GetAppMenuCommandState(IDC_INSTALL_PWA, new_browser), kNotPresent);
  EXPECT_EQ(GetAppMenuCommandState(IDC_OPEN_IN_PWA_WINDOW, new_browser),
            kEnabled);
}

// Check that toolbar is not shown for shortcut apps within extensions pages.
// This simulates a case where the user has manually navigated to a page hosted
// within an extension, then added it as a shortcut app.
// Regression test for https://crbug.com/40569785.
//
// TODO(crbug.com/40793595): Remove chrome-extension scheme for web apps.
IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest,
                       ShouldShowCustomTabBarForExtensionPage) {
  // TODO(https://crbug.com/40804030): Remove this when updated to use MV3.
  extensions::ScopedTestMV2Enabler mv2_enabler;

  // This involves the creation of a regular (non-app) extension with a popup
  // page, and the creation of a shortcut app created from the popup page URL
  // (allowing the extension's popup page to be loaded in a window).

  base::FilePath test_data_dir_;
  base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir_);

  // Install the extension that has the popup page.
  std::string extension_id =
      LoadExtension(profile(), test_data_dir_.AppendASCII("extensions")
                                   .AppendASCII("ui")
                                   .AppendASCII("browser_action_popup"));
  base::RunLoop().RunUntilIdle();  // Ensure the extension is fully loaded.

  // Install the shortcut app that links to the extension's popup page.
  const GURL popup_url("chrome-extension://" + extension_id + "/popup.html");

  NavigateViaLinkClickToURLAndWait(browser(), popup_url);

  // TODO(crbug.com/40793595): IDC_CREATE_SHORTCUT command must become disabled.
  ASSERT_TRUE(chrome::IsCommandEnabled(browser(), IDC_CREATE_SHORTCUT));

  const webapps::AppId app_id = InstallDiyAppForCurrentUrl();
  ASSERT_FALSE(app_id.empty());
  BrowserWindowInterface* const app_browser =
      LaunchWebAppBrowserAndWait(app_id);
  CHECK(app_browser);
  CHECK(app_browser != browser());

  // Navigate to the app's launch page; the toolbar should not be visible,
  // because extensions pages are secure.
  NavigateAndCheckForToolbar(app_browser, popup_url, false);
}

// Tests that Create Shortcut doesn't timeout on a page that has a delayed
// iframe load. Context: crbug.com/40671065
IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest, WorksAfterDelayedIFrameLoad) {
  ASSERT_TRUE(embedded_test_server()->Start());
  NavigateViaLinkClickToURLAndWait(
      browser(),
      embedded_test_server()->GetURL("/favicon/page_with_favicon.html"));

  // Append an iframe and wait for it to finish loading.
  const char script[] = R"(
    const iframe = document.createElement('iframe');
    new Promise(resolve => {
      iframe.onload = _ => resolve('success');
      iframe.srcdoc = 'inner page';
      document.body.appendChild(iframe);
    });
  )";
  EXPECT_EQ(content::EvalJs(
                browser()->tab_strip_model()->GetActiveWebContents(), script)
                .ExtractString(),
            "success");

  InstallDiyAppForCurrentUrl();
}

// Tests that Create Shortcut on non-promotable sites still uses available
// manifest data.
IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest,
                       UseNonPromotableManifestData) {
  ASSERT_TRUE(embedded_test_server()->Start());
  NavigateViaLinkClickToURLAndWait(
      browser(), embedded_test_server()->GetURL(
                     "/web_apps/get_manifest.html?theme_color_only.json"));
  webapps::AppId app_id = InstallDiyAppForCurrentUrl();
  EXPECT_EQ(registrar().GetAppThemeColor(app_id),
            SkColorSetRGB(0x12, 0x34, 0x56));
}

// Tests that Create Shortcut won't use manifest data that's invalid.
IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest, IgnoreInvalidManifestData) {
  ASSERT_TRUE(embedded_test_server()->Start());
  GURL url = embedded_test_server()->GetURL(
      "/web_apps/get_manifest.html?invalid_start_url.json");
  NavigateViaLinkClickToURLAndWait(browser(), url);
  webapps::AppId app_id = InstallDiyAppForCurrentUrl();
  EXPECT_EQ(registrar().GetAppStartUrl(app_id), url);
}

// TODO(crbug.com/40883914): Un-flake and re-enable this test.
IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest,
                       DISABLED_CreateShortcutAgainOverwriteUserDisplayMode) {
  base::UserActionTester user_action_tester;
  NavigateViaLinkClickToURLAndWait(browser(), GetInstallableAppURL());

  webapps::AppId app_id = InstallDiyAppForCurrentUrl();
  EXPECT_EQ(registrar().GetAppShortName(app_id), GetInstallableAppName());
  // Shortcut apps to PWAs should launch in a tab.
  EXPECT_EQ(registrar().GetAppUserDisplayMode(app_id),
            mojom::UserDisplayMode::kBrowser);
  // TODO(crbug.com/40808578): We need to wait a bit longer for the
  // WebAppInstallTask to complete before starting another install.
  // Move the install/update/uninstall events out of
  // WebAppRegistrarObserver and into a WebAppInstallManagerObserver
  // interface so they can be guaranteed to fire after the
  // WebAppInstallTask's lifetime has ended.
  base::RunLoop().RunUntilIdle();

  InstallDiyAppForCurrentUrl(/*open_as_window=*/true);
  // Re-install with enabling open_as_window should update user display mode.
  EXPECT_EQ(registrar().GetAppUserDisplayMode(app_id),
            mojom::UserDisplayMode::kStandalone);
}

// TODO(crbug.com/40908616): Re-enable this test
IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest,
                       DISABLED_OpenShortcutWindowOnlyOnce) {
  base::UserActionTester user_action_tester;
  NavigateViaLinkClickToURLAndWait(browser(), GetInstallableAppURL());

  WebAppTestInstallObserver observer(profile());
  // The "Create shortcut" call is executed twice, but the dialog
  // must be shown only once.
  ASSERT_TRUE(chrome::ExecuteCommand(browser(), IDC_CREATE_SHORTCUT));
  ASSERT_TRUE(chrome::ExecuteCommand(browser(), IDC_CREATE_SHORTCUT));

  EXPECT_EQ(1u, provider().command_manager().GetCommandCountForTesting());
}

// Tests that Create Shortcut on sites where the title is a url generates a
// letter icon correctly and does not use the "H" letter from the "https"
// scheme.
IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest, UseHostWhenTitleIsUrl) {
  NavigateViaLinkClickToURLAndWait(
      browser(),
      embedded_https_test_server().GetURL("example.com", "/empty.html"));
  webapps::AppId app_id = InstallDiyAppForCurrentUrl();

  base::test::TestFuture<IconMetadataFromDisk> future;
  WebAppProvider::GetForTest(profile())
      ->icon_manager()
      .ReadTrustedIconsWithFallbackToManifestIcons(
          app_id, {icon_size::k128}, IconPurpose::ANY, future.GetCallback());

  IconMetadataFromDisk icon_metadata = future.Take();
  OrderedSizeToBitmap icon_bitmaps = std::move(icon_metadata.icons_map);
  auto icon_it = icon_bitmaps.find(icon_size::k128);
  ASSERT_TRUE(icon_it != icon_bitmaps.end());
  SkBitmap bitmap = icon_it->second;

  // The letter for https://example.com should be the first letter of the host,
  // which is "E".
  SkBitmap generated_icon_bitmap =
      shortcuts::GenerateBitmap(icon_size::k128, u"E");
  EXPECT_TRUE(gfx::BitmapsAreEqual(bitmap, generated_icon_bitmap));
}

IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest,
                       InstallableSiteDifferentStartUrl) {
  NavigateViaLinkClickToURLAndWait(browser(), PageWithDifferentStartUrl());
  webapps::AppId app_id = InstallDiyAppForCurrentUrl();

  EXPECT_EQ(registrar().GetAppUserDisplayMode(app_id),
            mojom::UserDisplayMode::kBrowser);

  // Title from manifest.
  EXPECT_EQ(registrar().GetAppShortName(app_id), "Basic web app");

  // Start URL from manifest.
  EXPECT_EQ(registrar().GetAppById(app_id)->start_url(),
            PageWithDifferentStartUrlManifestStartUrl());
}

IN_PROC_BROWSER_TEST_P(CreateShortcutBrowserTest, InstallOverTabShortcutApp) {
  NavigateViaLinkClickToURLAndWait(browser(), GetInstallableAppURL());
  webapps::AppId app_installed_from_menu = InstallDiyAppForCurrentUrl();

  EXPECT_FALSE(registrar().AppMatches(app_installed_from_menu,
                                      WebAppFilter::IsCraftedApp()));

  BrowserWindowInterface* new_browser =
      NavigateInNewWindowAndAwaitInstallabilityCheck(GetInstallableAppURL());

  EXPECT_EQ(GetAppMenuCommandState(IDC_CREATE_SHORTCUT, new_browser), kEnabled);
  EXPECT_EQ(GetAppMenuCommandState(IDC_INSTALL_PWA, new_browser), kEnabled);
  EXPECT_EQ(GetAppMenuCommandState(IDC_OPEN_IN_PWA_WINDOW, new_browser),
            kNotPresent);

  webapps::AppId web_app_id = test::InstallPwaForCurrentUrl(new_browser);

  EXPECT_EQ(app_installed_from_menu, web_app_id);
  EXPECT_TRUE(registrar().AppMatches(web_app_id, WebAppFilter::IsCraftedApp()));
}

INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(CreateShortcutBrowserTest);

}  // namespace web_app
