// 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 <memory>
#include <string>
#include <vector>

#include "base/auto_reset.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/task/current_thread.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/test_simple_task_runner.h"
#include "base/time/time.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/banners/test_app_banner_manager_desktop.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.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/user_education/user_education_service.h"
#include "chrome/browser/user_education/user_education_service_factory.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/test/web_app_test_observers.h"
#include "chrome/browser/web_applications/visited_manifest_manager.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/webapps/installable/ml_promotion_browsertest_base.h"
#include "chrome/common/chrome_features.h"
#include "components/segmentation_platform/public/constants.h"
#include "components/segmentation_platform/public/input_context.h"
#include "components/segmentation_platform/public/testing/mock_segmentation_platform_service.h"
#include "components/segmentation_platform/public/trigger.h"
#include "components/segmentation_platform/public/types/processed_value.h"
#include "components/ukm/test_ukm_recorder.h"
#include "components/user_education/common/user_education_data.h"
#include "components/user_education/common/user_education_features.h"
#include "components/webapps/browser/features.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/installable/metrics/site_quality_metrics_task.h"
#include "components/webapps/browser/installable/ml_install_operation_tracker.h"
#include "components/webapps/browser/installable/ml_install_result_reporter.h"
#include "components/webapps/browser/installable/ml_installability_promoter.h"
#include "components/webapps/browser/test/service_worker_registration_waiter.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/test/dialog_test.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"

namespace webapps {
namespace {
using InstallUkmEntry = ukm::builders::Site_Install;
using ManifestUkmEntry = ukm::builders::Site_Manifest;
using QualityUkmEntry = ukm::builders::Site_Quality;
using segmentation_platform::HasTrainingLabel;
using segmentation_platform::InputContext;
using segmentation_platform::IsInputContextWithArgs;
using segmentation_platform::MockSegmentationPlatformService;
using segmentation_platform::TrainingRequestId;
using segmentation_platform::processing::ProcessedValue;
using testing::_;
using testing::Pointee;
using webapps::MLInstallabilityPromoter;
using webapps::SiteInstallMetrics;
using webapps::SiteQualityMetrics;
using MlInstallResponse = MlInstallResultReporter::MlInstallResponse;

segmentation_platform::ClassificationResult CreateClassificationResult(
    std::string label,
    TrainingRequestId request_id) {
  segmentation_platform::ClassificationResult result(
      segmentation_platform::PredictionStatus::kSucceeded);
  result.ordered_labels.emplace_back(label);
  result.request_id = request_id;
  return result;
}

enum class InstallDialogState {
  kSimpleInstallDialog = 0,
  kDetailedInstallDialog = 1,
  kCreateShortcutDialog = 2,
  kMaxValue = kCreateShortcutDialog
};

std::string GetMLPromotionDialogTestName(
    const ::testing::TestParamInfo<InstallDialogState>& info) {
  switch (info.param) {
    case InstallDialogState::kSimpleInstallDialog:
      return "Simple_Install_Dialog";
    case InstallDialogState::kDetailedInstallDialog:
      return "Detailed_Install_Dialog";
    case InstallDialogState::kCreateShortcutDialog:
      return "Create_Shortcut_Dialog";
  }
}

class WebContentsObserverAdapter : public content::WebContentsObserver {
 public:
  explicit WebContentsObserverAdapter(content::WebContents* web_contents)
      : content::WebContentsObserver(web_contents) {}

  ~WebContentsObserverAdapter() override { Observe(nullptr); }

  bool AwaitManifestUrlChanged(GURL expected_manifest_url) {
    expected_manifest_url_ = expected_manifest_url;
    manifest_run_loop_.Run();
    return manifest_url_updated_;
  }

  void AwaitFaviconUrlsChanged() { favicon_run_loop_.Run(); }

  void AwaitVisibilityHidden() {
    if (web_contents()->GetVisibility() == content::Visibility::HIDDEN) {
      return;
    }
    visibility_run_loop_.Run();
  }

 private:
  void DidUpdateWebManifestURL(content::RenderFrameHost* rfh,
                               const GURL& manifest_url) override {
    if (expected_manifest_url_ == manifest_url) {
      manifest_url_updated_ = true;
      manifest_run_loop_.Quit();
    }
  }

  void DidUpdateFaviconURL(
      content::RenderFrameHost* render_frame_host,
      const std::vector<blink::mojom::FaviconURLPtr>& candidates,
      blink::mojom::FaviconUpdateReason reason) override {
    favicon_run_loop_.Quit();
  }

  void OnVisibilityChanged(content::Visibility visibility) override {
    if (!visibility_run_loop_.running() ||
        visibility != content::Visibility::HIDDEN) {
      return;
    }
    visibility_run_loop_.Quit();
  }

  bool manifest_url_updated_ = false;
  GURL expected_manifest_url_;
  base::RunLoop manifest_run_loop_;

  base::RunLoop favicon_run_loop_;

  base::RunLoop visibility_run_loop_;
};

class MLPromotionBrowserTest : public MLPromotionBrowserTestBase {
 public:
  MLPromotionBrowserTest() {
    task_runner_ = base::MakeRefCounted<base::TestSimpleTaskRunner>();
    scoped_feature_list_.InitWithFeaturesAndParameters(
        /*enabled_features=*/
        {base::test::FeatureRefAndParams(
             webapps::features::kWebAppsEnableMLModelForPromotion,
             {{features::kWebAppsMLGuardrailResultReportProb.name, "1.0"},
              {features::kWebAppsMLModelUserDeclineReportProb.name, "1.0"}}),
         base::test::FeatureRefAndParams(::features::kWebAppInstallDialog, {})},
        /*disabled_features=*/{});
  }
  ~MLPromotionBrowserTest() override = default;

  void SetUpOnMainThread() override {
    MLPromotionBrowserTestBase::SetUpOnMainThread();
    ml_promoter()->SetTaskRunnerForTesting(task_runner_);
    test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();

    // Set the session start time to `features::GetSessionStartGracePeriod()`
    // time ago, to ensure ML install promotion isn't blocked by the grace
    // period.
    SetUserEducationSessionStartTime(
        base::Time::Now() -
        user_education::features::GetSessionStartGracePeriod());
  }

