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

#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/navigator/browser_navigator.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.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_launch_utils.h"
#include "chrome/browser/web_applications/manifest_update_manager.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/test/web_app_install_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_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/embedder_support/switches.h"
#include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_interceptor.h"
#include "third_party/blink/public/common/features.h"
#include "ui/base/page_transition_types.h"

namespace web_app {

namespace {

constexpr char kLaunchHandlerHistogram[] =
    "Launch.WebAppLaunchHandlerClientMode";

}  // namespace

using ClientMode = LaunchHandler::ClientMode;

class WebAppLaunchHandlerBrowserTest : public WebAppBrowserTestBase {
 public:
  WebAppLaunchHandlerBrowserTest() = default;
  ~WebAppLaunchHandlerBrowserTest() override = default;

  // WebAppBrowserTestBase:
  void SetUpOnMainThread() override {
    WebAppBrowserTestBase::SetUpOnMainThread();
    ASSERT_TRUE(embedded_test_server()->Start());
    web_app::test::WaitUntilReady(
        web_app::WebAppProvider::GetForTest(profile()));
  }

 protected:
  Profile* profile() { return browser()->GetProfile(); }

  webapps::AppId InstallTestWebApp(const char* test_file_path,
                                   bool await_metric = true) {
    page_load_metrics::PageLoadMetricsTestWaiter metrics_waiter(
        browser()->GetTabStripModel()->GetActiveWebContents());
    if (await_metric) {
      metrics_waiter.AddWebFeatureExpectation(
          blink::mojom::WebFeature::kWebAppManifestLaunchHandler);
    }

    webapps::AppId app_id = InstallWebAppFromPage(
        browser(), embedded_test_server()->GetURL(test_file_path));

    if (await_metric) {
      metrics_waiter.Wait();
    }

    return app_id;
  }

  const WebApp* GetWebApp(const webapps::AppId& app_id) {
    return WebAppProvider::GetForTest(profile())->registrar_unsafe().GetAppById(
        app_id);
  }

  std::optional<LaunchHandler> GetLaunchHandler(const webapps::AppId& app_id) {
    return GetWebApp(app_id)->launch_handler();
  }

  void ExpectNavigateNewBehavior(const webapps::AppId& app_id) {
    std::string start_url = GetWebApp(app_id)->start_url().spec();

    BrowserWindowInterface* browser_1 = LaunchWebAppBrowserAndWait(app_id);
    EXPECT_EQ(AwaitNextLaunchParamsTargetUrl(browser_1), start_url);

    BrowserWindowInterface* browser_2 = LaunchWebAppBrowserAndWait(app_id);
    EXPECT_EQ(AwaitNextLaunchParamsTargetUrl(browser_2), start_url);

    EXPECT_NE(browser_1, browser_2);
  }

  std::string AwaitNextLaunchParamsTargetUrl(BrowserWindowInterface* browser) {
    const char* script = R"(
      new Promise(resolve => {
        window.launchQueue.setConsumer(resolve);
      }).then(params => params.targetURL);
    )";
    return EvalJs(browser->GetTabStripModel()->GetActiveWebContents(), script)
        .ExtractString();
  }

  bool SetUpNextLaunchParamsTargetUrlPromise(BrowserWindowInterface* browser) {
    const char* script = R"(
        window.nextLaunchParamsTargetURLPromise = new Promise(resolve => {
          window.launchQueue.setConsumer(resolve);
        }).then(params => params.targetURL);
        true;
      )";
    return EvalJs(browser->GetTabStripModel()->GetActiveWebContents(), script)
        .ExtractBool();
  }

  std::string AwaitNextLaunchParamsTargetUrlPromise(
      BrowserWindowInterface* browser) {
    return EvalJs(browser->GetTabStripModel()->GetActiveWebContents(),
                  "window.nextLaunchParamsTargetURLPromise")
        .ExtractString();
  }
};

