// 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 <optional>

#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/link_capturing/link_capturing_feature_test_support.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/ui/web_applications/test/web_app_navigation_browsertest.h"
#include "chrome/browser/web_applications/manifest_update_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_origin_association_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chrome_features.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/webapps/services/web_app_origin_association/test/test_web_app_origin_association_fetcher.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/url_loader_interceptor.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
#include "url/origin.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/apps/app_service/app_registry_cache_waiter.h"
#include "chrome/browser/apps/intent_helper/preferred_apps_test_util.h"
#endif

namespace web_app {

class WebAppScopeExtensionsBrowserTest
    : public WebAppNavigationBrowserTest,
      public testing::WithParamInterface<
          apps::test::LinkCapturingFeatureVersion> {
 public:
  WebAppScopeExtensionsBrowserTest()
      : WebAppScopeExtensionsBrowserTest(/*enabled=*/true) {}

  explicit WebAppScopeExtensionsBrowserTest(bool enabled)
      : primary_server_(net::EmbeddedTestServer::TYPE_HTTPS),
        secondary_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
    std::vector<base::test::FeatureRefAndParams> enabled_features =
        apps::test::GetFeaturesToEnableLinkCapturingUX(GetParam());

    feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
  }
  ~WebAppScopeExtensionsBrowserTest() override = default;

  void SetUpOnMainThread() override {
    WebAppNavigationBrowserTest::SetUpOnMainThread();

    primary_server_.AddDefaultHandlers(GetChromeTestDataDir());
    primary_server_.RegisterRequestHandler(
        base::BindRepeating(&WebAppScopeExtensionsBrowserTest::RequestHandler,
                            base::Unretained(this)));
    ASSERT_TRUE(primary_server_.Start());
    primary_origin_ = primary_server_.GetOrigin();
    primary_scope_ = primary_server_.GetURL("/web_apps/basic.html");

    secondary_server_.AddDefaultHandlers(GetChromeTestDataDir());
    secondary_server_.RegisterRequestHandler(
        base::BindRepeating(&WebAppScopeExtensionsBrowserTest::RequestHandler,
                            base::Unretained(this)));
    ASSERT_TRUE(secondary_server_.Start());
    secondary_origin_ = secondary_server_.GetOrigin();
    secondary_scope_ = secondary_server_.GetURL("/web_apps/basic.html");

    unrelated_server_.AddDefaultHandlers(GetChromeTestDataDir());
    ASSERT_TRUE(unrelated_server_.Start());
    unrelated_url_ = unrelated_server_.GetURL("/simple.html");
  }

  void TearDownOnMainThread() override {
    app_ = nullptr;
    WebAppNavigationBrowserTest::TearDownOnMainThread();
  }

