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

#include <string>
#include <string_view>
#include <tuple>

#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_keybinding_registry.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/supervised_user/supervised_user_test_util.h"
#include "chrome/browser/ui/supervised_user/extension_install_blocked_by_parent_dialog.h"
#include "chrome/browser/ui/supervised_user/parent_permission_dialog.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "chrome/test/supervised_user/browser_user.h"
#include "chrome/test/supervised_user/family_live_test.h"
#include "components/prefs/pref_service.h"
#include "components/supervised_user/core/common/pref_names.h"
#include "components/supervised_user/test_support/family_link_settings_state_management.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/install_verifier.h"
#include "extensions/test/test_extension_dir.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/interaction/element_identifier.h"

namespace supervised_user {
namespace {

static constexpr std::string_view kChromeManageExternsionsUrl =
    "chrome://extensions/";
static constexpr std::string_view kExtensionSiteSettingsUrl =
    "chrome://settings/content/siteDetails?site=chrome-extension://";
static constexpr std::string_view kExtensionName = "An Extension";

// TODO(b/321242366): Consider moving to helper class.
// Checks if a page title matches the given regexp in ecma script dialect.
InteractiveBrowserTestApi::StateChange PageWithMatchingTitle(
    std::string_view title_regexp) {
  DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(kStateChange);
  InteractiveBrowserTestApi::StateChange state_change;
  state_change.type =
      InteractiveBrowserTestApi::StateChange::Type::kConditionTrue;
  state_change.event = kStateChange;
  state_change.test_function = base::StringPrintf(R"js(
    () => /%s/.test(document.title)
  )js",
                                                  title_regexp.data());
  state_change.continue_across_navigation = true;
  return state_change;
}

// Test the behavior of handling extensions for supervised users.
class SupervisedUserExtensionsParentalControlsUiTest
    : public InteractiveFamilyLiveTest,
      public testing::WithParamInterface<
          std::tuple<FamilyLiveTest::RpcMode,
                     /*permissions_switch_state=*/FamilyLinkToggleState,
                     /*extensions_switch_state=*/FamilyLinkToggleState>> {
 public:
  SupervisedUserExtensionsParentalControlsUiTest()
      : InteractiveFamilyLiveTest(GetRpcMode()) {}

 protected:
  // Child tries to enable a disabled extension (which is pending parent
  // approval) by clicking at the extension's toggle.
  // When the Extensions toggle is ON and used to manage the extensions,
  // the extension should be already enabled.
  // In that case the method only verifies the enabled state.
  auto ChildClicksEnableExtensionIfExtensionDisabled(
      ui::ElementIdentifier kChildTab,
      bool expected_extension_enabled) {
    return Steps(
        ExecuteJs(kChildTab, base::StringPrintf(
                                 R"js(
                () => {
                  const view_manager =
                    document.querySelector("extensions-manager").shadowRoot
                      .querySelector("#container").querySelector("#viewManager");
                  if (!view_manager) {
                    throw Error("Path to view_manager element is invalid.");
                  }
                  const container = view_manager.querySelector("#items-list")
                    .shadowRoot.querySelector("#container");
                  if (!container) {
                    throw Error("Path to container element is invalid.");
                  }
                  const count = container.querySelectorAll("extensions-item").length;
                  if (count !== 1) {
                    throw Error("Encountered unexpected number of extensions: " + count);
                  }
                  const extn = container.querySelectorAll("extensions-item")[0];
                  if (!extn) {
                    throw Error("Path to extension element is invalid.");
                  }
                  const toggle = extn.shadowRoot.querySelector("#enableToggle");
                  if (!toggle) {
                    throw Error("Path to extension toggle is invalid.");
                  }
                  if (toggle.ariaPressed != "%s") {
                    throw Error("Extension toggle in unexpected state: " + toggle.ariaPressed);
                  }
                  if (toggle.ariaPressed == "false") {
                    toggle.click();
                  }
                }
              )js",
                                 base::ToString(expected_extension_enabled))),
        Log("Child inspected extension toggle."));
  }