 protected:
  void SetUserEducationSessionStartTime(base::Time time) {
    UserEducationService* edu_service =
        UserEducationServiceFactory::GetForBrowserContext(profile());
    user_education::UserEducationSessionData session_data;
    session_data.start_time = time;
    session_data.most_recent_active_time = base::Time::Now();
    edu_service->user_education_storage_service()
        .set_profile_creation_time_for_testing(time);
    edu_service->user_education_storage_service().SaveSessionData(session_data);
  }

  GURL GetUrlWithFaviconsNoManifest() {
    return https_server()->GetURL("/banners/test_page_with_favicon.html");
  }

  GURL GetInstallableAppURL() {
    return https_server()->GetURL("/banners/manifest_test_page.html");
  }

  GURL GetUrlWithNoManifest() {
    return https_server()->GetURL("/banners/no_manifest_test_page.html");
  }

  GURL GetManifestUrlForNoManifestTestPage() {
    return https_server()->GetURL(
        "/banners/manifest_for_no_manifest_page.json");
  }

  GURL GetUrlWithManifestAllFieldsLoadedForML() {
    return https_server()->GetURL("/banners/test_page_for_ml_promotion.html");
  }

  GURL GetUrlWithNoSWNoFetchHandler() {
    return https_server()->GetURL("/banners/manifest_no_service_worker.html");
  }

  GURL GetUrlWithSWEmptyFetchHandler() {
    return https_server()->GetURL(
        "/banners/manifest_test_page_empty_fetch_handler.html");
  }

  GURL GetUrlWithSwNoFetchHandler() {
    return https_server()->GetURL(
        "/banners/no_sw_fetch_handler_test_page.html");
  }

  GURL GetUrlOuterApp() {
    return https_server()->GetURL("/web_apps/nesting/index.html");
  }

  GURL GetUrlInnerCraftedApp() {
    return https_server()->GetURL("/web_apps/nesting/nested/index.html");
  }

  GURL GetUrlInnerDiyApp() {
    return https_server()->GetURL("/web_apps/nesting/nested/diy.html");
  }

  MLInstallabilityPromoter* ml_promoter() {
    return MLInstallabilityPromoter::FromWebContents(web_contents());
  }

  web_app::WebAppProvider& provider() {
    // TODO(b/287255120) : Block this on Android.
    return *web_app::WebAppProvider::GetForTest(profile());
  }

  const ukm::TestAutoSetUkmRecorder& test_ukm_recorder() {
    return *test_ukm_recorder_;
  }

  // Wait for service workers to be registered and the MLInstallabilityPromoter
  // to finish.
  void NavigateAwaitSWRegistrationPendingDelayedTask(const GURL& url) {
    web_app::ServiceWorkerRegistrationWaiter registration_waiter(profile(),
                                                                 url);
    NavigateAndAwaitInstallabilityCheck(url);
    ml_promoter()->AwaitMetricsCollectionTasksCompleteForTesting();
    registration_waiter.AwaitRegistrationStored();
    ml_promoter()->AwaitMetricsCollectionTasksCompleteForTesting();
  }

  void NavigateUpdateManifestAndAwaitDelayedTaskPending(
      const GURL& site_url,
      const GURL& new_manifest_url) {
    NavigateAndAwaitMetricsCollectionPending(site_url);
    WebContentsObserverAdapter web_contents_observer(web_contents());
    EXPECT_TRUE(content::ExecJs(
        web_contents(),
        "addManifestLinkTag('/banners/manifest_for_no_manifest_page.json')"));
    EXPECT_TRUE(
        web_contents_observer.AwaitManifestUrlChanged(new_manifest_url));
    ml_promoter()->AwaitMetricsCollectionTasksCompleteForTesting();
  }

  void UpdateFaviconAndRunPipelinePendingDelayedTask(const GURL& url) {
    ml_promoter()->AwaitMetricsCollectionTasksCompleteForTesting();
    WebContentsObserverAdapter web_contents_observer(web_contents());
    NavigateAndAwaitInstallabilityCheck(url);
    EXPECT_TRUE(content::ExecJs(web_contents(), "addFavicon('favicon_1.ico')"));
    web_contents_observer.AwaitFaviconUrlsChanged();
    ml_promoter()->AwaitMetricsCollectionTasksCompleteForTesting();
  }

  void NavigateAndAwaitMetricsCollectionPending(const GURL& url) {
    NavigateAndAwaitInstallabilityCheck(url);
    ml_promoter()->AwaitMetricsCollectionTasksCompleteForTesting();
  }

  void ExpectClasificationCallReturnResult(
      GURL site_url,
      webapps::ManifestId manifest_id,
      std::string label_result,
      TrainingRequestId request_result,
      content::WebContents* custom_web_contents = nullptr,
      int times_called = 1) {
    if (!custom_web_contents) {
      custom_web_contents = web_contents();
    }
    base::flat_map<std::string, ProcessedValue> expected_input = {
        {"origin", ProcessedValue(url::Origin::Create(site_url).GetURL())},
        {"site_url", ProcessedValue(site_url)},
        {"manifest_id", ProcessedValue(manifest_id.value())}};
    EXPECT_CALL(*GetMockSegmentation(),
                GetClassificationResult(
                    segmentation_platform::kWebAppInstallationPromoKey, _,
                    Pointee(testing::Field(&InputContext::metadata_args,
                                           testing::Eq(expected_input))),
                    _))
        .Times(testing::Exactly(times_called))
        .WillRepeatedly(base::test::RunOnceCallbackRepeatedly<3>(
            CreateClassificationResult(label_result, request_result)));
  }