  std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
      const net::test_server::HttpRequest& request) {
    auto it = url_overrides_.find(request.GetURL());
    if (it == url_overrides_.end()) {
      return nullptr;
    }
    auto http_response =
        std::make_unique<net::test_server::BasicHttpResponse>();
    http_response->set_code(net::HTTP_OK);
    http_response->set_content(it->second);
    return http_response;
  }

  WebAppProvider& provider() {
    return *WebAppProvider::GetForTest(browser()->GetProfile());
  }

  bool LinkCapturingEnabledByDefault() const {
    return GetParam() == apps::test::LinkCapturingFeatureVersion::kV2DefaultOn;
  }

  webapps::AppId InstallScopeExtendedWebApp(std::string manifest_file,
                                            std::string association_file) {
    GURL manifest_url = primary_server_.GetURL("/web_apps/manifest.json");
    GURL association_url =
        secondary_server_.GetURL("/.well-known/web-app-origin-association");

    url_overrides_[manifest_url] = manifest_file;
    url_overrides_[association_url] = association_file;

    webapps::AppId app_id = InstallWebAppInNewTabAndClose(
        browser(),
        primary_server_.GetURL("/web_apps/get_manifest.html?manifest.json"));

    app_ = provider().registrar_unsafe().GetAppById(app_id);

    // Turn on link capturing if needed.
#if BUILDFLAG(IS_CHROMEOS)
    apps::AppReadinessWaiter(browser()->GetProfile(), app_id).Await();
#endif
    if (!LinkCapturingEnabledByDefault()) {
      EXPECT_THAT(apps::test::EnableLinkCapturingByUser(browser()->GetProfile(),
                                                        app_id),
                  base::test::HasValue());
    }
    return app_id;
  }

  std::optional<webapps::AppId> GetCapturingAppId(const GURL& url) {
    CHECK_NE(url, unrelated_url_);
    NavigateViaLinkClickToURLAndWait(browser(), unrelated_url_);

    ui_test_utils::BrowserCreatedObserver browser_created_observer;

    // This always creates a new top level browsing context which is essential
    // to trigger navigation capturing.
    WebAppNavigationBrowserTest::ClickLinkAndWaitForURL(
        browser()->tab_strip_model()->GetActiveWebContents(),
        /*link_url=*/url,
        /*target_url=*/url, WebAppNavigationBrowserTest::LinkTarget::BLANK,
        /*rel=*/"");

    // If `ClickLinkAndWaitForURL()` does not perform navigation capturing, then
    // it will open a new tab in the same browser, and the active web contents
    // will change.
    if (browser()->tab_strip_model()->GetActiveWebContents()->GetVisibleURL() ==
        url) {
      return std::nullopt;
    }

    BrowserWindowInterface* app_browser = browser_created_observer.Wait();
    if (!web_app::AppBrowserController::From(app_browser)) {
      chrome::CloseWindow(app_browser);
      return std::nullopt;
    }

    EXPECT_EQ(
        app_browser->tab_strip_model()->GetActiveWebContents()->GetVisibleURL(),
        url);
    webapps::AppId captured_app_id =
        web_app::AppBrowserController::From(app_browser)->app_id();
    chrome::CloseWindow(app_browser);
    return captured_app_id;
  }

  bool WebAppCapturesUrl(const GURL& url) {
    return GetCapturingAppId(url).has_value();
  }

 protected:
  net::EmbeddedTestServer primary_server_;
  url::Origin primary_origin_;
  GURL primary_scope_;

  net::EmbeddedTestServer secondary_server_;
  url::Origin secondary_origin_;
  GURL secondary_scope_;

  net::EmbeddedTestServer unrelated_server_;
  GURL unrelated_url_;

  std::map<GURL, std::string> url_overrides_;

  raw_ptr<const WebApp> app_ = nullptr;

  base::test::ScopedFeatureList feature_list_;
  content::ContentMockCertVerifier cert_verifier_;
};

IN_PROC_BROWSER_TEST_P(WebAppScopeExtensionsBrowserTest,
                       ExtendedLinkCapturingProperlyLimitsScope) {
  InstallScopeExtendedWebApp(
      /*manifest_file=*/base::ReplaceStringPlaceholders(
          R"(
          {
            "Name": "Test app",
            "start_url": "/",
            "scope": "/",
            "scope_extensions": [{
              "type": "origin", "origin": "$1"
            }]
          })",
          {secondary_origin_.Serialize()}, nullptr),
      /*association_file=*/base::ReplaceStringPlaceholders(
          R"(
          {
            "$1" : { "scope": "/scope-limiter" }
          })",
          {primary_origin_.Serialize()}, nullptr));

  EXPECT_THAT(app_->scope_extensions(),
              testing::ElementsAre(
                  ScopeExtensionInfo::CreateForOrigin(secondary_origin_)));

  // We expect that validated scope extensions differ from the requested
  // scope_extension defined in the app manifest.
  EXPECT_NE(app_->scope_extensions(), app_->validated_scope_extensions());
  GURL limited_scope(secondary_origin_.Serialize() + "/scope-limiter");
  EXPECT_THAT(
      app_->validated_scope_extensions(),
      testing::ElementsAre(ScopeExtensionInfo::CreateForScope(limited_scope)));

  // primary_server_ is the web app's server
  GURL primary_server_launch_url =
      primary_server_.GetURL("/web_apps/basic.html");
  EXPECT_TRUE(WebAppCapturesUrl(primary_server_launch_url));

  // secondary_server_ is the associate's server. We expect this navigation to
  // not capture since it is not in the extended scope "/scope-limiter"
  GURL secondary_server_launch_url =
      secondary_server_.GetURL("/web_apps/basic.html");
  EXPECT_FALSE(WebAppCapturesUrl(secondary_server_launch_url));
}

