// Copyright 2025 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 <utility>

#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/supervised_user/supervised_user_browsertest_base.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/google/core/common/google_switches.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/safe_search_api/url_checker_client.h"
#include "components/supervised_user/core/browser/android/android_parental_controls.h"
#include "components/url_matcher/url_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"

namespace supervised_user {
namespace {

using ::safe_search_api::ClientClassification;
using ::safe_search_api::URLCheckerClient;
using ::testing::_;

// Covers extra behaviors available only in Clank (Android) related to
// bootstrapping the supervised user service with Content Filters Observer (how
// the browser behaves after init, with no further manipulation of the content
// filters). The tests are parametrized so that they also try to "hot start" the
// browser, simulating that the browser thinks that it was previously
// supervised. To see tests that assert dynamic behaviors (when the filters are
// altered after the browser starts and urls are loaded), see
// supervised_user_navigation_observer_android_browsertest.cc
class SupervisedUserServiceBootstrapAndroidBrowserTestBase
    : public SupervisedUserBrowserTestBase {
 protected:
  content::WebContents* web_contents() {
    return chrome_test_utils::GetActiveWebContents(this);
  }
  base::HistogramTester& histogram_tester() { return histogram_tester_; }

 private:
  void SetUpOnMainThread() override {
    AndroidBrowserTest::SetUpOnMainThread();

    // Will resolve google.com to localhost, so the embedded test server can
    // serve some valid content for it.
    host_resolver()->AddRule("google.com", "127.0.0.1");
    embedded_test_server()->RegisterRequestHandler(base::BindLambdaForTesting(
        [](const net::test_server::HttpRequest& request)
            -> std::unique_ptr<net::test_server::HttpResponse> {
          if (request.GetURL().GetPath() != "/search") {
            return nullptr;
          }
          // HTTP 200 OK with empty response body.
          return std::make_unique<net::test_server::BasicHttpResponse>();
        }));
    CHECK(embedded_test_server()->Start());
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    AndroidBrowserTest::SetUpCommandLine(command_line);
    // The production code only allows known ports (80 for http and 443 for
    // https), but the embedded test server runs on a random port and adds it to
    // the url spec.
    command_line->AppendSwitch(switches::kIgnoreGooglePortNumbers);
  }

  base::HistogramTester histogram_tester_;
};

struct BootstrapServiceTestCase {
  std::string test_name;
  // Determines the value of browser device filter on browser startup.
  bool initial_browser_content_filters_value;
  // Determines the value of search device filter on browser startup.
  bool initial_search_content_filters_value;

  // Returns true if incognito should be blocked based on the initial values of
  // the content filters settings.
  bool ShouldBlockIncognito() const {
    return initial_browser_content_filters_value ||
           initial_search_content_filters_value;
  }
};

// Tests the aspect where the Family Link supervision is not enabled, but the
// content filters are set.
class SupervisedUserServiceBootstrapAndroidBrowserTest
    : public SupervisedUserServiceBootstrapAndroidBrowserTestBase,
      public testing::WithParamInterface<BootstrapServiceTestCase> {
 protected:
  SupervisedUserServiceBootstrapAndroidBrowserTest() {
    SetInitialSupervisedUserState(
        {.android_parental_controls = {
             .browser_filter =
                 GetTestCase().initial_browser_content_filters_value,
             .search_filter =
                 GetTestCase().initial_search_content_filters_value,
         }});
  }

  const BootstrapServiceTestCase& GetTestCase() const { return GetParam(); }
};

IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
                       IncognitoIsBlockedWhenAnyFilterIsEnabled) {
  ASSERT_NE(nullptr, GetSupervisedUserService());

  policy::IncognitoModeAvailability expected_incognito_mode_availability =
      GetTestCase().ShouldBlockIncognito()
          ? policy::IncognitoModeAvailability::kDisabled
          : policy::IncognitoModeAvailability::kEnabled;

  // TODO(http://crbug.com/433234589): this test could actually try to open
  // incognito (to no avail).
  EXPECT_EQ(expected_incognito_mode_availability,
            static_cast<policy::IncognitoModeAvailability>(
                GetProfile()->GetPrefs()->GetInteger(
                    policy::policy_prefs::kIncognitoModeAvailability)));
}

IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
                       SafeSearchIsEnforcedWhenSearchFilterIsEnabled) {
  GURL request_url =
      embedded_test_server()->GetURL("google.com", "/search?q=cat");
  GURL expected_url = GetTestCase().initial_search_content_filters_value
                          ? GURL(request_url.spec() + "&safe=active&ssui=on")
                          : request_url;

  if (GetTestCase().initial_browser_content_filters_value) {
    // Google search is not on the exempt list of the URL Filter: search
    // requests must be explicitly allowed.
    EXPECT_CALL(GetMockUrlCheckerClient(),
                CheckURL(url_matcher::util::Normalize(expected_url), _))
        .WillOnce([](const GURL& url,
                     URLCheckerClient::ClientCheckCallback callback) {
          std::move(callback).Run(url, ClientClassification::kAllowed);
        });
  }

  EXPECT_TRUE(
      content::NavigateToURL(web_contents(), request_url, expected_url));
}

IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
                       SafeSitesIsEnforcedWhenBrowserFilterIsEnabled) {
  GURL request_url =
      embedded_test_server()->GetURL("/supervised_user/simple.html");

  if (GetTestCase().initial_browser_content_filters_value) {
    EXPECT_CALL(GetMockUrlCheckerClient(),
                CheckURL(url_matcher::util::Normalize(request_url), _))
        .WillOnce([](const GURL& url,
                     URLCheckerClient::ClientCheckCallback callback) {
          std::move(callback).Run(url, ClientClassification::kAllowed);
        });
  } else {
    EXPECT_CALL(GetMockUrlCheckerClient(),
                CheckURL(url_matcher::util::Normalize(request_url), _))
        .Times(0);
  }

  // We assert here (rather than expect) because url checker mock declares the
  // requested url as allowed (or never classified) so they should render at all
  // times. The core of this test is to count calls to the url checker client.
  ASSERT_TRUE(content::NavigateToURL(web_contents(), request_url));
  ASSERT_EQ(web_contents()->GetTitle(), u"Supervised User test: simple page");
}

IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
                       SafeSitesBlocksPagesWhenEnabled) {
  if (!GetTestCase().initial_browser_content_filters_value) {
    GTEST_SKIP() << "This test requires the browser filter to be enabled.";
  }

  GURL request_url =
      embedded_test_server()->GetURL("/supervised_user/simple.html");

  EXPECT_CALL(GetMockUrlCheckerClient(),
              CheckURL(url_matcher::util::Normalize(request_url), _))
      .WillOnce(
          [](const GURL& url, URLCheckerClient::ClientCheckCallback callback) {
            std::move(callback).Run(url, ClientClassification::kRestricted);
          });

  // We assert here (rather than expect) because url checker mock declares the
  // requested url as blocked. What we do care about is that the classification
  // was requested.
  ASSERT_FALSE(content::NavigateToURL(web_contents(), request_url));
  ASSERT_EQ(web_contents()->GetTitle(), u"Site blocked");
}

IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
                       WebFilterTypeIsRecordedOnceWhenBrowserFilterIsEnabled) {
  if (GetTestCase().initial_browser_content_filters_value) {
    histogram_tester().ExpectBucketCount(
        "SupervisedUsers.WebFilterType.LocallySupervised",
        WebFilterType::kTryToBlockMatureSites, 1);
  } else if (GetTestCase().initial_search_content_filters_value) {
    // With url service enabled, when the search filter is enabled and the
    // browser filter is disabled, the web filter type indicates that it allows
    // all sites.
    WebFilterType expected_web_filter_type = WebFilterType::kAllowAllSites;
    histogram_tester().ExpectBucketCount(
        "SupervisedUsers.WebFilterType.LocallySupervised",
        expected_web_filter_type, 1);
  } else {
    histogram_tester().ExpectTotalCount(
        "SupervisedUsers.WebFilterType.LocallySupervised", 0);
  }

  // This histogram is not recorded for locally supervised users.
  histogram_tester().ExpectTotalCount("FamilyUser.WebFilterType", 0);
}