IN_PROC_BROWSER_TEST_F(WebAppLaunchHandlerBrowserTest, ClientModeEmpty) {
  base::HistogramTester histogram_tester;
  webapps::AppId app_id =
      InstallTestWebApp("/web_apps/basic.html", /*await_metric=*/false);
  EXPECT_EQ(GetLaunchHandler(app_id), std::nullopt);

  ExpectNavigateNewBehavior(app_id);

  histogram_tester.ExpectUniqueSample(kLaunchHandlerHistogram,
                                      ClientMode::kAuto, 2);
}

IN_PROC_BROWSER_TEST_F(WebAppLaunchHandlerBrowserTest, ClientModeAuto) {
  base::HistogramTester histogram_tester;
  webapps::AppId app_id = InstallTestWebApp(
      "/web_apps/get_manifest.html?launch_handler_client_mode_auto.json");
  auto launch_handler = GetLaunchHandler(app_id);
  EXPECT_EQ(ClientMode::kAuto, launch_handler->parsed_client_mode());
  EXPECT_TRUE(launch_handler->client_mode_valid_and_specified());

  ExpectNavigateNewBehavior(app_id);

  histogram_tester.ExpectUniqueSample(kLaunchHandlerHistogram,
                                      ClientMode::kAuto, 2);
}

IN_PROC_BROWSER_TEST_F(WebAppLaunchHandlerBrowserTest, ClientModeNavigateNew) {
  base::HistogramTester histogram_tester;
  webapps::AppId app_id = InstallTestWebApp(
      "/web_apps/get_manifest.html?"
      "launch_handler_client_mode_navigate_new.json");
  auto launch_handler = GetLaunchHandler(app_id);
  EXPECT_EQ(ClientMode::kNavigateNew, launch_handler->parsed_client_mode());
  EXPECT_TRUE(launch_handler->client_mode_valid_and_specified());

  ExpectNavigateNewBehavior(app_id);

  histogram_tester.ExpectUniqueSample(kLaunchHandlerHistogram,
                                      ClientMode::kNavigateNew, 2);
}

IN_PROC_BROWSER_TEST_F(WebAppLaunchHandlerBrowserTest,
                       ClientModeNavigateExisting) {
  webapps::AppId app_id = InstallTestWebApp(
      "/web_apps/get_manifest.html?"
      "launch_handler_client_mode_navigate_existing.json");

  GURL start_url = embedded_test_server()->GetURL(
      "/web_apps/basic.html?"
      "client_mode=navigate-existing");

  base::HistogramTester histogram_tester;
  auto launch_handler = GetLaunchHandler(app_id);
  EXPECT_EQ(ClientMode::kNavigateExisting,
            launch_handler->parsed_client_mode());
  EXPECT_TRUE(launch_handler->client_mode_valid_and_specified());

  // Create first web app browser window.
  BrowserWindowInterface* app_browser = LaunchWebAppBrowserAndWait(app_id);
  content::WebContents* app_web_contents =
      app_browser->GetTabStripModel()->GetActiveWebContents();
  EXPECT_EQ(app_web_contents->GetLastCommittedURL(), start_url);
  EXPECT_EQ(AwaitNextLaunchParamsTargetUrl(app_browser), start_url.spec());

  // Navigate window away from start_url to check that the next launch navs to
  // start_url again.
  {
    GURL alt_url = embedded_test_server()->GetURL("/web_apps/basic.html");
    EXPECT_TRUE(ui_test_utils::NavigateToURL(app_browser, alt_url));
    EXPECT_EQ(app_web_contents->GetLastCommittedURL(), alt_url);

    BrowserWindowInterface* app_browser_2 = LaunchWebAppBrowserAndWait(app_id);
    EXPECT_EQ(app_browser, app_browser_2);
    EXPECT_EQ(app_web_contents->GetLastCommittedURL(), start_url);
    EXPECT_EQ(AwaitNextLaunchParamsTargetUrl(app_browser_2), start_url.spec());
  }

  // Reparent an in scope browser tab and check that it navigates the existing
  // web app window.
  {
    content::TestNavigationObserver observer(
        app_web_contents, content::MessageLoopRunner::QuitMode::DEFERRED);

    chrome::NewTab(browser(), NewTabTypes::kNoUserAction);
    EXPECT_EQ(browser()->GetTabStripModel()->count(), 2);
    EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), start_url));
    ReparentWebAppForActiveTab(browser());
    EXPECT_EQ(browser()->GetTabStripModel()->count(), 1);

    observer.WaitForNavigationFinished();
    EXPECT_EQ(AwaitNextLaunchParamsTargetUrl(app_browser), start_url.spec());
  }

  histogram_tester.ExpectUniqueSample(kLaunchHandlerHistogram,
                                      ClientMode::kNavigateExisting, 3);
}