  void ExpectTrainingResult(
      TrainingRequestId request,
      MlInstallResponse response,
      content::WebContents* custom_web_contents = nullptr) {
    if (!custom_web_contents) {
      custom_web_contents = web_contents();
    }
    EXPECT_CALL(*GetMockSegmentation(),
                CollectTrainingData(
                    segmentation_platform::proto::SegmentId::
                        OPTIMIZATION_TARGET_WEB_APP_INSTALLATION_PROMO,
                    request,
                    HasTrainingLabel(
                        "WebApps.MlInstall.DialogResponse",
                        static_cast<base::HistogramBase::Sample32>(response)),
                    _));
  }

  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_;
};

// Manifest Data Fetching tests.
IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest, CompletelyFilledManifestUKM) {
  NavigateAndAwaitMetricsCollectionPending(
      GetUrlWithManifestAllFieldsLoadedForML());
  task_runner_->RunPendingTasks();

  auto entries =
      test_ukm_recorder().GetEntriesByName(ManifestUkmEntry::kEntryName);
  ASSERT_EQ(entries.size(), 1u);

  auto* entry = entries[0].get();
  test_ukm_recorder().ExpectEntrySourceHasUrl(
      entry, GetUrlWithManifestAllFieldsLoadedForML());
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kDisplayModeName, /*browser=*/1);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasBackgroundColorName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasNameName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasIconsAnyName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasIconsMaskableName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasScreenshotsName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasThemeColorName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasStartUrlName, /*kValid=*/2);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest, PartiallyFilledManifestUKM) {
  NavigateAndAwaitMetricsCollectionPending(GetInstallableAppURL());
  task_runner_->RunPendingTasks();

  auto entries =
      test_ukm_recorder().GetEntriesByName(ManifestUkmEntry::kEntryName);
  ASSERT_EQ(entries.size(), 1u);

  auto* entry = entries[0].get();
  test_ukm_recorder().ExpectEntrySourceHasUrl(entry, GetInstallableAppURL());
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kDisplayModeName, /*standalone=*/3);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasBackgroundColorName, false);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasNameName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasIconsAnyName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasIconsMaskableName, false);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasScreenshotsName, false);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasThemeColorName, false);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasStartUrlName, /*kValid=*/2);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest, NoManifestUKM) {
  NavigateAndAwaitMetricsCollectionPending(GetUrlWithNoManifest());
  task_runner_->RunPendingTasks();

  auto entries =
      test_ukm_recorder().GetEntriesByName(ManifestUkmEntry::kEntryName);
  ASSERT_EQ(entries.size(), 1u);

  auto* entry = entries[0].get();
  test_ukm_recorder().ExpectEntrySourceHasUrl(entry, GetUrlWithNoManifest());
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kDisplayModeName, -1);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasBackgroundColorName,
      /*NullableBoolean::Null=*/2);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(entry,
                                                 ManifestUkmEntry::kHasNameName,
                                                 /*NullableBoolean::Null=*/2);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasIconsAnyName,
      /*NullableBoolean::Null=*/2);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasIconsMaskableName,
      /*NullableBoolean::Null=*/2);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasScreenshotsName,
      /*NullableBoolean::Null=*/2);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasThemeColorName,
      /*NullableBoolean::Null=*/2);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasStartUrlName, -1);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest, ManifestUpdateChangesUKM) {
  // Run the pipeline with the first update, verify no manifest data is logged
  // to UKMs.
  NavigateAndAwaitMetricsCollectionPending(GetUrlWithNoManifest());
  task_runner_->RunPendingTasks();

  auto entries =
      test_ukm_recorder().GetEntriesByName(ManifestUkmEntry::kEntryName);
  ASSERT_EQ(entries.size(), 1u);

  auto* entry = entries[0].get();

  // Verify UKM records empty manifest data.
  test_ukm_recorder().ExpectEntrySourceHasUrl(entry, GetUrlWithNoManifest());
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kDisplayModeName, -1);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(entry,
                                                 ManifestUkmEntry::kHasNameName,
                                                 /*NullableBoolean::Null=*/2);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, ManifestUkmEntry::kHasStartUrlName, -1);

  // Restart the pipeline by navigating to about::blank and then navigating back
  // to the no manifest page.
  web_app::NavigateViaLinkClickToURLAndWait(browser(),
                                            GURL(url::kAboutBlankURL));

  NavigateUpdateManifestAndAwaitDelayedTaskPending(
      GetUrlWithNoManifest(), GetManifestUrlForNoManifestTestPage());
  task_runner_->RunPendingTasks();

  auto updated_entries =
      test_ukm_recorder().GetEntriesByName(ManifestUkmEntry::kEntryName);
  ASSERT_EQ(updated_entries.size(), 2u);
  auto* updated_entry = updated_entries[1].get();
  test_ukm_recorder().ExpectEntrySourceHasUrl(updated_entry,
                                              GetUrlWithNoManifest());
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      updated_entry, ManifestUkmEntry::kDisplayModeName, /*kStandAlone=*/3);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      updated_entry, ManifestUkmEntry::kHasNameName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      updated_entry, ManifestUkmEntry::kHasStartUrlName, /*kValid=*/2);
}

// SiteInstallMetrics tests.
IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest, FullyInstalledAppMeasurement) {
  NavigateAndAwaitInstallabilityCheck(GetInstallableAppURL());
  EXPECT_TRUE(InstallAppForCurrentWebContents(/*install_locally=*/true));

  NavigateAndAwaitInstallabilityCheck(GetUrlWithNoManifest());

  // A re-navigation should retrigger the ML pipeline.
  NavigateAndAwaitMetricsCollectionPending(GetInstallableAppURL());
  task_runner_->RunPendingTasks();

  auto entries =
      test_ukm_recorder().GetEntriesByName(InstallUkmEntry::kEntryName);
  ASSERT_EQ(entries.size(), 1u);

  auto* entry = entries[0].get();
  test_ukm_recorder().ExpectEntrySourceHasUrl(entry, GetInstallableAppURL());
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, InstallUkmEntry::kIsFullyInstalledName, true);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, InstallUkmEntry::kIsPartiallyInstalledName, false);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest,
                       PartiallyInstalledAppMeasurement) {
  NavigateAndAwaitInstallabilityCheck(GetInstallableAppURL());
  EXPECT_TRUE(InstallAppForCurrentWebContents(/*install_locally=*/false));

  NavigateAndAwaitInstallabilityCheck(GetUrlWithNoManifest());
  // A re-navigation should retrigger the ML pipeline.
  NavigateAndAwaitMetricsCollectionPending(GetInstallableAppURL());
  task_runner_->RunPendingTasks();

  auto entries =
      test_ukm_recorder().GetEntriesByName(InstallUkmEntry::kEntryName);
  ASSERT_EQ(entries.size(), 1u);

  auto* entry = entries[0].get();
  test_ukm_recorder().ExpectEntrySourceHasUrl(entry, GetInstallableAppURL());
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, InstallUkmEntry::kIsFullyInstalledName, false);
  ukm::TestAutoSetUkmRecorder::ExpectEntryMetric(
      entry, InstallUkmEntry::kIsPartiallyInstalledName, true);
}