IN_PROC_BROWSER_TEST_P(SupervisedUserServiceBootstrapAndroidBrowserTest,
                       FamilyLinkCoexistsWithDeviceSupervision) {
  bool is_initially_supervised_locally =
      GetTestCase().initial_browser_content_filters_value ||
      GetTestCase().initial_search_content_filters_value;

  // Device supervision is initially enabled/disabled based on the test case,
  // but Family Link supervision is always disabled.
  ASSERT_EQ(is_initially_supervised_locally,
            GetDeviceParentalControls().IsEnabled());
  ASSERT_FALSE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));

  EnableParentalControls(*GetProfile()->GetPrefs());

  // Family Link supervision is enabled and coexists with device controls.
  EXPECT_EQ(is_initially_supervised_locally,
            GetDeviceParentalControls().IsEnabled());
  EXPECT_TRUE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));
}

const BootstrapServiceTestCase kBootstrapServiceTestCases[] = {
    {.test_name = "AllFiltersDisabled",
     .initial_browser_content_filters_value = false,
     .initial_search_content_filters_value = false},
    {.test_name = "AllFiltersEnabled",
     .initial_browser_content_filters_value = true,
     .initial_search_content_filters_value = true},
    {.test_name = "SearchFilterEnabled",
     .initial_browser_content_filters_value = false,
     .initial_search_content_filters_value = true},
    {.test_name = "BrowserFilterEnabled",
     .initial_browser_content_filters_value = true,
     .initial_search_content_filters_value = false}};

INSTANTIATE_TEST_SUITE_P(
    ,
    SupervisedUserServiceBootstrapAndroidBrowserTest,
    testing::ValuesIn(kBootstrapServiceTestCases),
    [](const testing::TestParamInfo<
        SupervisedUserServiceBootstrapAndroidBrowserTest::ParamType>& info) {
      return info.param.test_name;
    });

// Tests the aspect where the Family Link supervision is enabled, but the
// content filters are not set.
class SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest
    : public SupervisedUserServiceBootstrapAndroidBrowserTestBase {
 protected:
  SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest() {
    SetInitialSupervisedUserState({.family_link_parental_controls = true});
  }
};

IN_PROC_BROWSER_TEST_F(
    SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
    IncognitoIsBlocked) {
  // TODO(http://crbug.com/433234589): this test could actually try to open
  // incognito (to no avail).
  EXPECT_EQ(static_cast<policy::IncognitoModeAvailability>(
                GetProfile()->GetPrefs()->GetInteger(
                    policy::policy_prefs::kIncognitoModeAvailability)),
            policy::IncognitoModeAvailability::kDisabled);
}

IN_PROC_BROWSER_TEST_F(
    SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
    SafeSitesBlocksPages) {
  GURL request_url =
      embedded_test_server()->GetURL("/supervised_user/simple.html");

  EXPECT_CALL(GetMockUrlCheckerClient(),
              CheckURL(url_matcher::util::Normalize(request_url), _))
      .WillOnce(
          [](const GURL& url, URLCheckerClient::ClientCheckCallback callback) {
            std::move(callback).Run(url, ClientClassification::kRestricted);
          });

  // We assert here (rather than expect) because url checker mock declares the
  // requested url as blocked. What we do care about is that the classification
  // was requested.
  ASSERT_FALSE(content::NavigateToURL(web_contents(), request_url));
  ASSERT_EQ(web_contents()->GetTitle(), u"Site blocked");
}