// TODO(crbug.com/40219080): Fix flakiness.
IN_PROC_BROWSER_TEST_F(WebAppLaunchHandlerBrowserTest,
                       DISABLED_ClientModeExistingClientRetain) {
  webapps::AppId app_id = InstallTestWebApp(
      "/web_apps/get_manifest.html?"
      "launch_handler_client_mode_focus_existing.json");

  GURL start_url = embedded_test_server()->GetURL(
      "/web_apps/basic.html?"
      "client_mode=focus-existing");

  base::HistogramTester histogram_tester;
  auto launch_handler = GetLaunchHandler(app_id);
  EXPECT_EQ(ClientMode::kFocusExisting, launch_handler->parsed_client_mode());
  EXPECT_TRUE(launch_handler->client_mode_valid_and_specified());

  BrowserWindowInterface* browser_1 = LaunchWebAppBrowserAndWait(app_id);
  content::WebContents* web_contents =
      browser_1->GetTabStripModel()->GetActiveWebContents();
  EXPECT_EQ(web_contents->GetLastCommittedURL(), start_url);
  EXPECT_EQ(AwaitNextLaunchParamsTargetUrl(browser_1), start_url.spec());

  // Navigate window away from start_url to an in scope URL, check that the
  // next launch doesn't navigate to start_url.
  {
    GURL in_scope_url = embedded_test_server()->GetURL("/web_apps/basic.html");
    NavigateViaLinkClickToURLAndWait(browser_1, in_scope_url);
    EXPECT_EQ(web_contents->GetLastCommittedURL(), in_scope_url);

    ASSERT_TRUE(SetUpNextLaunchParamsTargetUrlPromise(browser_1));
    BrowserWindowInterface* browser_2 = LaunchWebAppBrowserAndWait(app_id);
    EXPECT_EQ(browser_1, browser_2);
    EXPECT_EQ(AwaitNextLaunchParamsTargetUrlPromise(browser_2),
              start_url.spec());
    EXPECT_EQ(web_contents->GetLastCommittedURL(), in_scope_url);
  }

  // Navigate window away from start_url to an out of scope URL, check that
  // the next launch does navigate to start_url.
  {
    GURL out_of_scope_url = embedded_test_server()->GetURL("/empty.html");
    NavigateViaLinkClickToURLAndWait(browser_1, out_of_scope_url);
    EXPECT_EQ(web_contents->GetLastCommittedURL(), out_of_scope_url);

    BrowserWindowInterface* browser_2 = LaunchWebAppBrowserAndWait(app_id);
    EXPECT_EQ(browser_1, browser_2);
    EXPECT_EQ(AwaitNextLaunchParamsTargetUrl(browser_2), start_url.spec());
    EXPECT_EQ(web_contents->GetLastCommittedURL(), start_url);
  }

  // Trigger launch during navigation, check that the navigation gets
  // cancelled.
  {
    ASSERT_TRUE(
        EvalJs(web_contents, "window.thisIsTheSamePage = true").ExtractBool());

    GURL hanging_url = embedded_test_server()->GetURL("/hang");
    NavigateParams params(browser_1, hanging_url, ui::PAGE_TRANSITION_LINK);
    Navigate(&params);
    EXPECT_EQ(web_contents->GetVisibleURL(), hanging_url);
    EXPECT_EQ(web_contents->GetLastCommittedURL(), start_url);

    ASSERT_TRUE(SetUpNextLaunchParamsTargetUrlPromise(browser_1));
    BrowserWindowInterface* browser_2 = LaunchWebAppBrowserAndWait(app_id);
    EXPECT_EQ(browser_1, browser_2);
    EXPECT_EQ(AwaitNextLaunchParamsTargetUrlPromise(browser_2),
              start_url.spec());
    EXPECT_EQ(web_contents->GetVisibleURL(), start_url);
    EXPECT_EQ(web_contents->GetLastCommittedURL(), start_url);

    // Check that we never left the current page.
    EXPECT_TRUE(EvalJs(web_contents, "window.thisIsTheSamePage").ExtractBool());
  }

  histogram_tester.ExpectUniqueSample(kLaunchHandlerHistogram,
                                      ClientMode::kFocusExisting, 4);
}