// SiteQualityMetrics tests.
IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest,
                       SiteQualityMetrics_ServiceWorker_FetchHandler) {
  NavigateAwaitSWRegistrationPendingDelayedTask(GetInstallableAppURL());
  task_runner_->RunPendingTasks();

  auto entries = test_ukm_recorder().GetEntries(
      QualityUkmEntry::kEntryName,
      {QualityUkmEntry::kHasFetchHandlerName,
       QualityUkmEntry::kServiceWorkerScriptSizeName});
  ASSERT_EQ(entries.size(), 1u);

  auto entry = entries[0];
  EXPECT_EQ(test_ukm_recorder().GetSourceForSourceId(entry.source_id)->url(),
            GetInstallableAppURL());
  EXPECT_EQ(entry.metrics[QualityUkmEntry::kHasFetchHandlerName], /*true=*/1);
  EXPECT_GT(entry.metrics[QualityUkmEntry::kServiceWorkerScriptSizeName], 0);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest,
                       SiteQualityMetrics_NoServiceWorker_NoFetchHandler) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;
  NavigateAndAwaitMetricsCollectionPending(GetUrlWithNoSWNoFetchHandler());
  task_runner_->RunPendingTasks();

  auto entries = test_ukm_recorder().GetEntries(
      QualityUkmEntry::kEntryName,
      {QualityUkmEntry::kHasFetchHandlerName,
       QualityUkmEntry::kServiceWorkerScriptSizeName});
  ASSERT_EQ(entries.size(), 1u);

  auto entry = entries[0];
  EXPECT_EQ(test_ukm_recorder().GetSourceForSourceId(entry.source_id)->url(),
            GetUrlWithNoSWNoFetchHandler());
  EXPECT_EQ(entry.metrics[QualityUkmEntry::kHasFetchHandlerName], /*false=*/0);
  // Non-existence of a service worker is shown by a script size of 0.
  EXPECT_EQ(entry.metrics[QualityUkmEntry::kServiceWorkerScriptSizeName], 0);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest,
                       SiteQualityMetrics_ServiceWorker_EmptyFetchHandler) {
  NavigateAwaitSWRegistrationPendingDelayedTask(
      GetUrlWithSWEmptyFetchHandler());
  task_runner_->RunPendingTasks();

  // An empty fetch handler is also treated as an existence of a fetch handler.
  auto entries = test_ukm_recorder().GetEntries(
      QualityUkmEntry::kEntryName,
      {QualityUkmEntry::kHasFetchHandlerName,
       QualityUkmEntry::kServiceWorkerScriptSizeName});
  ASSERT_EQ(entries.size(), 1u);

  auto entry = entries[0];
  EXPECT_EQ(test_ukm_recorder().GetSourceForSourceId(entry.source_id)->url(),
            GetUrlWithSWEmptyFetchHandler());
  EXPECT_EQ(entry.metrics[QualityUkmEntry::kHasFetchHandlerName], /*true=*/1);
  EXPECT_GT(entry.metrics[QualityUkmEntry::kServiceWorkerScriptSizeName], 0);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest,
                       SiteQualityMetrics_ServiceWorker_NoFetchHandler) {
  NavigateAwaitSWRegistrationPendingDelayedTask(GetUrlWithSwNoFetchHandler());
  task_runner_->RunPendingTasks();

  auto entries = test_ukm_recorder().GetEntries(
      QualityUkmEntry::kEntryName,
      {QualityUkmEntry::kHasFetchHandlerName,
       QualityUkmEntry::kServiceWorkerScriptSizeName});
  ASSERT_EQ(entries.size(), 1u);

  auto entry = entries[0];
  EXPECT_EQ(test_ukm_recorder().GetSourceForSourceId(entry.source_id)->url(),
            GetUrlWithSwNoFetchHandler());
  EXPECT_EQ(entry.metrics[QualityUkmEntry::kHasFetchHandlerName], /*false=*/0);
  EXPECT_GT(entry.metrics[QualityUkmEntry::kServiceWorkerScriptSizeName], 0);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest,
                       PageLoadsWithDefaultFaviconName) {
  GURL url_with_default_favicon_name = https_server()->GetURL(
      "/banners/test_page_with_default_favicon_name.html");
  NavigateAndAwaitMetricsCollectionPending(url_with_default_favicon_name);
  task_runner_->RunPendingTasks();

  auto entries = test_ukm_recorder().GetEntries(
      QualityUkmEntry::kEntryName, {QualityUkmEntry::kHasFaviconsName});
  ASSERT_EQ(entries.size(), 1u);

  auto entry = entries[0];
  EXPECT_EQ(test_ukm_recorder().GetSourceForSourceId(entry.source_id)->url(),
            url_with_default_favicon_name);
  EXPECT_EQ(entry.metrics[QualityUkmEntry::kHasFaviconsName], 1);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest, PageLoadsWithExistingFavicon) {
  NavigateAndAwaitMetricsCollectionPending(GetUrlWithFaviconsNoManifest());
  task_runner_->RunPendingTasks();

  auto entries = test_ukm_recorder().GetEntries(
      QualityUkmEntry::kEntryName, {QualityUkmEntry::kHasFaviconsName});
  ASSERT_EQ(entries.size(), 1u);

  auto entry = entries[0];
  EXPECT_EQ(test_ukm_recorder().GetSourceForSourceId(entry.source_id)->url(),
            GetUrlWithFaviconsNoManifest());
  EXPECT_EQ(entry.metrics[QualityUkmEntry::kHasFaviconsName], 1);
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest, PageLoadVerifyFaviconUpdate) {
  NavigateAndAwaitMetricsCollectionPending(GetUrlWithNoManifest());
  task_runner_->RunPendingTasks();

  auto entries = test_ukm_recorder().GetEntries(
      QualityUkmEntry::kEntryName, {QualityUkmEntry::kHasFaviconsName});
  ASSERT_EQ(entries.size(), 1u);

  auto entry = entries[0];
  EXPECT_EQ(test_ukm_recorder().GetSourceForSourceId(entry.source_id)->url(),
            GetUrlWithNoManifest());
  EXPECT_EQ(entry.metrics[QualityUkmEntry::kHasFaviconsName], 0);

  // Navigate to a different page to reset the site URL the pipeline needs to be
  // triggered from.
  web_app::NavigateViaLinkClickToURLAndWait(browser(),
                                            GURL(url::kAboutBlankURL));

  // Add favicons to page after loading and trigger pipeline for testing.
  UpdateFaviconAndRunPipelinePendingDelayedTask(GetUrlWithNoManifest());

  task_runner_->RunPendingTasks();

  auto updated_entries = test_ukm_recorder().GetEntries(
      QualityUkmEntry::kEntryName, {QualityUkmEntry::kHasFaviconsName});
  EXPECT_EQ(updated_entries.size(), 2u);

  auto updated_entry = updated_entries[1];
  EXPECT_EQ(test_ukm_recorder().GetSourceForSourceId(entry.source_id)->url(),
            GetUrlWithNoManifest());
  EXPECT_EQ(updated_entry.metrics[QualityUkmEntry::kHasFaviconsName], 1);
}