IN_PROC_BROWSER_TEST_P(WebAppScopeExtensionsBrowserTest,
                       ExtendedLinkCapturingBasic) {
  InstallScopeExtendedWebApp(
      /*manifest_file=*/base::ReplaceStringPlaceholders(
          R"(
          {
            "Name": "Test app",
            "start_url": "/",
            "scope": "/",
            "scope_extensions": [{
              "type": "origin", "origin": "$1"
            }]
          })",
          {secondary_origin_.Serialize()}, nullptr),
      /*association_file=*/base::ReplaceStringPlaceholders(
          R"(
          {
            "$1" : { "scope": "/web_apps/basic.html" }
          })",
          {primary_origin_.Serialize()}, nullptr));

  EXPECT_THAT(app_->scope_extensions(),
              testing::ElementsAre(
                  ScopeExtensionInfo::CreateForOrigin(secondary_origin_)));

  EXPECT_THAT(app_->validated_scope_extensions(),
              testing::ElementsAre(
                  ScopeExtensionInfo::CreateForScope(secondary_scope_)));

  EXPECT_TRUE(
      WebAppCapturesUrl(primary_server_.GetURL("/web_apps/basic.html")));
  EXPECT_TRUE(
      WebAppCapturesUrl(secondary_server_.GetURL("/web_apps/basic.html")));
}

IN_PROC_BROWSER_TEST_P(WebAppScopeExtensionsBrowserTest,
                       ExtendedLinkCapturingFocusExisting) {
  InstallScopeExtendedWebApp(
      /*manifest_file=*/base::ReplaceStringPlaceholders(
          R"(
          {
            "Name": "Test app",
            "start_url": "/simple.html",
            "scope": "/",
            "scope_extensions": [{
              "type": "origin", "origin": "$1"
            }],
            "launch_handler": {
              "client_mode": "focus-existing"
            }
          })",
          {secondary_origin_.Serialize()}, nullptr),
      /*association_file=*/base::ReplaceStringPlaceholders(
          R"(
          {
            "$1" : {}
          })",
          {primary_server_.GetURL("/simple.html").spec()}, nullptr));

  BrowserWindowInterface* app_browser =
      LaunchWebAppBrowserAndWait(app_->app_id());
  content::WebContents* app_web_contents =
      app_browser->tab_strip_model()->GetActiveWebContents();

  // Await the first LaunchParams.
  const char* script = R"(
    window.launchParamsPromise = new Promise(resolve => {
      window.resolveLaunchParamsPromise = resolve;
    });
    launchQueue.setConsumer(launchParams => {
      window.resolveLaunchParamsPromise(launchParams.targetURL);
      window.resolveLaunchParamsPromise = null;
    });
    window.launchParamsPromise;
  )";
  EXPECT_EQ(EvalJs(app_web_contents, script).ExtractString(),
            app_->start_url().spec());

  // Set up the next LaunchParams promise.
  script = R"(
    window.launchParamsPromise = new Promise(resolve => {
      window.resolveLaunchParamsPromise = resolve;
    });
    true;
  )";
  EXPECT_TRUE(EvalJs(app_web_contents, script).ExtractBool());

  // Link capture an extended scope URL.
  GURL extended_scope_url =
      secondary_server_.GetURL("/url/that/does/not/get/navigated/to");
  ClickLink(browser()->tab_strip_model()->GetActiveWebContents(),
            /*link_url=*/extended_scope_url, LinkTarget::BLANK);

  // Await the second LaunchParams in the same app document.
  EXPECT_EQ(
      EvalJs(app_web_contents, "window.launchParamsPromise").ExtractString(),
      extended_scope_url.spec());
  // The document should not have navigated due to "focus-existing".
  EXPECT_EQ(app_web_contents->GetVisibleURL(), app_->start_url().spec());
}

IN_PROC_BROWSER_TEST_P(WebAppScopeExtensionsBrowserTest,
                       ExtendedLinkCapturingBadAssociationFile) {
  InstallScopeExtendedWebApp(
      /*manifest_file=*/base::ReplaceStringPlaceholders(
          R"(
          {
            "Name": "Test app",
            "start_url": "/",
            "scope": "/",
            "scope_extensions": [{
              "type": "origin", "origin": "$1"
            }]
          })",
          {secondary_origin_.Serialize()}, nullptr),
      /*association_file=*/"garbage");

  EXPECT_TRUE(
      WebAppCapturesUrl(primary_server_.GetURL("/web_apps/basic.html")));
  EXPECT_FALSE(
      WebAppCapturesUrl(secondary_server_.GetURL("/web_apps/basic.html")));
}

