// Copyright 2013 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 "base/command_line.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/activity_log/activity_log.h"
#include "chrome/browser/extensions/api/developer_private/developer_private_api.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/safety_hub/extensions_result.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/webui/extensions/extension_settings_test_base.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/guest_view/buildflags/buildflags.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/test/extension_test_message_listener.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/safety_hub/menu_notification_service_factory.h"  // nogncheck
#include "chrome/browser/ui/safety_hub/safety_hub_constants.h"  // nogncheck
#include "chrome/browser/ui/safety_hub/safety_hub_test_util.h"  // nogncheck
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#endif  // BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(ENABLE_GUEST_VIEW)
#include "components/guest_view/browser/guest_view_base.h"
#include "components/guest_view/browser/guest_view_manager_delegate.h"
#include "components/guest_view/browser/test_guest_view_manager.h"
#endif  // BUILDFLAG(ENABLE_GUEST_VIEW)

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

class ExtensionSettingsUIBrowserTest : public ExtensionSettingsTestBase {
#if BUILDFLAG(ENABLE_GUEST_VIEW)
 public:
  guest_view::TestGuestViewManager* GetGuestViewManager() {
    return factory_.GetOrCreateTestGuestViewManager(
        GetProfile(), extensions::ExtensionsAPIClient::Get()
                          ->CreateGuestViewManagerDelegate());
  }

 private:
  guest_view::TestGuestViewManagerFactory factory_;
#endif  // BUILDFLAG(ENABLE_GUEST_VIEW)
};

#if BUILDFLAG(ENABLE_GUEST_VIEW)
#if !BUILDFLAG(IS_ANDROID)  // TODO(b/476468383): does not build on android.
// Tests that viewing a source of the options page works fine.
// This is a regression test for https://crbug.com/41361513.
IN_PROC_BROWSER_TEST_F(ExtensionSettingsUIBrowserTest, ViewSource) {
  // Navigate to an in-page (guest-view-based) extension options page
  // and grab the WebContents hosting the options page.
  const extensions::Extension* extension = InstallExtensionWithInPageOptions();
  GURL options_url("chrome://extensions/?options=" + extension->id());
  content::WebContents* options_contents = nullptr;
  {
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), options_url));
    auto* guest = GetGuestViewManager()->WaitForSingleGuestViewCreated();
    GetGuestViewManager()->WaitUntilAttached(guest);
    options_contents = guest->web_contents();
  }
  ASSERT_TRUE(options_contents);
  EXPECT_TRUE(content::WaitForLoadStop(options_contents));
  EXPECT_EQ(extension->GetResourceURL("options.html"),
            options_contents->GetLastCommittedURL());

  // Open the view-source of the options page.
  int old_tabs_count = browser()->GetTabStripModel()->count();
  content::WebContentsAddedObserver view_source_contents_added_observer;
  options_contents->GetPrimaryMainFrame()->ViewSource();
  content::WebContents* view_source_contents =
      view_source_contents_added_observer.GetWebContents();
  ASSERT_TRUE(view_source_contents);
  EXPECT_TRUE(content::WaitForLoadStop(view_source_contents));

  // Verify that the view-source is present in the tab-strip.
  int new_tabs_count = browser()->GetTabStripModel()->count();
  EXPECT_EQ(new_tabs_count, old_tabs_count + 1);
  EXPECT_EQ(view_source_contents,
            browser()->GetTabStripModel()->GetActiveWebContents());

  // Verify the contents of the view-source tab.
  std::string view_source_extraction_script = R"(
      output = "";
      document.querySelectorAll(".line-content").forEach(function(elem) {
          output += elem.innerText;
      });
      output; )";
  std::string actual_source_text =
      content::EvalJs(view_source_contents, view_source_extraction_script)
          .ExtractString();
  base::FilePath source_path =
      test_data_dir().AppendASCII("options_page_in_view/options.html");
  std::string expected_source_text;
  {
    base::ScopedAllowBlockingForTesting scoped_allow_blocking;
    EXPECT_TRUE(base::ReadFileToString(source_path, &expected_source_text));
  }
  EXPECT_TRUE(
      base::RemoveChars(expected_source_text, "\n", &expected_source_text));
  EXPECT_EQ(expected_source_text, actual_source_text);
}
#endif  // !BUILDFLAG(IS_ANDROID)
#endif  // BUILDFLAG(ENABLE_GUEST_VIEW)