IN_PROC_BROWSER_TEST_F(WebAppLaunchHandlerBrowserTest,
                       ClientModeFocusExistingMultipleLaunches) {
  base::HistogramTester histogram_tester;
  webapps::AppId app_id = InstallTestWebApp(
      "/web_apps/get_manifest.html?"
      "launch_handler_client_mode_focus_existing.json");
  auto launch_handler = GetLaunchHandler(app_id);
  EXPECT_EQ(ClientMode::kFocusExisting, launch_handler->parsed_client_mode());
  EXPECT_TRUE(launch_handler->client_mode_valid_and_specified());

  ui_test_utils::UrlLoadObserver url_observer(
      WebAppProvider::GetForTest(profile())->registrar_unsafe().GetAppLaunchUrl(
          app_id));

  // Launch the app three times in quick succession.
  BrowserWindowInterface* browser_1 = LaunchWebAppBrowser(app_id);
  BrowserWindowInterface* browser_2 = LaunchWebAppBrowser(app_id);
  BrowserWindowInterface* browser_3 = LaunchWebAppBrowser(app_id);
  EXPECT_EQ(browser_1, browser_2);
  EXPECT_EQ(browser_2, browser_3);

  url_observer.Wait();
  // Wait for all launches to complete.
  WebAppProvider::GetForTest(profile())
      ->command_manager()
      .AwaitAllCommandsCompleteForTesting();

  // Check that all 3 LaunchParams got enqueued.
  content::WebContents* web_contents =
      browser_1->GetTabStripModel()->GetActiveWebContents();
  GURL start_url = embedded_test_server()->GetURL(
      "/web_apps/basic.html?client_mode=focus-existing");
  const char* script = R"(
      new Promise(resolve => {
        let remaining = 3;
        let targetURLs = [];
        window.launchQueue.setConsumer(launchParams => {
          targetURLs.push(launchParams.targetURL);
          if (--remaining == 0) {
            resolve(targetURLs.join('|'));
          }
        });
      });
    )";
  EXPECT_EQ(EvalJs(web_contents, script).ExtractString(),
            base::StrCat({start_url.spec(), "|", start_url.spec(), "|",
                          start_url.spec()}));

  histogram_tester.ExpectUniqueSample(kLaunchHandlerHistogram,
                                      ClientMode::kFocusExisting, 3);
}