  // Installs programmatically (not through the UI) an extension for the given
  // user.
  void InstallExtension(Profile* profile) {
    std::string extension_manifest = base::StringPrintf(
        R"({
            "name": "%s",
            "manifest_version": 3,
            "version": "0.1",
            "host_permissions": ["<all_urls>"],
            "permissions": [ "geolocation" ]
          })",
        kExtensionName.data());
    extensions::TestExtensionDir extension_dir;
    extension_dir.WriteManifest(extension_manifest);

    extensions::ChromeTestExtensionLoader extension_loader(profile);
    extension_loader.set_ignore_manifest_warnings(true);
    extension_loader.LoadExtension(extension_dir.Pack());
  }

  ui::ElementIdentifier GetTargetUIElement() {
    CHECK(GetExtensionsSwitchTargetState() == FamilyLinkToggleState::kDisabled);
    // Parent approval dialog should appear.
    return ParentPermissionDialog::kDialogViewIdForTesting;
  }

  // Navigates to the `Settings` page for the installed extension under test
  // and inspects the permissions granted to the `Location` setting.
  // Checks if the `Locations` attribute is editable or not (html attribute
  // should be disabled), respecting the configuration of the "Permissions"
  // switch in Family Link.
  auto CheckExtensionLocationPermissions(ui::ElementIdentifier kChildElementId,
                                         Profile* profile) {
    extensions::ExtensionId installed_extension_id;
    const auto& installed_extensions =
        extensions::ExtensionRegistry::Get(profile)
            ->GenerateInstalledExtensionsSet();
    for (const auto& extension : installed_extensions) {
      if (extension->name() == kExtensionName) {
        installed_extension_id = extension->id();
        break;
      }
    }
    CHECK(installed_extension_id.size() > 0)
        << "There must be an installed extension.";

    // When the Permissions FL switch is Off, the Location permissions button
    // should be disabled (unmodifiable).
    bool permissions_button_greyed_out =
        GetPermissionsSwitchTargetState() == FamilyLinkToggleState::kDisabled;
    return Steps(
        Log("With installed extension : " + installed_extension_id),
        NavigateWebContents(kChildElementId,
                            GURL(std::string(kExtensionSiteSettingsUrl) +
                                 std::string(installed_extension_id))),
        WaitForStateChange(kChildElementId, PageWithMatchingTitle("Settings")),
        Log("With extension settings page open."),
        // Detect the Location permission and check whether it's user
        // modifiable.
        ExecuteJs(kChildElementId,
                  base::StringPrintf(
                      R"js(
          () => { const location_permission = document.querySelector("body > settings-ui")
                .shadowRoot.querySelector("#main")
                .shadowRoot.querySelector("settings-basic-page")
                .shadowRoot.querySelector("#basicPage > settings-section.expanded > settings-privacy-page")
                .shadowRoot.querySelector("#pages > settings-subpage > site-details")
                .shadowRoot.querySelector('[label="Location"]')
                .shadowRoot.querySelector("#permission");
                if (!location_permission) {
                  throw Error('No location permission menu was found.');
                }
                if (location_permission.disabled === "%s") {
                  throw Error('Unexpected Location Permission state: ' + permission_drop.disabled);
                }
              }
          )js",
                      base::ToString(permissions_button_greyed_out))),
        Log("Child inspected Location Permission button."));
  }

  auto CheckForParentDialogIfExtensionDisabled(
      bool is_expected_extension_enabled) {
    if (is_expected_extension_enabled) {
      // No dialog appears in this case.
      return Steps(Log("No dialog check is done, the extension is enabled."));
    }
    auto target_ui_element_id = GetTargetUIElement();
    return Steps(
        Log(base::StringPrintf("Waiting for the %s to appear.",
                               (target_ui_element_id ==
                                ParentPermissionDialog::kDialogViewIdForTesting)
                                   ? "parent approval dialog"
                                   : "blocked extension message")),
        WaitForShow(target_ui_element_id),
        Log(base::StringPrintf("The %s appears.",
                               (target_ui_element_id ==
                                ParentPermissionDialog::kDialogViewIdForTesting)
                                   ? "parent approval dialog"
                                   : "blocked extension message")));
  }

  static FamilyLiveTest::RpcMode GetRpcMode() {
    return std::get<0>(GetParam());
  }

  static FamilyLinkToggleState GetPermissionsSwitchTargetState() {
    return std::get<1>(GetParam());
  }

  static FamilyLinkToggleState GetExtensionsSwitchTargetState() {
    return std::get<2>(GetParam());
  }
};

