// 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 "chrome/browser/enterprise/connectors/analysis/content_analysis_info.h"

#include "base/command_line.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/enterprise/connectors/test/active_user_test_mixin.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/safe_browsing_navigation_observer_manager_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chrome/test/base/test_browser_window.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/download/public/common/mock_download_item.h"
#include "components/enterprise/connectors/core/content_area_user_provider.h"
#include "components/safe_browsing/content/browser/safe_browsing_navigation_observer_manager.h"
#include "components/signin/public/identity_manager/test_identity_manager_observer.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "google_apis/gaia/fake_gaia.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_id.h"
#include "google_apis/gaia/gaia_switches.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace enterprise_connectors {

namespace {

using ::testing::_;
using testing::DoAll;
using testing::Return;
using testing::SetArgPointee;

struct ActiveUserTestCase {
  const char* url;
  std::vector<const char*> emails;
  // Represents the email that's expected to be retrieved from `url`. This can
  // be an empty string.
  const char* expected_active_email;
  // Represents the value returned by `ContentAreaUserProvider::GetUser`. When
  // this is null, this is the same as `expected_active_email`, and when it's
  // set it's because a fallback default is used instead of something being
  // retrieved from `url`.
  const char* expected_default_active_email = "";
};

std::vector<ActiveUserTestCase> TestCases() {
  return {
      // "/u/<N>/" test cases:
      ActiveUserTestCase{
          .url = "https://mail.google.com/abcd/u/0/efgh/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "foo@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://meet.google.com/abcd/u/1/efgh/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "bar@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://datastudio.google.com/abcd/u/2/efgh/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          // The index is out of bounds so we can't tell which of the two
          // accounts is active.
          .expected_active_email = "",
          .expected_default_active_email = "foo@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://sites.google.com/abcd/u/0/efgh/",
          .emails = {"bar@gmail.com"},
          .expected_active_email = "bar@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://keep.google.com/abcd/u/1/efgh/",
          .emails = {"bar@gmail.com"},
          // Even if the index doesn't match the number of cookies, we select
          // the email when only one is present.
          .expected_active_email = "bar@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://invalid.case.com/u/0/efgh/",
          .emails = {"bar@gmail.com"},
          .expected_active_email = "",
      },

      // "authuser=<N>" test cases:
      ActiveUserTestCase{
          .url = "https://calendar.google.com/?authuser=0",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "foo@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://drive.google.com/?authuser=1",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "bar@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://gemini.google.com/?authuser=2",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          // The index is out of bounds so we can't tell which of the two
          // accounts is active.
          .expected_active_email = "",
          .expected_default_active_email = "foo@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://script.google.com/?authuser=0",
          .emails = {"bar@gmail.com"},
          .expected_active_email = "bar@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://cloudsearch.google.com/?authuser=1",
          .emails = {"bar@gmail.com"},
          // Even if the index doesn't match the number of cookies, we select
          // the email when only one is present.
          .expected_active_email = "bar@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://invalid.case.com/?authuser=0",
          .emails = {"bar@gmail.com"},
          .expected_active_email = "",
      },

      // The user specified by "authuser=N" has precedence on the one specified
      // in "/u/<N>/".
      ActiveUserTestCase{
          .url = "https://docs.google.com/abcd/u/1/efgh/?authuser=0",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "foo@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://docs.google.com/abcd/u/0/efgh/?authuser=1",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "bar@gmail.com",
      },

      // No index in URL test cases:
      ActiveUserTestCase{
          .url = "https://docs.google.com/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "",
          .expected_default_active_email = "foo@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://console.cloud.google.com/",
          .emails = {"bar@gmail.com"},
          // With only 1 user it has to be the active one.
          .expected_active_email = "bar@gmail.com",
      },
      ActiveUserTestCase{
          .url = "https://invalid.case.com/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "",
      },
  };
}

class ActiveUserEmailBrowserTest
    : public MixinBasedInProcessBrowserTest,
      public testing::WithParamInterface<ActiveUserTestCase> {
 public:
  ActiveUserEmailBrowserTest() {
    active_user_test_mixin_ = std::make_unique<test::ActiveUserTestMixin>(
        &mixin_host_, this, &embedded_https_test_server(), GetParam().emails);
  }

  GURL url() const { return GURL(GetParam().url); }

  std::string expected_active_email() const {
    return GetParam().expected_active_email;
  }

  std::string expected_default_active_email() const {
    return GetParam().expected_default_active_email;
  }

 protected:
  std::unique_ptr<test::ActiveUserTestMixin> active_user_test_mixin_;
};

struct ActiveFrameUserTestCase {
  const char* tab_url;
  const char* frame_url;
  std::vector<const char*> emails;
  const char* expected_active_email;
};

std::vector<ActiveFrameUserTestCase> FrameUserTestCases() {
  return {
      // Invalid Workspace tab URL with invalid frame URL test case.
      ActiveFrameUserTestCase{
          .tab_url = "https://bar.baz.com/",
          .frame_url = "https://foo.bar/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "",
      },
      // Valid Workspace tab URL with invalid frame URL test case.
      ActiveFrameUserTestCase{
          .tab_url = "https://mail.google.com/",
          .frame_url = "https://foo.bar/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "",
      },
      // Invalid Workspace tab URL with valid frame URL test case.
      ActiveFrameUserTestCase{
          .tab_url = "https://foo.bar/",
          .frame_url = "https://ogs.google.com/u/0/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "",
      },
      // Valid "/u/<N>/" test cases.
      ActiveFrameUserTestCase{
          .tab_url = "https://gemini.google.com/",
          .frame_url = "https://ogs.google.com/u/0/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "foo@gmail.com",
      },
      ActiveFrameUserTestCase{
          .tab_url = "https://docs.google.com/",
          .frame_url = "https://ogs.google.com/abcd/efgh/u/1/ijkl/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "bar@gmail.com",
      },
      // Valid "authuser=<N>"/ test cases.
      ActiveFrameUserTestCase{
          .tab_url = "https://gemini.google.com/",
          .frame_url = "https://ogs.google.com/?authuser=0",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "foo@gmail.com",
      },
      ActiveFrameUserTestCase{
          .tab_url = "https://docs.google.com/",
          .frame_url = "https://ogs.google.com/abcd/efgh/ijkl/?authuser=1",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "bar@gmail.com",
      },
      // Valid URLs with no valid index test cases.
      ActiveFrameUserTestCase{
          .tab_url = "https://docs.google.com/",
          .frame_url = "https://ogs.google.com/",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "",
      },
      ActiveFrameUserTestCase{
          .tab_url = "https://gemini.google.com/",
          .frame_url = "https://ogs.google.com/abcd/efgh/ijkl/?authuser=foo",
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "",
      },
      ActiveFrameUserTestCase{
          .tab_url = "https://docs.google.com/",
          .frame_url = "https://ogs.google.com/",
          .emails = {"bar@gmail.com"},
          // With only 1 user it has to be the active one.
          .expected_active_email = "bar@gmail.com",
      },
  };
}

class ActiveFrameUserEmailBrowserTest
    : public MixinBasedInProcessBrowserTest,
      public testing::WithParamInterface<ActiveFrameUserTestCase> {
 public:
  ActiveFrameUserEmailBrowserTest() {
    active_user_test_mixin_ = std::make_unique<test::ActiveUserTestMixin>(
        &mixin_host_, this, &embedded_https_test_server(), GetParam().emails);
  }

  GURL tab_url() const { return GURL(GetParam().tab_url); }
  GURL frame_url() const { return GURL(GetParam().frame_url); }

  std::string expected_active_email() const {
    return GetParam().expected_active_email;
  }

 protected:
  std::unique_ptr<test::ActiveUserTestMixin> active_user_test_mixin_;
};

struct ReferrerChainTestCase {
  std::vector<const char*> referrer_chain;
  std::vector<const char*> emails;
  const char* expected_active_email;
};

std::vector<ReferrerChainTestCase> ReferrerChainTestCases() {
  return {
      ReferrerChainTestCase{
          .referrer_chain = {"https://bar.baz.com/", "https://foo.bar/"},
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "foo@gmail.com",
      },
      ReferrerChainTestCase{
          .referrer_chain = {"https://bar.baz.com/",
                             "https://script.google.com/?authuser=0"},
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "foo@gmail.com",
      },
      ReferrerChainTestCase{
          .referrer_chain = {"https://bar.baz.com/",
                             "https://script.google.com/?authuser=1"},
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "bar@gmail.com",
      },
      ReferrerChainTestCase{
          .referrer_chain = {"https://bar.baz.com/",
                             "https://keep.google.com/abcd/u/0/efgh/"},
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "foo@gmail.com",
      },
      ReferrerChainTestCase{
          .referrer_chain = {"https://keep.google.com/abcd/u/1/efgh/",
                             "https://bar.baz.com/"},
          .emails = {"foo@gmail.com", "bar@gmail.com"},
          .expected_active_email = "bar@gmail.com",
      },
  };
}

class MockSafeBrowsingNavigationObserverManager
    : public safe_browsing::SafeBrowsingNavigationObserverManager {
 public:
  explicit MockSafeBrowsingNavigationObserverManager(
      PrefService* pref_service,
      content::ServiceWorkerContext* context)
      : safe_browsing::SafeBrowsingNavigationObserverManager(pref_service,
                                                             context),
        notification_context_for_removal_(context) {}

  ~MockSafeBrowsingNavigationObserverManager() override {
    if (notification_context_for_removal_) {
      notification_context_for_removal_->RemoveObserver(this);
      ui::Clipboard::GetForCurrentThread()->RemoveObserver(this);
    }
  }

  MOCK_METHOD4(IdentifyReferrerChainByEventURL,
               safe_browsing::ReferrerChainProvider::AttributionResult(
                   const GURL& event_url,
                   SessionID event_tab_id,
                   int user_gesture_count_limit,
                   safe_browsing::ReferrerChain* out_referrer_chain));

 private:
  raw_ptr<content::ServiceWorkerContext> notification_context_for_removal_;
};

class ReferrerChainActiveUserEmailBrowserTest
    : public MixinBasedInProcessBrowserTest,
      public testing::WithParamInterface<ReferrerChainTestCase> {
 public:
  ReferrerChainActiveUserEmailBrowserTest() {
    active_user_test_mixin_ = std::make_unique<test::ActiveUserTestMixin>(
        &mixin_host_, this, &embedded_https_test_server(), GetParam().emails);
  }

  std::string expected_active_email() const {
    return GetParam().expected_active_email;
  }

  std::unique_ptr<KeyedService> BuildMockNavigationObserverManager(
      content::BrowserContext* context) {
    Profile* profile = Profile::FromBrowserContext(context);
    PrefService* pref_service = profile->GetPrefs();
    CHECK(pref_service);
    content::ServiceWorkerContext* service_worker_context =
        profile->GetDefaultStoragePartition()->GetServiceWorkerContext();
    CHECK(service_worker_context);
    auto mock_manager =
        std::make_unique<MockSafeBrowsingNavigationObserverManager>(
            pref_service, service_worker_context);

    safe_browsing::ReferrerChain referrer_chain;
    for (const char* referrer_url : GetParam().referrer_chain) {
      safe_browsing::ReferrerChainEntry referrer_chain_entry;
      referrer_chain_entry.set_url(referrer_url);
      referrer_chain.Add(std::move(referrer_chain_entry));
    }
    EXPECT_CALL(*mock_manager, IdentifyReferrerChainByEventURL(_, _, _, _))
        .WillOnce(DoAll(SetArgPointee<3>(referrer_chain),
                        Return(safe_browsing::ReferrerChainProvider::SUCCESS)));

    return mock_manager;
  }

  void SetUpBrowserContextKeyedServices(
      content::BrowserContext* context) override {
    auto* navigation_observer_manager_factory = safe_browsing::
        SafeBrowsingNavigationObserverManagerFactory::GetInstance();
    navigation_observer_manager_factory->SetTestingFactory(
        context, base::BindRepeating(&ReferrerChainActiveUserEmailBrowserTest::
                                         BuildMockNavigationObserverManager,
                                     base::Unretained(this)));
  }

 protected:
  std::unique_ptr<test::ActiveUserTestMixin> active_user_test_mixin_;
};

}  // namespace