// This test is not parameterized because the site has no manifest (hence no
// screenshots), hence only the single bubble view should show up.
IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest,
                       SitePassingGuardrailsNoManifestDoesNotCrash) {
  NavigateAndAwaitMetricsCollectionPending(GetUrlWithFaviconsNoManifest());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlWithFaviconsNoManifest(),
      /*manifest_id=*/webapps::ManifestId(GetUrlWithFaviconsNoManifest()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(1ll));

  // Since the site is not installable, the diy install dialog shows up for
  // universal install.
  std::string dialog_name =
      base::FeatureList::IsEnabled(::features::kWebAppInstallDialog)
          ? "WebAppInstallFlowDialog"
          : "WebAppDiyInstallDialog";
  std::optional<base::AutoReset<web_app::InstallDialogTestResponse>>
      auto_accept;
  std::optional<web_app::WebAppTestInstallWithOsHooksObserver> install_observer;
  std::optional<views::NamedWidgetShownWaiter> waiter;

  if (dialog_name == "WebAppInstallFlowDialog") {
    auto_accept.emplace(web_app::SetPwaInstallationAutoRespondForTesting(
        web_app::InstallDialogTestResponse::kAcceptAndLaunch));
    install_observer.emplace(profile());
    install_observer->BeginListening();
  } else {
    waiter.emplace(views::test::AnyWidgetTestPasskey{}, dialog_name);
  }

  ExpectTrainingResult(TrainingRequestId(1ll), MlInstallResponse::kAccepted);
  task_runner_->RunPendingTasks();

  if (auto_accept) {
    install_observer->Wait();
  } else {
    views::Widget* widget = waiter->WaitIfNeededAndGet();
    views::test::AcceptDialog(widget);
    provider().command_manager().AwaitAllCommandsCompleteForTesting();
  }

  EXPECT_FALSE(provider().registrar_unsafe().is_empty());
  webapps::AppId app_id = provider().registrar_unsafe().GetAppIds()[0];
  EXPECT_EQ("Web App Test Page with Favicon",
            provider().registrar_unsafe().GetAppShortName(app_id));
  auto user_display_mode =
      provider().registrar_unsafe().GetAppUserDisplayMode(app_id);
  EXPECT_TRUE(user_display_mode.has_value());
  EXPECT_THAT(user_display_mode.value(),
              web_app::mojom::UserDisplayMode::kStandalone);
}

// The fact that this test does not crash is proof that the guardrails based
// check works for an empty site (no manifest and no icons).
IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest, MLInstallEmptyPageNoIcons) {
  NavigateAndAwaitMetricsCollectionPending(GetUrlWithNoManifest());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlWithNoManifest(),
      /*manifest_id=*/webapps::ManifestId(GetUrlWithNoManifest()),
      MLInstallabilityPromoter::kShowInstallPromptLabel, TrainingRequestId(1ll),
      web_contents());
  task_runner_->RunPendingTasks();

  ExpectTrainingResult(TrainingRequestId(1ll),
                       MlInstallResponse::kBlockedGuardrails);
  // Doing another navigation should now trigger the guardrail blocked signal.
  web_app::NavigateViaLinkClickToURLAndWait(browser(),
                                            GURL(url::kAboutBlankURL));
}

// Test for crbug.com/40069486, where an already open dialog would cause
// the CHECK in MLInstallOperationTracker::OnMlResultForInstallation
// to fail, since the operation tracker would outlive the results of the
// pipeline.
IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTest,
                       MLPipelineNoCrashForExistingTracker) {
  NavigateAndAwaitMetricsCollectionPending(GetInstallableAppURL());

  // Expect the pipeline to trigger both on the first and second url.
  ExpectClasificationCallReturnResult(
      /*site_url=*/GetInstallableAppURL(),
      /*manifest_id=*/webapps::ManifestId(GetInstallableAppURL()),
      MLInstallabilityPromoter::kShowInstallPromptLabel, TrainingRequestId(1ll),
      web_contents());
  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlOuterApp(),
      /*manifest_id=*/webapps::ManifestId(GetUrlOuterApp()),
      MLInstallabilityPromoter::kShowInstallPromptLabel, TrainingRequestId(2ll),
      web_contents());

  std::string dialog_name =
      base::FeatureList::IsEnabled(::features::kWebAppInstallDialog)
          ? "WebAppInstallFlowDialog"
          : "WebAppSimpleInstallDialog";
  views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                       dialog_name);
  chrome::ExecuteCommand(browser(), IDC_INSTALL_PWA);
  views::Widget* widget = waiter.WaitIfNeededAndGet();
  EXPECT_TRUE(widget != nullptr);

  task_runner_->RunPendingTasks();

  // Refreshing the page should exit the pipeline early, and should not crash.
  web_app::NavigateViaLinkClickToURLAndWait(browser(), GetUrlOuterApp());
  ml_promoter()->AwaitMetricsCollectionTasksCompleteForTesting();
  task_runner_->RunPendingTasks();
}

