// Copyright 2018 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/banners/app_banner_manager_desktop.h"

#include <memory>
#include <string>
#include <utility>

#include "base/auto_reset.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/banners/app_banner_manager_browsertest_base.h"
#include "chrome/browser/banners/test_app_banner_manager_desktop.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/actions/chrome_action_id.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_command_controller.h"
#include "chrome/browser/ui/browser_window.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_dialog_utils.h"
#include "chrome/browser/ui/web_applications/web_app_dialogs.h"
#include "chrome/browser/web_applications/external_install_options.h"
#include "chrome/browser/web_applications/externally_managed_app_manager.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/password_manager/content/common/web_ui_constants.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/webapps/browser/banners/app_banner_metrics.h"
#include "components/webapps/browser/banners/app_banner_settings_helper.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/browser/uninstall_result_code.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/common/extension.h"

namespace webapps {

using State = AppBannerManager::State;

class AppBannerManagerDesktopBrowserTest
    : public AppBannerManagerBrowserTestBase {
 public:
  AppBannerManagerDesktopBrowserTest() {
    scoped_feature_list_.InitAndDisableFeature(
        ::features::kWebAppInstallDialog);
  }

  void SetUp() override {
    TestAppBannerManagerDesktop::SetUp();
    AppBannerManagerBrowserTestBase::SetUp();
  }

  void SetUpOnMainThread() override {
    AppBannerManagerBrowserTestBase::SetUpOnMainThread();
  }

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

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  base::AutoReset<web_app::InstallDialogTestResponse>
      auto_accept_pwa_install_confirmation_{
          web_app::SetPwaInstallationAutoRespondForTesting(
              web_app::InstallDialogTestResponse::kAcceptAndLaunch)};
};  // namespace webapps

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       WebAppBannerResolvesUserChoice) {
  base::HistogramTester tester;
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  auto* manager = TestAppBannerManagerDesktop::FromWebContents(web_contents);

  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), GetBannerURLWithAction("stash_event")));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  {
    // Trigger the installation prompt and wait for installation to occur.
    base::RunLoop run_loop;
    manager->SetCompleteCallback(run_loop.QuitClosure());
    ExecuteScript(web_contents, "callStashedPrompt();",
                  true /* with_gesture */);
    run_loop.Run();
    EXPECT_EQ(State::COMPLETE, manager->state_for_testing());
  }

  // Ensure that the userChoice promise resolves.
  const std::u16string title = u"Got userChoice: accepted";
  content::TitleWatcher watcher(web_contents, title);
  EXPECT_EQ(title, watcher.WaitAndGetTitle());

  tester.ExpectUniqueSample(kInstallDisplayModeHistogram,
                            blink::mojom::DisplayMode::kStandalone, 1);
}

// TODO(crbug.com/40637899): Flakes on most platforms.
IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       DISABLED_WebAppBannerFiresAppInstalled) {
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  auto* manager = TestAppBannerManagerDesktop::FromWebContents(web_contents);

  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), GetBannerURLWithAction("verify_appinstalled_stash_event")));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  {
    // Trigger the installation prompt and wait for installation to occur.
    base::RunLoop run_loop;
    manager->SetCompleteCallback(run_loop.QuitClosure());

    const GURL url = GetBannerURL();
    bool callback_called = false;

    web_app::SetInstalledCallbackForTesting(
        base::BindLambdaForTesting([&](const webapps::AppId& installed_app_id,
                                       webapps::InstallResultCode code) {
          EXPECT_EQ(webapps::InstallResultCode::kSuccessNewInstall, code);
          EXPECT_EQ(installed_app_id, web_app::GenerateAppId(
                                          /*manifest_id=*/std::nullopt, url));
          callback_called = true;
        }));

    ExecuteScript(web_contents, "callStashedPrompt();",
                  true /* with_gesture */);

    run_loop.Run();

    EXPECT_EQ(State::COMPLETE, manager->state_for_testing());
    EXPECT_TRUE(callback_called);
  }

  // Ensure that the appinstalled event fires.
  const std::u16string title = u"Got appinstalled: listener, attr";
  content::TitleWatcher watcher(web_contents, title);
  EXPECT_EQ(title, watcher.WaitAndGetTitle());
}