IN_PROC_BROWSER_TEST_P(ActiveUserEmailBrowserTest, GetActiveUser) {
  active_user_test_mixin_->SetFakeCookieValue();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url()));
  std::string expected_email = expected_active_email();
  if (!expected_default_active_email().empty()) {
    expected_email = expected_default_active_email();
  }
  ASSERT_EQ(expected_email,
            ContentAreaUserProvider::GetUser(browser()->GetProfile(),
                                             /*web_contents=*/nullptr, url()));

  auto* identity_manager =
      IdentityManagerFactory::GetForProfile(browser()->GetProfile());
  ASSERT_EQ(expected_active_email(),
            GetActiveContentAreaUser(identity_manager, url()));
  ASSERT_EQ(expected_active_email(),
            GetActiveContentAreaUser(identity_manager, url()));
}

IN_PROC_BROWSER_TEST_P(ActiveUserEmailBrowserTest,
                       GetActiveUser_URLUnsupportedByURLF) {
  // The point of this test is to validate a domain that only the non-URLF
  // getter functions can obtain, so non-google.com test cases are skipped.
  if (url().DomainIs("invalid.case.com")) {
    return;
  }

  // Replace only the host by one not allowlisted by the URLF getter so as to
  // validate that it always return an empty string, while still validating the
  // non-URLF getters can still use the `/u/<id>/` and `authuser=<id>` logic to
  // identify the right user.
  GURL::Replacements replace_host;
  replace_host.SetHostStr("notebooklm.google.com");
  GURL non_urlf_url = url().ReplaceComponents(replace_host);

  active_user_test_mixin_->SetFakeCookieValue();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), non_urlf_url));

  std::string expected_email = expected_active_email();
  if (!expected_default_active_email().empty()) {
    expected_email = expected_default_active_email();
  }
  ASSERT_EQ(expected_email, ContentAreaUserProvider::GetUser(
                                browser()->GetProfile(),
                                /*web_contents=*/nullptr, non_urlf_url));

  auto* identity_manager =
      IdentityManagerFactory::GetForProfile(browser()->GetProfile());
  ASSERT_EQ(expected_active_email(),
            GetActiveContentAreaUser(identity_manager, non_urlf_url));
}