IN_PROC_BROWSER_TEST_F(
    SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
    WebFilterTypeIsRecordedOnce) {
  histogram_tester().ExpectBucketCount(
      "SupervisedUsers.WebFilterType.FamilyLink",
      WebFilterType::kTryToBlockMatureSites, 1);
  histogram_tester().ExpectBucketCount(
      "FamilyUser.WebFilterType", WebFilterType::kTryToBlockMatureSites, 1);
}

IN_PROC_BROWSER_TEST_F(
    SupervisedUserServiceBootstrapAndroidBrowserWithSupervisedUserTest,
    FamilyLinkIsImmuneToDeviceSupervision) {
  // Device supervision is initially disabled and Family Link supervision is
  // initially enabled.
  ASSERT_FALSE(GetDeviceParentalControls().IsEnabled());
  ASSERT_TRUE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));

  // Try turning the knob on the local supervision (browser filtering).
  GetDeviceParentalControls().SetBrowserContentFiltersEnabledForTesting(true);
  EXPECT_TRUE(GetDeviceParentalControls().IsEnabled());
  EXPECT_TRUE(GetDeviceParentalControls().IsBrowserContentFiltersEnabled());
  EXPECT_TRUE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));

  // Try turning the knob on the local supervision (search filtering).
  GetDeviceParentalControls().SetSearchContentFiltersEnabledForTesting(true);
  EXPECT_TRUE(GetDeviceParentalControls().IsEnabled());
  EXPECT_TRUE(GetDeviceParentalControls().IsSearchContentFiltersEnabled());
  EXPECT_TRUE(IsSubjectToParentalControls(*GetProfile()->GetPrefs()));
}

// Tests the aspect where the Family Link supervision is disabled and the
// content filters are not set.
class SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest
    : public SupervisedUserServiceBootstrapAndroidBrowserTestBase {};

IN_PROC_BROWSER_TEST_F(
    SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest,
    IncognitoIsNotBlocked) {
  // TODO(http://crbug.com/433234589): this test could actually try to open
  // incognito (to no avail).
  EXPECT_EQ(static_cast<policy::IncognitoModeAvailability>(
                GetProfile()->GetPrefs()->GetInteger(
                    policy::policy_prefs::kIncognitoModeAvailability)),
            policy::IncognitoModeAvailability::kEnabled);
}

IN_PROC_BROWSER_TEST_F(
    SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest,
    SafeSitesIsNotUsed) {
  GURL request_url =
      embedded_test_server()->GetURL("/supervised_user/simple.html");
  EXPECT_CALL(GetMockUrlCheckerClient(),
              CheckURL(url_matcher::util::Normalize(request_url), _))
      .Times(0);

  ASSERT_TRUE(content::NavigateToURL(web_contents(), request_url));
  ASSERT_EQ(web_contents()->GetTitle(), u"Supervised User test: simple page");
}

IN_PROC_BROWSER_TEST_F(
    SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest,
    WebFilterTypeIsNotRecorded) {
  histogram_tester().ExpectTotalCount(
      "SupervisedUsers.WebFilterType.LocallySupervised", 0);
  histogram_tester().ExpectTotalCount("FamilyUser.WebFilterType", 0);
}

IN_PROC_BROWSER_TEST_F(
    SupervisedUserServiceBootstrapAndroidBrowserWithRegularUserTest,
    SafeSearchIsNotEnforcedAtBrowserLevel) {
  GURL url = embedded_test_server()->GetURL("google.com", "/search?q=cat");

  EXPECT_CALL(GetMockUrlCheckerClient(),
              CheckURL(url_matcher::util::Normalize(url), _))
      .Times(0);
  EXPECT_TRUE(content::NavigateToURL(web_contents(), url));
}

}  // namespace
}  // namespace supervised_user