class MLPromotionBrowserTestNestedPromptBlocking
    : public MLPromotionBrowserTest {
 public:
  MLPromotionBrowserTestNestedPromptBlocking() = default;

  base::test::ScopedFeatureList enable_feature_{
      web_app::kBlockMlPromotionInNestedPagesNoManifest};
};

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTestNestedPromptBlocking,
                       NoPromptForNestedDiy) {
  NavigateAndAwaitMetricsCollectionPending(GetUrlOuterApp());

  // Wait for the full ml pipeline to run. The reporting of the manifest from
  // the AppBannerManager to the VisitedManifestManager should happen by the
  // time this finishes.
  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlOuterApp(),
      /*manifest_id=*/webapps::ManifestId(GetUrlOuterApp()),
      MLInstallabilityPromoter::kDontShowLabel, TrainingRequestId(1ll),
      web_contents());
  task_runner_->RunPendingTasks();
  ExpectTrainingResult(TrainingRequestId(1ll),
                       MlInstallResponse::kReporterDestroyed);

  // Navigate now to the DIY app (no manifest) to ensure that it is blocked.
  NavigateAndAwaitMetricsCollectionPending(GetUrlInnerDiyApp());

  task_runner_->RunPendingTasks();
  base::test::RunUntil(
      [this]() { return ml_promoter()->IsCompleteForTesting(); });
}

IN_PROC_BROWSER_TEST_F(MLPromotionBrowserTestNestedPromptBlocking,
                       PromptForNestedCraftedApp) {
  NavigateAndAwaitMetricsCollectionPending(GetUrlOuterApp());

  // Wait for the full ml pipeline to run. The reporting of the manifest from
  // the AppBannerManager to the VisitedManifestManager should happen by the
  // time this finishes.
  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlOuterApp(),
      /*manifest_id=*/webapps::ManifestId(GetUrlOuterApp()),
      MLInstallabilityPromoter::kDontShowLabel, TrainingRequestId(1ll),
      web_contents());
  task_runner_->RunPendingTasks();
  ExpectTrainingResult(TrainingRequestId(1ll),
                       MlInstallResponse::kReporterDestroyed);

  // Navigate now to the crafted app to ensure that it is not blocked, and test
  // installation.
  NavigateAndAwaitMetricsCollectionPending(GetUrlInnerCraftedApp());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlInnerCraftedApp(),
      /*manifest_id=*/webapps::ManifestId(GetUrlInnerCraftedApp()),
      MLInstallabilityPromoter::kShowInstallPromptLabel, TrainingRequestId(2ll),
      web_contents());

  base::AutoReset<web_app::InstallDialogTestResponse> auto_accept =
      web_app::SetPwaInstallationAutoRespondForTesting(
          web_app::InstallDialogTestResponse::kAcceptAndLaunch);
  ExpectTrainingResult(TrainingRequestId(2ll), MlInstallResponse::kAccepted);
  web_app::WebAppTestInstallWithOsHooksObserver install_observer(profile());
  install_observer.BeginListening();
  task_runner_->RunPendingTasks();
  install_observer.Wait();

  ASSERT_FALSE(provider().registrar_unsafe().is_empty());
  webapps::AppId app_id = provider().registrar_unsafe().GetAppIds()[0];
  EXPECT_EQ(GetUrlInnerCraftedApp(),
            provider().registrar_unsafe().GetAppStartUrl(app_id));
}