IN_PROC_BROWSER_TEST_F(WebAppLaunchHandlerBrowserTest,
                       ClientModeNavigateExistingMultipleLaunches) {
  base::HistogramTester histogram_tester;
  webapps::AppId app_id = InstallTestWebApp(
      "/web_apps/get_manifest.html?"
      "launch_handler_client_mode_navigate_existing.json");
  auto launch_handler = GetLaunchHandler(app_id);
  EXPECT_EQ(ClientMode::kNavigateExisting,
            launch_handler->parsed_client_mode());
  EXPECT_TRUE(launch_handler->client_mode_valid_and_specified());

  // Launch the app three times in quick succession.
  BrowserWindowInterface* browser_1 = LaunchWebAppBrowserAndWait(app_id);
  BrowserWindowInterface* browser_2 = LaunchWebAppBrowserAndWait(app_id);
  BrowserWindowInterface* browser_3 = LaunchWebAppBrowserAndWait(app_id);
  EXPECT_EQ(browser_1, browser_2);
  EXPECT_EQ(browser_2, browser_3);

  // Check that only the last LaunchParams made it through.
  content::WebContents* web_contents =
      browser_1->GetTabStripModel()->GetActiveWebContents();
  GURL start_url = embedded_test_server()->GetURL(
      "/web_apps/basic.html?client_mode=navigate-existing");
  const char* script = R"(
      new Promise(resolve => {
        let targetURLs = [];
        window.launchQueue.setConsumer(launchParams => {
          targetURLs.push(launchParams.targetURL);
          // Wait a tick to let any additional erroneous launchParams get added.
          requestAnimationFrame(() => {
            resolve(targetURLs.join('|'));
          });
        });
      });
    )";
  EXPECT_EQ(EvalJs(web_contents, script).ExtractString(), start_url.spec());

  histogram_tester.ExpectUniqueSample(kLaunchHandlerHistogram,
                                      ClientMode::kNavigateExisting, 3);
}

IN_PROC_BROWSER_TEST_F(WebAppLaunchHandlerBrowserTest,
                       LaunchNavigationInterruptedByOutOfScopeNavigation) {
  base::HistogramTester histogram_tester;
  webapps::AppId app_id = InstallTestWebApp(
      "/web_apps/get_manifest.html?"
      "launch_handler_client_mode_navigate_new.json");
  auto launch_handler = GetLaunchHandler(app_id);
  EXPECT_EQ(ClientMode::kNavigateNew, launch_handler->parsed_client_mode());
  EXPECT_TRUE(launch_handler->client_mode_valid_and_specified());

  // Launch the web app and immediately navigate it out of scope during its
  // initial navigation.
  BrowserWindowInterface* app_browser = LaunchWebAppBrowserAndWait(app_id);
  GURL out_of_scope_url = embedded_test_server()->GetURL("/empty.html");
  NavigateViaLinkClickToURLAndWait(app_browser, out_of_scope_url);
  content::WebContents* web_contents =
      app_browser->GetTabStripModel()->GetActiveWebContents();
  EXPECT_EQ(web_contents->GetLastCommittedURL(), out_of_scope_url);

  // Check that the launch params are not enqueued in the out of scope document.
  const char* script = R"(
      new Promise(resolve => {
        let targetURLs = [];
        window.launchQueue.setConsumer(launchParams => {
          targetURLs.push(launchParams.targetURL);
        });
        // Wait a tick to let any erroneous launch params get added.
        requestAnimationFrame(() => {
          resolve(targetURLs.join('|'));
        });
      });
    )";
  EXPECT_EQ(EvalJs(web_contents, script).ExtractString(), "");

  histogram_tester.ExpectUniqueSample(kLaunchHandlerHistogram,
                                      ClientMode::kNavigateNew, 1);
}

IN_PROC_BROWSER_TEST_F(WebAppLaunchHandlerBrowserTest, GlobalLaunchQueue) {
  base::HistogramTester histogram_tester;
  webapps::AppId app_id =
      InstallTestWebApp("/web_apps/basic.html", /*await_metric=*/false);

  BrowserWindowInterface* app_browser = LaunchWebAppBrowserAndWait(app_id);
  content::WebContents* web_contents =
      app_browser->GetTabStripModel()->GetActiveWebContents();

  EXPECT_TRUE(EvalJs(web_contents, "!!window.LaunchQueue").ExtractBool());
  EXPECT_TRUE(EvalJs(web_contents, "!!window.launchQueue").ExtractBool());
  EXPECT_TRUE(EvalJs(web_contents, "!!window.LaunchParams").ExtractBool());

  histogram_tester.ExpectUniqueSample(kLaunchHandlerHistogram,
                                      ClientMode::kAuto, 1);
}

}  // namespace web_app