INSTANTIATE_TEST_SUITE_P(,
                         ActiveUserEmailBrowserTest,
                         testing::ValuesIn(TestCases()));

IN_PROC_BROWSER_TEST_P(ActiveFrameUserEmailBrowserTest, GetActiveUserForFrame) {
  active_user_test_mixin_->SetFakeCookieValue();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), tab_url()));
  ASSERT_EQ(expected_active_email(),
            GetActiveFrameUser(
                IdentityManagerFactory::GetForProfile(browser()->GetProfile()),
                tab_url(), frame_url()));
}

INSTANTIATE_TEST_SUITE_P(,
                         ActiveFrameUserEmailBrowserTest,
                         testing::ValuesIn(FrameUserTestCases()));

IN_PROC_BROWSER_TEST_P(ReferrerChainActiveUserEmailBrowserTest,
                       ContentAreaUserProvider) {
  active_user_test_mixin_->SetFakeCookieValue();

  GURL url("https://docs.google.com/");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ASSERT_EQ(ContentAreaUserProvider::GetUser(
                browser()->GetProfile(),
                browser()->tab_strip_model()->GetActiveWebContents(), url),
            expected_active_email());
}

INSTANTIATE_TEST_SUITE_P(,
                         ReferrerChainActiveUserEmailBrowserTest,
                         testing::ValuesIn(ReferrerChainTestCases()));