// Verify that listeners for the developer private API are only registered
// when there is a chrome://extensions page open. This is important, since some
// of the event construction can be expensive.
IN_PROC_BROWSER_TEST_F(ExtensionSettingsUIBrowserTest, ListenerRegistration) {
  Profile* profile = GetProfile();
  extensions::EventRouter* event_router = extensions::EventRouter::Get(profile);
  extensions::DeveloperPrivateAPI* dev_private_api =
      extensions::DeveloperPrivateAPI::Get(profile);
  auto expect_has_listeners = [event_router,
                               dev_private_api](bool has_listeners) {
    EXPECT_EQ(has_listeners, event_router->HasEventListener(
                                 "developerPrivate.onItemStateChanged"));
    EXPECT_EQ(has_listeners, event_router->HasEventListener(
                                 "developerPrivate.onProfileStateChanged"));
    EXPECT_EQ(has_listeners,
              dev_private_api->developer_private_event_router() != nullptr);
  };

  {
    SCOPED_TRACE("Before page load");
    expect_has_listeners(false);
  }

  auto* tab = chrome_test_utils::GetActiveTab(this);
  ASSERT_TRUE(tab);

  ASSERT_TRUE(chrome_test_utils::NavigateToURL(tab->GetContents(),
                                               GURL("chrome://extensions")));

  {
    SCOPED_TRACE("With page loaded");
    expect_has_listeners(true);
  }

  tab->Close();

  {
    SCOPED_TRACE("After page unload");
    expect_has_listeners(false);
  }
}

IN_PROC_BROWSER_TEST_F(ExtensionSettingsUIBrowserTest,
                       ActivityLogInactiveWithoutSwitch) {
  content::WebContents* page_contents =
      chrome_test_utils::GetActiveWebContents(this);
  ASSERT_TRUE(page_contents);

  // Navigate to chrome://extensions which is a allowlisted URL for the
  // chrome.activityLogPrivate API.
  GURL extensions_url("chrome://extensions");
  ASSERT_TRUE(chrome_test_utils::NavigateToURL(page_contents, extensions_url));

  // Attempt to add an event listener for the
  // activityLogPrivate.onExtensionActivity event.
  ASSERT_TRUE(content::ExecJs(page_contents, R"(
      let activityLogListener = () => {};
      chrome.activityLogPrivate.onExtensionActivity.addListener(
          activityLogListener);
    )"));

  // Activity log will be inactive as the command line switch is not present and
  // no allowlisted extensions for activityLogPrivate are enabled.
  extensions::ActivityLog* activity_log =
      extensions::ActivityLog::GetInstance(GetProfile());
  ASSERT_FALSE(activity_log->is_active());
}

class ExtensionsActivityLogTest : public ExtensionSettingsUIBrowserTest {
 protected:
  // Enable command line flags for test.
  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(switches::kEnableExtensionActivityLogging);
  }
};