IN_PROC_BROWSER_TEST_P(WebAppScopeExtensionsBrowserTest,
                       PrimaryScopeTakesPriorityOverExtendedScope) {
  // Install App A (regular scope on secondary_server_, no extended scope).
  GURL app_a_manifest_url =
      secondary_server_.GetURL("/web_apps/app_a.webmanifest");
  GURL app_a_page_url = secondary_server_.GetURL("/web_apps/page1.html");
  url_overrides_[app_a_manifest_url] =
      base::ReplaceStringPlaceholders(R"({
          "name": "App A",
          "start_url": "$1",
          "scope": "/web_apps/"
        })",
                                      {app_a_page_url.GetPath()}, nullptr);
  webapps::AppId app_a_id = InstallWebAppFromPage(
      browser(), secondary_server_.GetURL(
                     "/web_apps/get_manifest.html?app_a.webmanifest"));
  if (!LinkCapturingEnabledByDefault()) {
    ASSERT_EQ(apps::test::EnableLinkCapturingByUser(browser()->GetProfile(),
                                                    app_a_id),
              base::ok());
  }

  // Install App B (regular scope on primary_server_, extended scope on
  // secondary_server_).
  const GURL app_b_page_url = primary_server_.GetURL("/index.html");
  webapps::AppId app_b_id = InstallScopeExtendedWebApp(
      /*manifest_file=*/base::ReplaceStringPlaceholders(
          R"({
            "name": "App B",
            "start_url": "$1",
            "scope": "/",
            "scope_extensions": [{ "type": "origin", "origin": "$2" }]
          })",
          {app_b_page_url.GetPath(), secondary_origin_.Serialize()}, nullptr),
      /*association_file=*/base::ReplaceStringPlaceholders(
          R"({ "$1": { "scope": "/web_apps/longer/" } })",
          {app_b_page_url.spec()}, nullptr));

  bool allow_overlapping_scopes = true;
  // Overlapping scopes are only allowed if navigation capturing
  // is on-by-default.
#if BUILDFLAG(IS_CHROMEOS)
  allow_overlapping_scopes = LinkCapturingEnabledByDefault();
#endif  // BUILDFLAG(IS_CHROMEOS)

  if (allow_overlapping_scopes) {
    EXPECT_EQ(app_a_id, GetCapturingAppId(app_a_page_url));
    EXPECT_EQ(app_b_id, GetCapturingAppId(app_b_page_url));
  } else {
    // On Chrome OS enabling link capturing for an app whose scope overlaps in
    // any way with another apps scope disables capturing for the other app. As
    // such the scope extensions for app B will have disabled the link capturing
    // for app A.
    EXPECT_EQ(std::nullopt, GetCapturingAppId(app_a_page_url));
    EXPECT_EQ(app_b_id, GetCapturingAppId(app_b_page_url));

    // Re-enable link capturing for app A, which (on Chrome OS) disables
    // capturing for app B.
    ASSERT_THAT(apps::test::EnableLinkCapturingByUser(browser()->GetProfile(),
                                                      app_a_id),
                base::test::HasValue());
    EXPECT_EQ(app_a_id, GetCapturingAppId(app_a_page_url));
    EXPECT_EQ(std::nullopt, GetCapturingAppId(app_b_page_url));
  }

  GURL target_url = secondary_server_.GetURL("/web_apps/longer/page.html");
  url_overrides_[target_url] = R"(<html></html>)";
  EXPECT_EQ(app_a_id, GetCapturingAppId(target_url));

  if (allow_overlapping_scopes) {
    // After uninstalling app A, links should be captured by app B.
    UninstallWebApp(app_a_id);
    EXPECT_EQ(app_b_id, GetCapturingAppId(target_url));
  }
}