// TODO(crbug.com/40817384): Flaky failures.
#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
#define MAYBE_DestroyWebContents DISABLED_DestroyWebContents
#else
#define MAYBE_DestroyWebContents DestroyWebContents
#endif
IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       MAYBE_DestroyWebContents) {
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  auto* manager = TestAppBannerManagerDesktop::FromWebContents(web_contents);

  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), GetBannerURLWithAction("stash_event")));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  {
    // Trigger the installation and wait for termination to occur.
    base::RunLoop run_loop;
    bool callback_called = false;

    web_app::SetInstalledCallbackForTesting(
        base::BindLambdaForTesting([&](const webapps::AppId& installed_app_id,
                                       webapps::InstallResultCode code) {
          EXPECT_EQ(webapps::InstallResultCode::kWebContentsDestroyed, code);
          callback_called = true;
          run_loop.Quit();
        }));

    ExecuteScript(web_contents, "callStashedPrompt();",
                  true /* with_gesture */);

    // Closing WebContents destroys WebContents and AppBannerManager.
    browser()->tab_strip_model()->CloseWebContentsAt(
        browser()->tab_strip_model()->active_index(), 0);
    manager = nullptr;
    web_contents = nullptr;

    run_loop.Run();
    EXPECT_TRUE(callback_called);
  }
}

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       InstallPromptAfterUserMenuInstall) {
  base::HistogramTester tester;

  TestAppBannerManagerDesktop* manager =
      TestAppBannerManagerDesktop::FromWebContents(
          browser()->tab_strip_model()->GetActiveWebContents());

  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), GetBannerURLWithManifestAndQuery("/banners/minimal-ui.json",
                                                    "action", "stash_event")));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  // Install the app via the menu instead of the banner.
  chrome::BrowserCommandController::From(browser())->ExecuteCommand(
      IDC_INSTALL_PWA);
  manager->AwaitAppInstall();

  EXPECT_FALSE(manager->IsPromptAvailableForTesting());

  tester.ExpectUniqueSample(kInstallDisplayModeHistogram,
                            blink::mojom::DisplayMode::kMinimalUi, 1);
}

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       InstallPromptAfterUserOmniboxInstall) {
  base::HistogramTester tester;

  TestAppBannerManagerDesktop* manager =
      TestAppBannerManagerDesktop::FromWebContents(
          browser()->tab_strip_model()->GetActiveWebContents());

  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), GetBannerURLWithManifestAndQuery("/banners/fullscreen.json",
                                                    "action", "stash_event")));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  // Install the app via the menu instead of the banner.
  actions::ActionManager::Get().FindAction(kActionInstallPwa)->InvokeAction();

  manager->AwaitAppInstall();

  EXPECT_FALSE(manager->IsPromptAvailableForTesting());

  tester.ExpectUniqueSample(kInstallDisplayModeHistogram,
                            blink::mojom::DisplayMode::kFullscreen, 1);
}

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       PolicyAppInstalled_Prompt) {
  TestAppBannerManagerDesktop* manager =
      TestAppBannerManagerDesktop::FromWebContents(
          browser()->tab_strip_model()->GetActiveWebContents());

  // Install web app by policy.
  web_app::ExternalInstallOptions options =
      web_app::CreateInstallOptions(GetBannerURL());
  options.install_source = web_app::ExternalInstallSource::kExternalPolicy;
  options.user_display_mode = web_app::mojom::UserDisplayMode::kBrowser;
  web_app::ExternallyManagedAppManagerInstall(browser()->GetProfile(), options);

  // Run promotability check.
  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetBannerURL()));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  EXPECT_EQ(InstallableWebAppCheckResult::kYes_Promotable,
            manager->GetInstallableWebAppCheckResult());
  EXPECT_TRUE(manager->IsPromptAvailableForTesting());
}

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       PolicyAppUninstalled_Prompt) {
  TestAppBannerManagerDesktop* manager =
      TestAppBannerManagerDesktop::FromWebContents(
          browser()->tab_strip_model()->GetActiveWebContents());
  Profile* profile = browser()->GetProfile();

  // Install web app by policy.
  web_app::ExternalInstallOptions options =
      web_app::CreateInstallOptions(GetBannerURL());
  options.install_source = web_app::ExternalInstallSource::kExternalPolicy;
  options.user_display_mode = web_app::mojom::UserDisplayMode::kBrowser;
  web_app::ExternallyManagedAppManagerInstall(profile, options);

  // Uninstall web app by policy by synchronizing with an empty
  // `ExternalInstallOptions` vector.
  {
    base::RunLoop run_loop;
    web_app::WebAppProvider::GetForTest(profile)
        ->externally_managed_app_manager()
        .SynchronizeInstalledApps(
            {}, web_app::ExternalInstallSource::kExternalPolicy,
            base::BindLambdaForTesting(
                [&](std::map<GURL /*install_url*/,
                             web_app::ExternallyManagedAppManagerInstallResult>
                        install_results,
                    std::map<GURL /*install_url*/, webapps::UninstallResultCode>
                        uninstall_results) {
                  EXPECT_TRUE(install_results.empty());
                  ASSERT_TRUE(uninstall_results.contains(GetBannerURL()));
                  EXPECT_EQ(webapps::UninstallResultCode::kAppRemoved,
                            uninstall_results[GetBannerURL()]);
                  run_loop.Quit();
                }));
    run_loop.Run();
  }

  // Run promotability check.
  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetBannerURL()));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  EXPECT_EQ(InstallableWebAppCheckResult::kYes_Promotable,
            manager->GetInstallableWebAppCheckResult());
  EXPECT_TRUE(manager->IsPromptAvailableForTesting());
}

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       InstallPromptAfterUserMenuInstall_DisplayOverride) {
  base::HistogramTester tester;

  TestAppBannerManagerDesktop* manager =
      TestAppBannerManagerDesktop::FromWebContents(
          browser()->tab_strip_model()->GetActiveWebContents());

  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(), GetBannerURLWithManifestAndQuery(
                       "/banners/manifest_display_override.json", "action",
                       "stash_event")));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  // Install the app via the menu instead of the banner.
  chrome::BrowserCommandController::From(browser())->ExecuteCommand(
      IDC_INSTALL_PWA);
  manager->AwaitAppInstall();

  EXPECT_FALSE(manager->IsPromptAvailableForTesting());

  tester.ExpectUniqueSample(kInstallDisplayModeHistogram,
                            blink::mojom::DisplayMode::kMinimalUi, 1);
}

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       WebAppBannerResolvesUserChoice_DisplayOverride) {
  base::HistogramTester tester;
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  auto* manager = TestAppBannerManagerDesktop::FromWebContents(web_contents);

  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(),
        GetBannerURLWithManifestAndQuery(
            "/banners/manifest_display_override_display_is_browser.json",
            "action", "stash_event")));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  {
    // Trigger the installation prompt and wait for installation to occur.
    base::RunLoop run_loop;
    manager->SetCompleteCallback(run_loop.QuitClosure());
    ExecuteScript(web_contents, "callStashedPrompt();",
                  true /* with_gesture */);
    run_loop.Run();
    EXPECT_EQ(State::COMPLETE, manager->state_for_testing());
  }

  // Ensure that the userChoice promise resolves.
  const std::u16string title = u"Got userChoice: accepted";
  content::TitleWatcher watcher(web_contents, title);
  EXPECT_EQ(title, watcher.WaitAndGetTitle());

  tester.ExpectUniqueSample(kInstallDisplayModeHistogram,
                            blink::mojom::DisplayMode::kStandalone, 1);
}

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       PolicyAppInstalled_Prompt_DisplayOverride) {
  TestAppBannerManagerDesktop* manager =
      TestAppBannerManagerDesktop::FromWebContents(
          browser()->tab_strip_model()->GetActiveWebContents());

  // Install web app by policy.
  web_app::ExternalInstallOptions options =
      web_app::CreateInstallOptions(GetBannerURLWithManifest(
          "/banners/manifest_display_override_contains_browser.json"));
  options.install_source = web_app::ExternalInstallSource::kExternalPolicy;
  options.user_display_mode = web_app::mojom::UserDisplayMode::kBrowser;
  web_app::ExternallyManagedAppManagerInstall(browser()->GetProfile(), options);

  // Run promotability check.
  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GetBannerURL()));
    run_loop.Run();
    EXPECT_EQ(State::PENDING_PROMPT, manager->state_for_testing());
  }

  EXPECT_EQ(InstallableWebAppCheckResult::kYes_Promotable,
            manager->GetInstallableWebAppCheckResult());
  EXPECT_TRUE(manager->IsPromptAvailableForTesting());
}