// TODO(b/285361272): Add tests for cache storage sizes.
// TODO(b/329696741): Update DIY App dialog information also here.
class MLPromotionInstallDialogBrowserTest
    : public MLPromotionBrowserTest,
      public ::testing::WithParamInterface<InstallDialogState> {
 public:
  MLPromotionInstallDialogBrowserTest() = default;
  ~MLPromotionInstallDialogBrowserTest() = default;

 protected:
  const std::string GetDialogName() {
    if (base::FeatureList::IsEnabled(::features::kWebAppInstallDialog)) {
      return "WebAppInstallFlowDialog";
    }
    switch (GetParam()) {
      case InstallDialogState::kSimpleInstallDialog:
        return "WebAppSimpleInstallDialog";
      case InstallDialogState::kDetailedInstallDialog:
        return "WebAppDetailedInstallDialog";
      case InstallDialogState::kCreateShortcutDialog:
        return "CreateShortcutConfirmationView";
    }
  }

  const GURL GetUrlBasedOnDialogState() {
    switch (GetParam()) {
      case InstallDialogState::kSimpleInstallDialog:
        return GetInstallableAppURL();
      case InstallDialogState::kDetailedInstallDialog:
        return https_server()->GetURL(
            "/banners/manifest_test_page_screenshots.html");
      case InstallDialogState::kCreateShortcutDialog:
        return GetUrlWithNoManifest();
    }
  }

  // These names are obtained from the manifests in chrome/test/data/banners/
  const std::string GetAppNameBasedOnDialogState() {
    switch (GetParam()) {
      case InstallDialogState::kSimpleInstallDialog:
        return "Manifest test app";
      case InstallDialogState::kDetailedInstallDialog:
        return "PWA Bottom Sheet";
      case InstallDialogState::kCreateShortcutDialog:
        NOTREACHED();
    }
  }

  void InstallAppBasedOnDialogState() {
    switch (GetParam()) {
      case InstallDialogState::kSimpleInstallDialog:
      case InstallDialogState::kDetailedInstallDialog:
        InstallAppForCurrentWebContents(/*install_locally=*/true);
        break;
      case InstallDialogState::kCreateShortcutDialog:
        auto_accept_ = web_app::SetPwaInstallationAutoRespondForTesting(
            web_app::InstallDialogTestResponse::kAcceptAndLaunch);
        chrome::ExecuteCommand(browser(), IDC_CREATE_SHORTCUT);
        break;
    }
  }

  bool IsCurrentTestStateShortcutDialog() {
    return GetParam() == InstallDialogState::kCreateShortcutDialog;
  }

  std::optional<base::AutoReset<web_app::InstallDialogTestResponse>>
      auto_accept_;
};

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest, MlInstallNotShown) {
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      "DontShow", TrainingRequestId(1ll));

  // This calls unblocks the metrics tasks, allowing ML to be called.
  task_runner_->RunPendingTasks();

  provider().command_manager().AwaitAllCommandsCompleteForTesting();

  EXPECT_TRUE(provider().registrar_unsafe().is_empty());
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlInstallShownCancelled) {
  if (IsCurrentTestStateShortcutDialog()) {
    GTEST_SKIP()
        << "Skipping because ML cannot trigger the Create Shortcut Dialog.";
  }
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(1ll));

  views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                       GetDialogName());
  // This calls unblocks the metrics tasks, allowing ML to be called.
  task_runner_->RunPendingTasks();
  views::Widget* widget = waiter.WaitIfNeededAndGet();

  ExpectTrainingResult(TrainingRequestId(1ll), MlInstallResponse::kCancelled);

  views::test::CancelDialog(widget);

  provider().command_manager().AwaitAllCommandsCompleteForTesting();

  EXPECT_TRUE(provider().registrar_unsafe().is_empty());
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlInstallShownIgnoredNavigation) {
  if (IsCurrentTestStateShortcutDialog()) {
    GTEST_SKIP()
        << "Skipping because ML cannot trigger the Create Shortcut Dialog.";
  }
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(1ll));

  views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                       GetDialogName());
  // This calls unblocks the metrics tasks, allowing ML to be called.
  task_runner_->RunPendingTasks();
  views::Widget* widget = waiter.WaitIfNeededAndGet();

  ExpectTrainingResult(TrainingRequestId(1ll), MlInstallResponse::kIgnored);

  views::test::WidgetDestroyedWaiter destroyed(widget);
  web_app::NavigateViaLinkClickToURLAndWait(browser(),
                                            GURL(url::kAboutBlankURL));
  destroyed.Wait();

  provider().command_manager().AwaitAllCommandsCompleteForTesting();

  EXPECT_TRUE(provider().registrar_unsafe().is_empty());
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlInstallShownIgnoredWidgetClosed) {
  if (IsCurrentTestStateShortcutDialog()) {
    GTEST_SKIP()
        << "Skipping because ML cannot trigger the Create Shortcut Dialog.";
  }
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(1ll));

  views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                       GetDialogName());
  // This calls unblocks the metrics tasks, allowing ML to be called.
  task_runner_->RunPendingTasks();
  views::Widget* widget = waiter.WaitIfNeededAndGet();
  views::test::WidgetDestroyedWaiter destroyed(widget);
  ExpectTrainingResult(TrainingRequestId(1ll), MlInstallResponse::kIgnored);
  widget->Close();
  destroyed.Wait();

  provider().command_manager().AwaitAllCommandsCompleteForTesting();

  EXPECT_TRUE(provider().registrar_unsafe().is_empty());
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlInstallShownAccepted) {
  if (IsCurrentTestStateShortcutDialog()) {
    GTEST_SKIP()
        << "Skipping because ML cannot trigger the Create Shortcut Dialog.";
  }
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(1ll));

  base::AutoReset<web_app::InstallDialogTestResponse> auto_accept =
      web_app::SetPwaInstallationAutoRespondForTesting(
          web_app::InstallDialogTestResponse::kAcceptAndLaunch);
  ExpectTrainingResult(TrainingRequestId(1ll), MlInstallResponse::kAccepted);
  web_app::WebAppTestInstallWithOsHooksObserver install_observer(profile());
  install_observer.BeginListening();
  // This calls unblocks the metrics tasks, allowing ML to be called.
  task_runner_->RunPendingTasks();
  install_observer.Wait();

  EXPECT_FALSE(provider().registrar_unsafe().is_empty());
  webapps::AppId app_id = provider().registrar_unsafe().GetAppIds()[0];
  EXPECT_EQ(GetAppNameBasedOnDialogState(),
            provider().registrar_unsafe().GetAppShortName(app_id));
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlNotShownAlreadyInstalled) {
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());
  InstallAppBasedOnDialogState();

  int segmentation_trigger_time =
      (GetParam() == InstallDialogState::kCreateShortcutDialog);
  // ML Model is still triggered for shortcuts which are treated separately as
  // PWAs.
  EXPECT_CALL(*GetMockSegmentation(), GetClassificationResult(_, _, _, _))
      .Times(segmentation_trigger_time);

  // This calls unblocks the metrics tasks, allowing ML to be called. It should
  // not, though, as the app is installed.
  task_runner_->RunPendingTasks();

  provider().command_manager().AwaitAllCommandsCompleteForTesting();
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlHandlesInvisible) {
  if (IsCurrentTestStateShortcutDialog()) {
    GTEST_SKIP()
        << "Skipping because ML cannot trigger the Create Shortcut Dialog.";
  }
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  MLInstallabilityPromoter* original_tab_promoter = ml_promoter();
  content::WebContents* original_web_contents = web_contents();

  // Creating a new tab should ensure that visibility changes.
  WebContentsObserverAdapter hidden_waiter(original_web_contents);
  chrome::NewTab(browser(), NewTabTypes::kNoUserAction);
  hidden_waiter.AwaitVisibilityHidden();

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel, TrainingRequestId(1ll),
      original_web_contents);

  // This calls unblocks the metrics tasks, allowing ML to be called. However,
  // because the web contents isn't visible, the results won't be reported yet.
  task_runner_->RunPendingTasks();
  EXPECT_TRUE(original_tab_promoter->IsPendingVisibilityForTesting());
  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  EXPECT_TRUE(provider().registrar_unsafe().is_empty());

  // Navigating to the previous tab will resume the installation UX reporting,
  // so handle installation request.
  base::AutoReset<web_app::InstallDialogTestResponse> auto_accept =
      web_app::SetPwaInstallationAutoRespondForTesting(
          web_app::InstallDialogTestResponse::kAcceptAndLaunch);
  web_app::WebAppTestInstallWithOsHooksObserver install_observer(profile());
  install_observer.BeginListening();
  ExpectTrainingResult(TrainingRequestId(1ll), MlInstallResponse::kAccepted,
                       original_web_contents);
  chrome::SelectPreviousTab(browser());
  install_observer.Wait();

  EXPECT_FALSE(provider().registrar_unsafe().is_empty());
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlInstallBlockedIphGracePeriod) {
  if (IsCurrentTestStateShortcutDialog()) {
    GTEST_SKIP()
        << "Skipping because ML cannot trigger the Create Shortcut Dialog.";
  }
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(1ll));

  // Setting the start time to now should apply the grace period, blocking ml
  // install promotion via guardrail.
  SetUserEducationSessionStartTime(base::Time::Now());

  // This calls unblocks the metrics tasks, allowing the pipeline to continue
  // and check guardrails.
  task_runner_->RunPendingTasks();

  // Ensure that nothing is installed.
  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  EXPECT_TRUE(provider().registrar_unsafe().is_empty());

  ExpectTrainingResult(TrainingRequestId(1ll),
                       MlInstallResponse::kBlockedGuardrails, web_contents());
  // Doing another navigation should now trigger the guardrail blocked signal.
  web_app::NavigateViaLinkClickToURLAndWait(browser(),
                                            GURL(url::kAboutBlankURL));
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlInstallGuardrailBlocked) {
  if (IsCurrentTestStateShortcutDialog()) {
    GTEST_SKIP()
        << "Skipping because ML cannot trigger the Create Shortcut Dialog.";
  }
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(1ll));

  views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                       GetDialogName());
  // This calls unblocks the metrics tasks, allowing ML to be called.
  task_runner_->RunPendingTasks();

  // Cancelling the dialog will save that result in the guardrails, which should
  // cause the next immediate install call to trigger the guardrail response.
  views::Widget* widget = waiter.WaitIfNeededAndGet();
  ExpectTrainingResult(TrainingRequestId(1ll), MlInstallResponse::kCancelled);
  views::test::WidgetDestroyedWaiter destroyed(widget);
  views::test::CancelDialog(widget);
  destroyed.Wait();

  // Ensure that nothing is installed.
  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  EXPECT_TRUE(provider().registrar_unsafe().is_empty());

  web_app::NavigateViaLinkClickToURLAndWait(browser(),
                                            GURL(url::kAboutBlankURL));

  // Test that guardrails now block the install.
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(2ll));
  // This will cause the ML pipeline to complete, but not report anything yet.
  // This allows the user install to possibly still happen and report success.
  task_runner_->RunPendingTasks();

  ExpectTrainingResult(TrainingRequestId(2ll),
                       MlInstallResponse::kBlockedGuardrails, web_contents());
  // Doing another navigation should now trigger the guardrail blocked signal.
  web_app::NavigateViaLinkClickToURLAndWait(browser(),
                                            GURL(url::kAboutBlankURL));
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlInstallGuardrailIgnoredUserInstallAccepted) {
  if (IsCurrentTestStateShortcutDialog()) {
    GTEST_SKIP()
        << "Skipping because ML cannot trigger the Create Shortcut Dialog.";
  }
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(1ll));

  // Cancelling the dialog will save that result in the guardrails, which
  // should cause the next immediate install call to trigger the guardrail
  // response. This is not triggered for the create shortcut dialog since that
  // flow is not shown here.
  {
    views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                         GetDialogName());
    // This calls unblocks the metrics tasks, allowing ML to be called.
    task_runner_->RunPendingTasks();

    views::Widget* widget = waiter.WaitIfNeededAndGet();
    ExpectTrainingResult(TrainingRequestId(1ll), MlInstallResponse::kCancelled);
    views::test::WidgetDestroyedWaiter destroyed(widget);
    views::test::CancelDialog(widget);
    destroyed.Wait();
  }
  // Ensure that nothing is installed.
  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  EXPECT_TRUE(provider().registrar_unsafe().is_empty());

  web_app::NavigateViaLinkClickToURLAndWait(browser(),
                                            GURL(url::kAboutBlankURL));

  // Navigate back to the app url to re-trigger the ml pipeline.
  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(2ll));
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());
  task_runner_->RunPendingTasks();

  // Test that the guardrail isn't reported when the user completes the install,
  // and instead reports success.
  ExpectTrainingResult(TrainingRequestId(2ll), MlInstallResponse::kAccepted);
  EXPECT_TRUE(
      InstallAppFromUserInitiation(/*accept_install=*/true, GetDialogName()));
}