IN_PROC_BROWSER_TEST_P(WebAppScopeExtensionsBrowserTest,
                       CrossOriginCrossPathCoexistence) {
  // Install App A (regular scope on secondary_server_, no extended scope).
  GURL app_a_manifest_url =
      secondary_server_.GetURL("/web_apps/app_a.webmanifest");
  GURL app_a_page_url = secondary_server_.GetURL("/web_apps/page1.html");
  url_overrides_[app_a_manifest_url] =
      base::ReplaceStringPlaceholders(R"({
          "name": "App A",
          "start_url": "$1",
          "scope": "/web_apps/"
        })",
                                      {app_a_page_url.GetPath()}, nullptr);
  webapps::AppId app_a_id = InstallWebAppFromPage(
      browser(), secondary_server_.GetURL(
                     "/web_apps/get_manifest.html?app_a.webmanifest"));
  if (!LinkCapturingEnabledByDefault()) {
    ASSERT_EQ(apps::test::EnableLinkCapturingByUser(browser()->GetProfile(),
                                                    app_a_id),
              base::ok());
  }

  // Install App B (regular scope on primary_server_, extended scope on
  // secondary_server_).
  const GURL app_b_page_url =
      primary_server_.GetURL("/web_apps/longer/page1.html");
  webapps::AppId app_b_id = InstallScopeExtendedWebApp(
      /*manifest_file=*/base::ReplaceStringPlaceholders(
          R"({
            "name": "App B",
            "start_url": "$1",
            "scope": "/",
            "scope_extensions": [{ "type": "origin", "origin": "$2" }]
          })",
          {app_b_page_url.GetPath(), secondary_origin_.Serialize()}, nullptr),
      /*association_file=*/base::ReplaceStringPlaceholders(
          R"({ "$1": { "scope": "/web_apps_inner/page1.html" } })",
          {app_b_page_url.spec()}, nullptr));

  GURL app_b_extended_url =
      secondary_server_.GetURL("/web_apps_inner/page1.html");
  url_overrides_[app_b_extended_url] = R"(<html></html>)";

  // Since the scopes do not overlap, App B installation should NOT disable
  // App A on any platform/configuration. Both should be able to capture
  // their respective URLs.
  EXPECT_EQ(app_a_id, GetCapturingAppId(app_a_page_url));
  EXPECT_EQ(app_b_id, GetCapturingAppId(app_b_page_url));
  EXPECT_EQ(app_b_id, GetCapturingAppId(app_b_extended_url));
}

INSTANTIATE_TEST_SUITE_P(
    All,
    WebAppScopeExtensionsBrowserTest,
    testing::Values(apps::test::LinkCapturingFeatureVersion::kV2DefaultOff,
                    apps::test::LinkCapturingFeatureVersion::kV2DefaultOn),
    apps::test::LinkCapturingVersionToString);

class WebAppScopeExtensionsDisabledBrowserTest
    : public WebAppScopeExtensionsBrowserTest {
 public:
  WebAppScopeExtensionsDisabledBrowserTest()
      : WebAppScopeExtensionsBrowserTest(/*enabled=*/false) {}
};

IN_PROC_BROWSER_TEST_P(WebAppScopeExtensionsDisabledBrowserTest,
                       ExtendedLinkCapturing) {
  InstallScopeExtendedWebApp(
      /*manifest_file=*/base::ReplaceStringPlaceholders(
          R"(
          {
            "Name": "Test app",
            "start_url": "/",
            "scope": "/",
            "scope_extensions": [{
              "type": "origin", "origin": "$1"
            }]
          })",
          {secondary_origin_.Serialize()}, nullptr),
      /*association_file=*/base::ReplaceStringPlaceholders(
          R"(
          {
            "$1" : {}
          })",
          {primary_origin_.Serialize()}, nullptr));

  EXPECT_FALSE(app_->scope_extensions().empty());
  EXPECT_FALSE(app_->validated_scope_extensions().empty());

  ASSERT_TRUE(
      WebAppCapturesUrl(primary_server_.GetURL("/web_apps/basic.html")));
  EXPECT_TRUE(
      WebAppCapturesUrl(secondary_server_.GetURL("/web_apps/basic.html")));
}

INSTANTIATE_TEST_SUITE_P(
    All,
    WebAppScopeExtensionsDisabledBrowserTest,
    testing::Values(apps::test::LinkCapturingFeatureVersion::kV2DefaultOff,
                    apps::test::LinkCapturingFeatureVersion::kV2DefaultOn),
    apps::test::LinkCapturingVersionToString);

}  // namespace web_app