IN_PROC_BROWSER_TEST_P(SupervisedUserExtensionsParentalControlsUiTest,
                       ChildTogglesExtensionMissingParentApproval) {
  extensions::ScopedInstallVerifierBypassForTest install_verifier_bypass;

  DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kChildElementId);
  DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(InIntendedStateObserver,
                                      kDefineStateObserverId);
  DEFINE_LOCAL_STATE_IDENTIFIER_VALUE(InIntendedStateObserver,
                                      kResetStateObserverId);
  const int child_tab_index = 0;

  // The extensions should be disabled (pending parent approval) in all cases,
  // expect when the new "Extensions" FL switch is ON.
  const bool should_be_enabled =
      GetExtensionsSwitchTargetState() == FamilyLinkToggleState::kEnabled;

  SigninToBrowser();

  // Set the FL switch in the value that require parent approvals for
  // extension installation.
  RunTestSequence(Log("Set config that requires parental approvals."),
                  WaitForStateSeeding(
                      kResetStateObserverId, child(),
                      FamilyLinkSettingsState::SetAdvancedSettingsDefault()));

  InstallExtension(&child().profile());

  RunTestSequence(InAnyContext(
      Log("Given an installed disabled extension."),
      // Parent sets both the FL Permissions and Extensions switches.
      // Only one of them impacts the handling of supervised user extensions.
      WaitForStateSeeding(
          kDefineStateObserverId, child(),
          FamilyLinkSettingsState::AdvancedSettingsToggles(
              {FamilyLinkToggleConfiguration(
                   {.type = FamilyLinkToggleType::kExtensionsToggle,
                    .state = GetExtensionsSwitchTargetState()}),
               FamilyLinkToggleConfiguration(
                   {.type = FamilyLinkToggleType::kPermissionsToggle,
                    .state = GetPermissionsSwitchTargetState()})})),
      // Child navigates to the extensions page and tries to enable the
      // extension, if it is disabled.
      Log("When child visits the extensions management page."),
      InstrumentTab(kChildElementId, child_tab_index, &child().browser()),
      NavigateWebContents(kChildElementId, GURL(kChromeManageExternsionsUrl)),
      WaitForStateChange(kChildElementId, PageWithMatchingTitle("Extensions")),
      Log("When child tries to enable the extension."),
      ChildClicksEnableExtensionIfExtensionDisabled(kChildElementId,
                                                    should_be_enabled),
      // If the extension is not already enabled, check that the expect UI
      // dialog appears.
      CheckForParentDialogIfExtensionDisabled(should_be_enabled),
      CheckExtensionLocationPermissions(kChildElementId, &child().profile())));
}

INSTANTIATE_TEST_SUITE_P(
    All,
    SupervisedUserExtensionsParentalControlsUiTest,
    testing::Combine(
        testing::Values(FamilyLiveTest::RpcMode::kProd,
                        FamilyLiveTest::RpcMode::kTestImpersonation),
        /*permissions_switch_target_value=*/
        testing::Values(FamilyLinkToggleState::kEnabled,
                        FamilyLinkToggleState::kDisabled),
        /*extensions_switch_target_value==*/
        testing::Values(FamilyLinkToggleState::kEnabled,
                        FamilyLinkToggleState::kDisabled)),
    [](const auto& info) {
      return ToString(std::get<0>(info.param)) +
             std::string(
                 (std::get<1>(info.param) == FamilyLinkToggleState::kEnabled
                      ? "WithPermissionsOn"
                      : "WithPermissionsOff")) +
             std::string(
                 (std::get<2>(info.param) == FamilyLinkToggleState::kEnabled
                      ? "WithExtensionsOn"
                      : "WithExtensionsOff"));
    });

}  // namespace
}  // namespace supervised_user