using DownloadContentAreaUserProviderTest = InProcessBrowserTest;

IN_PROC_BROWSER_TEST_F(DownloadContentAreaUserProviderTest, FrameUrlChain) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate to a page with an existing iframe.
  GURL main_url = embedded_test_server()->GetURL("/iframe.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();

  GURL iframe_url = embedded_test_server()->GetURL("/title1.html");
  content::RenderFrameHost* child_rfh =
      content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0);
  ASSERT_TRUE(child_rfh);

  // Focus the main frame, simulating focus shifting away from the iframe.
  web_contents->Focus();

  // Construct a mock DownloadItem originating from the child frame.
  GURL empty_url;
  std::vector<GURL> empty_url_chain;
  testing::NiceMock<download::MockDownloadItem> mock_download_item;
  ON_CALL(mock_download_item, GetURL())
      .WillByDefault(testing::ReturnRef(empty_url));
  ON_CALL(mock_download_item, GetTabUrl())
      .WillByDefault(testing::ReturnRef(empty_url));
  ON_CALL(mock_download_item, GetUrlChain())
      .WillByDefault(testing::ReturnRef(empty_url_chain));

  content::DownloadItemUtils::AttachInfo(&mock_download_item,
                                         browser()->GetProfile(), web_contents,
                                         child_rfh->GetGlobalId());

  // Initialize the provider, trigger iframe urls collection.
  DownloadContentAreaUserProvider provider(mock_download_item);

  // Verify that the iframe url chain contains the initiating iframe despite
  // main frame focus.
  auto frame_url_chain = provider.frame_url_chain();
  ASSERT_EQ(1u, frame_url_chain.size());
  EXPECT_EQ(iframe_url.spec(), frame_url_chain[0]);
}

}  // namespace enterprise_connectors