IN_PROC_BROWSER_TEST_P(MLPromotionInstallDialogBrowserTest,
                       MlInstallGuardrailIgnoredUserInstallCancelled) {
  if (IsCurrentTestStateShortcutDialog()) {
    GTEST_SKIP()
        << "Skipping because ML cannot trigger the Create Shortcut Dialog.";
  }
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());

  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId((GetUrlBasedOnDialogState())),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(1ll));

  // Cancelling the dialog will save that result in the guardrails, which
  // should cause the next immediate install call to trigger the guardrail
  // response.
  {
    views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                         GetDialogName());
    // This calls unblocks the metrics tasks, allowing ML to be called.
    task_runner_->RunPendingTasks();

    views::Widget* widget = waiter.WaitIfNeededAndGet();
    ExpectTrainingResult(TrainingRequestId(1ll), MlInstallResponse::kCancelled);
    views::test::WidgetDestroyedWaiter destroyed(widget);
    views::test::CancelDialog(widget);
    destroyed.Wait();
  }
  // Ensure that nothing is installed.
  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  EXPECT_TRUE(provider().registrar_unsafe().is_empty());

  web_app::NavigateViaLinkClickToURLAndWait(browser(),
                                            GURL(url::kAboutBlankURL));

  // Navigate back to the app url to re-trigger the ml pipeline.
  ExpectClasificationCallReturnResult(
      /*site_url=*/GetUrlBasedOnDialogState(),
      /*manifest_id=*/webapps::ManifestId(GetUrlBasedOnDialogState()),
      MLInstallabilityPromoter::kShowInstallPromptLabel,
      TrainingRequestId(2ll));
  NavigateAndAwaitMetricsCollectionPending(GetUrlBasedOnDialogState());
  task_runner_->RunPendingTasks();

  // Test that the guardrail isn't reported when the user completes the install,
  // and instead reports success.
  ExpectTrainingResult(TrainingRequestId(2ll), MlInstallResponse::kCancelled);
  EXPECT_TRUE(
      InstallAppFromUserInitiation(/*accept_install=*/false, GetDialogName()));
}

INSTANTIATE_TEST_SUITE_P(
    All,
    MLPromotionInstallDialogBrowserTest,
    ::testing::Values(InstallDialogState::kSimpleInstallDialog,
                      InstallDialogState::kDetailedInstallDialog,
                      InstallDialogState::kCreateShortcutDialog),
    GetMLPromotionDialogTestName);

}  // namespace
}  // namespace webapps