IN_PROC_BROWSER_TEST_F(ExtensionsActivityLogTest, TestActivityLogVisible) {
  content::WebContents* page_contents =
      chrome_test_utils::GetActiveWebContents(this);
  ASSERT_TRUE(page_contents);

  base::FilePath test_data_dir;
  ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir));
  test_data_dir = test_data_dir.AppendASCII("extensions");
  extensions::ChromeTestExtensionLoader loader(GetProfile());

  ExtensionTestMessageListener listener("ready");
  scoped_refptr<const extensions::Extension> extension = loader.LoadExtension(
      test_data_dir.AppendASCII("activity_log/simple_call"));
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  GURL activity_log_url("chrome://extensions/?activity=" + extension->id());
  ASSERT_TRUE(
      chrome_test_utils::NavigateToURL(page_contents, activity_log_url));
  content::WebContents* activity_log_contents =
      chrome_test_utils::GetActiveWebContents(this);
  ASSERT_TRUE(activity_log_contents);
  EXPECT_EQ(activity_log_url, activity_log_contents->GetLastCommittedURL());

  // We are looking for the 'test.sendMessage' entry in the activity log as
  // that is the only API call the simple_call.crx extension does.
  // The querySelectors and shadowRoots are used here in order to penetrate
  // multiple nested shadow DOMs in the chrome://extensions page.
  // This test only serves as an end to end test, and most of the functionality
  // is covered in the JS unit tests.
  EXPECT_EQ(true,
            content::EvalJs(
                activity_log_contents,
                R"(let manager = document.querySelector('extensions-manager');
         let activityLog =
             manager.shadowRoot.querySelector('extensions-activity-log');
         let activityLogHistory =
             activityLog.shadowRoot.querySelector('activity-log-history');
         activityLogHistory.whenDataFetched()
             .then(() => activityLogHistory.updateComplete)
             .then(() => {
               let item = activityLogHistory.shadowRoot.querySelector(
                   'activity-log-history-item');
               let activityKey = item.shadowRoot.getElementById('activity-key');
               return activityKey.innerText === 'test.sendMessage';
             });
      )"));
}

// Safety hub is not supported on Android.
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(ExtensionSettingsUIBrowserTest,
                       TestSafetyHubMenuNotificationDismissed) {
  Profile* profile = browser()->GetProfile();
  extensions::ExtensionPrefs* extension_prefs =
      extensions::ExtensionPrefs::Get(profile);
  const extensions::Extension* extension = InstallExtensionWithInPageOptions();
  SafetyHubMenuNotificationService* notification_service =
      SafetyHubMenuNotificationServiceFactory::GetForProfile(profile);
  // Safety Hub services will be initialized when
  // SafetyHubMenuNotificationService is created. Let Safety Hub services to
  // initialize properly.
  safety_hub_test_util::RunUntilPasswordCheckCompleted(profile);
  // No unpublished extensions yet, so there shouldn't be a menu notifications.
  std::optional<MenuNotificationEntry> notification =
      notification_service->GetNotificationToShow();
  ASSERT_FALSE(notification.has_value());
  // Update the extension pref to flag the extension as unpublished.
  base::DictValue dict;
  dict.Set("is-present", true);
  dict.Set("is-live", true);
  dict.Set("last-updated-time-millis", 100000000);
  dict.Set("violation-type", 0);
  dict.Set("no-privacy-practice", false);
  dict.Set("unpublished-long-ago", true);
  extension_prefs->SetDictionaryPref(
      extension->id(),
      {"cws-info", extensions::kDictionary,
       extensions::PrefScope::kExtensionSpecific},
      std::move(dict));
  notification_service->UpdateResultGetterForTesting(
      safety_hub::SafetyHubModuleType::EXTENSIONS,
      base::BindRepeating(&SafetyHubExtensionsResult::GetResult, profile,
                          /*only_unpublished_extensions=*/true));
  // An extension was unpublished, so we now should get an associated menu
  // notification.
  notification = notification_service->GetNotificationToShow();
  ASSERT_TRUE(notification.has_value());

  // When the user visits the extensions page, notifications for the extension
  // module of Safety Hub should be dismissed.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL("chrome://extensions/")));
  notification = notification_service->GetNotificationToShow();
  ASSERT_FALSE(notification.has_value());
}
#endif  // BUILDFLAG(IS_ANDROID)