class AppBannerManagerDesktopBrowserTestForPasswordManagerPage
    : public AppBannerManagerDesktopBrowserTest {
 public:
  void SetUp() override {
    TestAppBannerManagerDesktop::SetUp();
    AppBannerManagerBrowserTestBase::SetUp();
  }
};

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTestForPasswordManagerPage,
                       WebUiPasswordManagerApp) {
  TestAppBannerManagerDesktop* manager =
      TestAppBannerManagerDesktop::FromWebContents(
          browser()->tab_strip_model()->GetActiveWebContents());

  // Simulate loading a PasswordManager page.
  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(),
        GURL(base::StrCat(
            {"chrome://", password_manager::kChromeUIPasswordManagerHost}))));
    run_loop.Run();
  }

  EXPECT_EQ(InstallableWebAppCheckResult::kYes_Promotable,
            manager->GetInstallableWebAppCheckResult());
}

IN_PROC_BROWSER_TEST_F(AppBannerManagerDesktopBrowserTest,
                       PipelineRunsAfterStop) {
  TestAppBannerManagerDesktop* manager =
      TestAppBannerManagerDesktop::FromWebContents(
          browser()->tab_strip_model()->GetActiveWebContents());

  {
    base::RunLoop run_loop;
    manager->SetBannerPromptReplyCallback(run_loop.QuitClosure());

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser(),
        embedded_test_server()->GetURL("/web_apps/stop-loading-early.html")));
    run_loop.Run();
  }

  EXPECT_EQ(InstallableWebAppCheckResult::kYes_Promotable,
            manager->GetInstallableWebAppCheckResult())
      << manager->debug_log();
}

}  // namespace webapps
