// 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 <memory>
#include <optional>
#include <ostream>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/types/expected.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/webid/idp_network_request_manager.h"
#include "content/browser/webid/request.h"
#include "content/browser/webid/request_service.h"
#include "content/browser/webid/test/federated_request_token_callback_helper.h"
#include "content/browser/webid/test/mock_api_permission_delegate.h"
#include "content/browser/webid/test/mock_auto_reauthn_permission_delegate.h"
#include "content/browser/webid/test/mock_identity_registry.h"
#include "content/browser/webid/test/mock_identity_request_dialog_controller.h"
#include "content/browser/webid/test/mock_idp_network_request_manager.h"
#include "content/browser/webid/test/mock_modal_dialog_view_delegate.h"
#include "content/browser/webid/test/mock_permission_delegate.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/webid/identity_request_dialog_controller.h"
#include "content/public/common/content_features.h"
#include "content/public/test/navigation_simulator.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/webid/federated_request.mojom.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace content::webid {

using ::testing::NiceMock;
using ApiPermissionStatus =
    FederatedIdentityApiPermissionContextDelegate::PermissionStatus;
using FedCmEntry = ukm::builders::Blink_FedCm;
using FedCmIdpEntry = ukm::builders::Blink_FedCmIdp;
using MediationRequirement = ::password_manager::CredentialMediationRequirement;
using RequestCallbackHelper = FederatedRequestTokenCallbackHelper;
using StartTokenRequestCallback =
    blink::mojom::FederatedRequestService::StartTokenRequestCallback;
using blink::mojom::FederatedRequest;
using blink::mojom::FederatedRequestService;
using blink::mojom::RequestTokenStatus;
using blink::mojom::TokenRequestFailurePtr;
using blink::mojom::TokenRequestSuccessPtr;

namespace {

constexpr char kIdpUrl[] = "https://idp.example/";
constexpr char kProviderUrlFull[] = "https://idp.example/fedcm.json";
constexpr char kProviderUrlTwoFull[] = "https://idp-2.example/fedcm.json";
constexpr char kTopFrameUrl[] = "https://top-frame.example/";
constexpr char kAccountsEndpoint[] = "https://idp.example/accounts";
constexpr char kClientMetadataEndpoint[] = "https://idp.example/clientmd";
constexpr char kTokenEndpoint[] = "https://idp.example/token";
constexpr char kLoginUrl[] = "https://idp.example/login";
constexpr char kClientId[] = "client_id_123";
constexpr char kNonce[] = "nonce123";
constexpr char kAccountId[] = "1234";
constexpr char kToken[] = "[not a real token]";

static std::vector<IdentityRequestAccountPtr> kAccounts;

// IdpNetworkRequestManager which returns valid data from IdP.
class TestIdpNetworkRequestManager : public MockIdpNetworkRequestManager {
 public:
  void SetSendClientIsThirdPartyToTopFrameOrigin(bool value) {
    send_client_is_third_party_to_top_frame_origin_ = value;
  }

  void FetchWellKnown(const GURL& provider,
                      FetchWellKnownCallback callback) override {
    IdpNetworkRequestManager::WellKnown well_known;
    std::set<GURL> well_known_configs;
    well_known_configs.insert(GURL(kProviderUrlFull));
    well_known.provider_urls = std::move(well_known_configs);
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback), kFetchStatusSuccess, well_known));
  }

  void FetchConfig(const GURL& provider,
                   int idp_brand_icon_ideal_size,
                   int idp_brand_icon_minimum_size,
                   FetchConfigCallback callback) override {
    IdpNetworkRequestManager::Endpoints endpoints;
    endpoints.token = GURL(kTokenEndpoint);
    endpoints.accounts = GURL(kAccountsEndpoint);
    if (send_client_is_third_party_to_top_frame_origin_) {
      endpoints.client_metadata = GURL(kClientMetadataEndpoint);
    }

    IdentityProviderMetadata idp_metadata;
    idp_metadata.idp_login_url = GURL(kLoginUrl);
    idp_metadata.config_url = provider;

    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), kFetchStatusSuccess,
                                  endpoints, idp_metadata));
  }

  bool SendAccountsRequest(const url::Origin& idp_origin,
                           const GURL& accounts_url,
                           AccountsRequestCallback callback) override {
    IdpNetworkRequestManager::AccountsResponse response;
    response.accounts = kAccounts;
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), kFetchStatusSuccess,
                                  std::move(response)));
    return true;
  }

  void SendTokenRequest(
      const GURL& token_url,
      const std::string& account,
      const std::string& url_encoded_post_data,
      bool idp_blindness,
      TokenRequestCallback callback,
      ContinueOnCallback continue_on,
      RedirectToCallback redirect_to,
      RecordErrorMetricsCallback record_error_metrics_callback) override {
    TokenResult result;
    result.token = base::Value(kToken);
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), kFetchStatusSuccess,
                                  std::move(result)));
  }

  void FetchClientMetadata(const GURL& endpoint,
                           const std::string& client_id,
                           int rp_brand_icon_ideal_size,
                           int rp_brand_icon_minimum_size,
                           FetchClientMetadataCallback callback) override {
    ClientMetadata client_metadata;
    if (send_client_is_third_party_to_top_frame_origin_) {
      client_metadata.client_is_third_party_to_top_frame_origin = true;
    }
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), kFetchStatusSuccess,
                                  client_metadata));
  }

 private:
  // If true, will send `client_is_third_party_to_top_frame_origin: true` in the
  // client metadata request.
  bool send_client_is_third_party_to_top_frame_origin_{false};
  FetchStatus kFetchStatusSuccess{ParseStatus::kSuccess, net::HTTP_OK};
};

class TestDialogController
    : public NiceMock<MockIdentityRequestDialogController> {
 public:
  struct State {
    bool did_show_accounts_dialog{false};
    std::string rp_for_display;
    std::string iframe_for_display;
  };

  enum class AccountsDialogAction {
    kNone,
    kSelectAccount,
  };

  // `state` is a pointer parameter so that it can outlive TestDialogController.
  TestDialogController(AccountsDialogAction accounts_dialog_action,
                       State* state)
      : accounts_dialog_action_(accounts_dialog_action), state_(state) {}

  ~TestDialogController() override = default;
  TestDialogController(TestDialogController&) = delete;
  TestDialogController& operator=(TestDialogController&) = delete;

  bool ShowAccountsDialog(
      RelyingPartyData rp_data,
      const std::vector<scoped_refptr<IdentityProviderData>>& idp_list,
      const std::vector<scoped_refptr<IdentityRequestAccount>>& accounts,
      const std::vector<scoped_refptr<IdentityRequestAccount>>&
          filtered_accounts,
      blink::mojom::RpMode rp_mode,
      IdentityRequestDialogController::AccountSelectionCallback on_selected,
      IdentityRequestDialogController::LoginToIdPCallback on_add_account,
      IdentityRequestDialogController::DismissCallback dismiss_callback,
      IdentityRequestDialogController::AccountsDisplayedCallback
          accounts_displayed_callback) override {
    std::move(accounts_displayed_callback).Run();
    state_->did_show_accounts_dialog = true;
    state_->rp_for_display = base::UTF16ToUTF8(rp_data.rp_for_display);
    state_->iframe_for_display = base::UTF16ToUTF8(rp_data.iframe_for_display);
    if (accounts_dialog_action_ == AccountsDialogAction::kSelectAccount) {
      std::move(on_selected)
          .Run(GURL(kProviderUrlFull), kAccountId, /*is_sign_in=*/true);
    }
    return true;
  }

 private:
  AccountsDialogAction accounts_dialog_action_{AccountsDialogAction::kNone};
  raw_ptr<State> state_;
};

class TestApiPermissionDelegate : public MockApiPermissionDelegate {
 public:
  ApiPermissionStatus GetApiPermissionStatus(
      const url::Origin& origin) override {
    return ApiPermissionStatus::GRANTED;
  }
};

class TestFederatedIdentityModalDialogViewDelegate
    : public NiceMock<MockModalDialogViewDelegate> {
 public:
  base::WeakPtr<TestFederatedIdentityModalDialogViewDelegate> GetWeakPtr() {
    return weak_ptr_factory_.GetWeakPtr();
  }

 private:
  base::WeakPtrFactory<TestFederatedIdentityModalDialogViewDelegate>
      weak_ptr_factory_{this};
};

}  // namespace

class RequestMultipleFramesTest : public RenderViewHostImplTestHarness {
 protected:
  RequestMultipleFramesTest() = default;
  ~RequestMultipleFramesTest() override = default;

  void SetUp() override {
    RenderViewHostImplTestHarness::SetUp();
    // Initialize `kAccounts` on SetUp() to ensure it is initialized correctly
    // in every test.
    kAccounts = {base::MakeRefCounted<IdentityRequestAccount>(
        kAccountId,                  // id
        "ken@idp.example",           // display_identifier
        "Ken R. Example",            // display_name
        "ken@idp.example",           // email
        "Ken R. Example",            // name
        "Ken",                       // given_name
        GURL(),                      // picture
        "(403) 293-3421",            // phone
        "@kenr",                     // username
        std::vector<std::string>(),  // potentially_approved_site_hashes
        std::vector<std::string>(),  // login_hints
        std::vector<std::string>(),  // domain_hints
        std::vector<std::string>()   // labels
        )};
    test_api_permission_delegate_ =
        std::make_unique<TestApiPermissionDelegate>();
    mock_auto_reauthn_permission_delegate_ =
        std::make_unique<NiceMock<MockAutoReauthnPermissionDelegate>>();
    mock_permission_delegate_ =
        std::make_unique<NiceMock<MockPermissionDelegate>>();
    ON_CALL(*mock_permission_delegate_, OnSetRequiresUserMediation)
        .WillByDefault(base::test::RunOnceCallbackRepeatedly<1>());
    test_modal_dialog_view_delegate_ =
        std::make_unique<TestFederatedIdentityModalDialogViewDelegate>();
    mock_identity_registry_ = std::make_unique<NiceMock<MockIdentityRegistry>>(
        web_contents(), test_modal_dialog_view_delegate_->GetWeakPtr(),
        GURL(kIdpUrl));

    static_cast<TestWebContents*>(web_contents())
        ->NavigateAndCommit(GURL(kTopFrameUrl), ui::PAGE_TRANSITION_LINK);
    ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
  }

  // Does token request and waits for result.
  void DoRequestTokenAndWait(
      mojo::Remote<FederatedRequestService>& service_remote,
      mojo::Remote<FederatedRequest>& request_remote,
      RequestCallbackHelper& callback_helper) {
    DoRequestToken(service_remote, request_remote, callback_helper.callback());
    request_remote.set_disconnect_handler(callback_helper.quit_closure());

    // Ensure that the request makes its way to Request.
    base::RunLoop().RunUntilIdle();
    // Fast forward clock so that the pending
    // Request::OnRejectRequest() task, if any, gets a
    // chance to run.
    task_environment()->FastForwardBy(base::Minutes(10));

    callback_helper.WaitForCallback();
    request_remote.set_disconnect_handler(base::OnceClosure());
  }

  void CreateRequest(
      RenderFrameHost& render_frame_host,
      mojo::Remote<FederatedRequestService>& service_remote,
      TestDialogController::AccountsDialogAction accounts_dialog_action,
      TestDialogController::State* dialog_controller_state,
      TestIdpNetworkRequestManager** out_network_manager = nullptr) {
    RequestService* service =
        RequestService::GetOrCreateForCurrentDocument(&render_frame_host);

    // Inject the mock permission delegates owned by the test fixture!
    service->SetDelegatesForTesting(
        test_api_permission_delegate_.get(),
        mock_auto_reauthn_permission_delegate_.get(),
        mock_permission_delegate_.get(), mock_identity_registry_.get());

    service->BindFederatedRequestService(
        service_remote.BindNewPipeAndPassReceiver());

    service->SetDialogControllerForTests(std::make_unique<TestDialogController>(
        accounts_dialog_action, dialog_controller_state));
    auto network_manager = std::make_unique<TestIdpNetworkRequestManager>();
    if (out_network_manager) {
      *out_network_manager = network_manager.get();
    }
    service->SetNetworkManagerForTests(std::move(network_manager));
  }

  // Creates a child frame with identity-credentials-get PP delegation
  // (equivalent to <iframe allow="identity-credentials-get">).
  TestRenderFrameHost* AppendChildFrameWithIdentityCredentialsGetPolicy(
      const std::string& frame_name) {
    network::ParsedPermissionsPolicy frame_policy(1);
    frame_policy[0].feature =
        network::mojom::PermissionsPolicyFeature::kIdentityCredentialsGet;
    frame_policy[0].matches_all_origins = true;
    frame_policy[0].matches_opaque_src = false;
    return static_cast<TestRenderFrameHost*>(
               web_contents()->GetPrimaryMainFrame())
        ->AppendChildWithPolicy(frame_name, frame_policy);
  }

  void DoRequestToken(mojo::Remote<FederatedRequestService>& service_remote,
                      mojo::Remote<FederatedRequest>& request_remote,
                      StartTokenRequestCallback callback,
                      const char* provider = kProviderUrlFull) {
    auto config_ptr = blink::mojom::IdentityProviderConfig::New();
    config_ptr->config_url = GURL(provider);
    config_ptr->client_id = kClientId;
    auto federated = blink::mojom::IdentityProviderRequestOptions::New();
    federated->config = std::move(config_ptr);
    federated->nonce = kNonce;
    std::vector<blink::mojom::IdentityProviderRequestOptionsPtr> idp_ptrs;
    idp_ptrs.push_back(std::move(federated));
    auto get_params = blink::mojom::IdentityProviderGetParameters::New(
        std::move(idp_ptrs),
        /*rp_context=*/blink::mojom::RpContext::kSignIn,
        /*rp_mode=*/blink::mojom::RpMode::kPassive);
    std::vector<blink::mojom::IdentityProviderGetParametersPtr> idp_get_params;
    idp_get_params.push_back(std::move(get_params));

    service_remote->StartTokenRequest(
        std::move(idp_get_params), MediationRequirement::kOptional,
        request_remote.BindNewPipeAndPassReceiver(), std::move(callback));
    service_remote.FlushForTesting();
  }

  void ExpectUkmValueInEntry(const std::string& metric_name,
                             const char* entry_name,
                             int expected_value) {
    auto entries = ukm_recorder_->GetEntriesByName(entry_name);
    int count = 0;
    for (const ukm::mojom::UkmEntry* const entry : entries) {
      const int64_t* value = ukm_recorder_->GetEntryMetric(entry, metric_name);
      if (!value) {
        continue;
      }
      ++count;
    }
    EXPECT_GT(count, 0) << "Did not find " << metric_name << " in "
                        << entry_name;
  }

  ukm::TestAutoSetUkmRecorder* ukm_recorder() { return ukm_recorder_.get(); }

 protected:
  std::unique_ptr<TestApiPermissionDelegate> test_api_permission_delegate_;
  std::unique_ptr<NiceMock<MockAutoReauthnPermissionDelegate>>
      mock_auto_reauthn_permission_delegate_;
  std::unique_ptr<NiceMock<MockPermissionDelegate>> mock_permission_delegate_;
  std::unique_ptr<TestFederatedIdentityModalDialogViewDelegate>
      test_modal_dialog_view_delegate_;
  std::unique_ptr<NiceMock<MockIdentityRegistry>> mock_identity_registry_;
  std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
};

// Test that test harness can execute successful FedCM flow for iframe.
TEST_F(RequestMultipleFramesTest, TestHarness) {
  RenderFrameHost* iframe_rfh =
      RenderFrameHostTester::For(main_rfh())->AppendChild(/*frame_name=*/"");

  mojo::Remote<FederatedRequestService> iframe_service_remote;
  mojo::Remote<FederatedRequest> iframe_request_remote;
  TestDialogController::State iframe_dialog_state;
  CreateRequest(*iframe_rfh, iframe_service_remote,
                TestDialogController::AccountsDialogAction::kSelectAccount,
                &iframe_dialog_state);

  RequestCallbackHelper iframe_callback_helper;
  DoRequestTokenAndWait(iframe_service_remote, iframe_request_remote,
                        iframe_callback_helper);
  EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
  EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
}

// Test that FedCM request fails on iframe if there is an in-progress FedCM
// request for a different frame on the page.
TEST_F(RequestMultipleFramesTest, IframeTooManyRequests) {
  base::HistogramTester histogram_tester;

  mojo::Remote<FederatedRequestService> main_frame_service_remote;
  mojo::Remote<FederatedRequest> main_frame_request_remote;
  TestDialogController::State main_frame_dialog_state;
  CreateRequest(*main_rfh(), main_frame_service_remote,
                TestDialogController::AccountsDialogAction::kNone,
                &main_frame_dialog_state);
  DoRequestToken(main_frame_service_remote, main_frame_request_remote,
                 StartTokenRequestCallback());
  EXPECT_TRUE(main_frame_dialog_state.did_show_accounts_dialog);

  RenderFrameHost* iframe_rfh =
      RenderFrameHostTester::For(main_rfh())->AppendChild(/*frame_name=*/"");
  mojo::Remote<FederatedRequestService> iframe_service_remote;
  mojo::Remote<FederatedRequest> iframe_request_remote;
  TestDialogController::State iframe_dialog_state;
  CreateRequest(*iframe_rfh, iframe_service_remote,
                TestDialogController::AccountsDialogAction::kSelectAccount,
                &iframe_dialog_state);

  RequestCallbackHelper iframe_callback_helper;
  DoRequestTokenAndWait(iframe_service_remote, iframe_request_remote,
                        iframe_callback_helper);
  EXPECT_EQ(RequestTokenStatus::kErrorTooManyRequests,
            iframe_callback_helper.status());
  EXPECT_FALSE(iframe_dialog_state.did_show_accounts_dialog);
  histogram_tester.ExpectUniqueSample(
      "Blink.FedCm.MultipleRequestsFromDifferentIdPs", 0, 1);
}

// Test that when requests from different IdPs get rejected, a proper histogram
// can be recorded.
TEST_F(RequestMultipleFramesTest, IframeTooManyRequestsDifferentIdP) {
  base::HistogramTester histogram_tester;

  mojo::Remote<FederatedRequestService> main_frame_service_remote;
  mojo::Remote<FederatedRequest> main_frame_request_remote;
  TestDialogController::State main_frame_dialog_state;
  CreateRequest(*main_rfh(), main_frame_service_remote,
                TestDialogController::AccountsDialogAction::kNone,
                &main_frame_dialog_state);
  DoRequestToken(main_frame_service_remote, main_frame_request_remote,
                 StartTokenRequestCallback());
  EXPECT_TRUE(main_frame_dialog_state.did_show_accounts_dialog);

  RenderFrameHost* iframe_rfh =
      RenderFrameHostTester::For(main_rfh())->AppendChild(/*frame_name=*/"");
  mojo::Remote<FederatedRequestService> iframe_service_remote;
  mojo::Remote<FederatedRequest> iframe_request_remote;
  TestDialogController::State iframe_dialog_state;
  CreateRequest(*iframe_rfh, iframe_service_remote,
                TestDialogController::AccountsDialogAction::kSelectAccount,
                &iframe_dialog_state);

  // Initiates a new API call with a different IdP.
  DoRequestToken(iframe_service_remote, iframe_request_remote,
                 StartTokenRequestCallback(), kProviderUrlTwoFull);
  EXPECT_FALSE(iframe_dialog_state.did_show_accounts_dialog);
  histogram_tester.ExpectUniqueSample(
      "Blink.FedCm.MultipleRequestsFromDifferentIdPs", 1, 1);
}

// Test that only top frame URL is available for display when FedCM is called
// within iframes which are same-origin with the top frame.
TEST_F(RequestMultipleFramesTest, SameOriginIframe) {
  base::HistogramTester histogram_tester;

  const char kSameOriginIframeUrl[] = "https://top-frame.example/iframe.html";
  RenderFrameHost* same_origin_iframe =
      NavigationSimulator::NavigateAndCommitFromDocument(
          GURL(kSameOriginIframeUrl),
          RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame())
              ->AppendChild("same_origin_iframe"));

  mojo::Remote<FederatedRequestService> iframe_service_remote;
  mojo::Remote<FederatedRequest> iframe_request_remote;
  TestDialogController::State iframe_dialog_state;
  CreateRequest(*same_origin_iframe, iframe_service_remote,
                TestDialogController::AccountsDialogAction::kSelectAccount,
                &iframe_dialog_state);

  base::RunLoop ukm_loop;
  ukm_recorder()->SetOnAddEntryCallback(FedCmEntry::kEntryName,
                                        ukm_loop.QuitClosure());

  RequestCallbackHelper iframe_callback_helper;
  DoRequestTokenAndWait(iframe_service_remote, iframe_request_remote,
                        iframe_callback_helper);

  ukm_loop.Run();

  EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
  EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
  EXPECT_EQ("top-frame.example", iframe_dialog_state.rp_for_display);

  // Same-origin iframe is treated the same as same-site frame.
  histogram_tester.ExpectUniqueSample(
      "Blink.FedCm.FrameType",
      static_cast<int>(RequesterFrameType::kSameSiteIframe), 1);
  ExpectUkmValueInEntry("FrameType", FedCmEntry::kEntryName,
                        static_cast<int>(RequesterFrameType::kSameSiteIframe));
  ExpectUkmValueInEntry("FrameType", FedCmIdpEntry::kEntryName,
                        static_cast<int>(RequesterFrameType::kSameSiteIframe));
}

// Test that only top frame URL is available for display when FedCM is called
// within iframes which are same-site with the top frame.
TEST_F(RequestMultipleFramesTest, SameSiteIframe) {
  base::HistogramTester histogram_tester;

  // Same-site but cross-origin (subdomain differs), so
  // identity-credentials-get PP delegation is needed because the default
  // allowlist is EnableForSelf (same-origin, not same-site).
  const char kSameSiteIframeUrl[] =
      "https://subdomain.top-frame.example/iframe.html";
  RenderFrameHost* same_site_iframe =
      NavigationSimulator::NavigateAndCommitFromDocument(
          GURL(kSameSiteIframeUrl),
          AppendChildFrameWithIdentityCredentialsGetPolicy("same_site_iframe"));

  mojo::Remote<FederatedRequestService> iframe_service_remote;
  mojo::Remote<FederatedRequest> iframe_request_remote;
  TestDialogController::State iframe_dialog_state;
  CreateRequest(*same_site_iframe, iframe_service_remote,
                TestDialogController::AccountsDialogAction::kSelectAccount,
                &iframe_dialog_state);

  base::RunLoop ukm_loop;
  ukm_recorder()->SetOnAddEntryCallback(FedCmEntry::kEntryName,
                                        ukm_loop.QuitClosure());

  RequestCallbackHelper iframe_callback_helper;
  DoRequestTokenAndWait(iframe_service_remote, iframe_request_remote,
                        iframe_callback_helper);

  ukm_loop.Run();

  EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
  EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
  EXPECT_EQ("top-frame.example", iframe_dialog_state.rp_for_display);

  histogram_tester.ExpectUniqueSample(
      "Blink.FedCm.FrameType",
      static_cast<int>(RequesterFrameType::kSameSiteIframe), 1);
  ExpectUkmValueInEntry("FrameType", FedCmEntry::kEntryName,
                        static_cast<int>(RequesterFrameType::kSameSiteIframe));
  ExpectUkmValueInEntry("FrameType", FedCmIdpEntry::kEntryName,
                        static_cast<int>(RequesterFrameType::kSameSiteIframe));
}

// Test that both top frame and iframe URLs are available for display when FedCM
// is called within iframes which are cross-site with the top frame.
TEST_F(RequestMultipleFramesTest, CrossSiteIframe) {
  base::HistogramTester histogram_tester;

  const char kCrossSiteIframeUrl[] = "https://cross-site.example/iframe.html";
  RenderFrameHost* cross_site_iframe =
      NavigationSimulator::NavigateAndCommitFromDocument(
          GURL(kCrossSiteIframeUrl),
          AppendChildFrameWithIdentityCredentialsGetPolicy(
              "cross_site_iframe"));

  mojo::Remote<FederatedRequestService> iframe_service_remote;
  mojo::Remote<FederatedRequest> iframe_request_remote;
  TestDialogController::State iframe_dialog_state;
  CreateRequest(*cross_site_iframe, iframe_service_remote,
                TestDialogController::AccountsDialogAction::kSelectAccount,
                &iframe_dialog_state);

  base::RunLoop ukm_loop;
  ukm_recorder()->SetOnAddEntryCallback(FedCmEntry::kEntryName,
                                        ukm_loop.QuitClosure());

  RequestCallbackHelper iframe_callback_helper;
  DoRequestTokenAndWait(iframe_service_remote, iframe_request_remote,
                        iframe_callback_helper);

  ukm_loop.Run();

  EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
  EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
  EXPECT_EQ("top-frame.example", iframe_dialog_state.rp_for_display);

  histogram_tester.ExpectUniqueSample(
      "Blink.FedCm.FrameType",
      static_cast<int>(RequesterFrameType::kCrossSiteIframe), 1);
  ExpectUkmValueInEntry("FrameType", FedCmEntry::kEntryName,
                        static_cast<int>(RequesterFrameType::kCrossSiteIframe));
  ExpectUkmValueInEntry("FrameType", FedCmIdpEntry::kEntryName,
                        static_cast<int>(RequesterFrameType::kCrossSiteIframe));
}

// Tests that we send a client metadata request for cross-site iframes even if
// all accounts are returning.
TEST_F(RequestMultipleFramesTest, CrossSiteIframeSendClientMetadata) {
  const char kCrossSiteIframeUrl[] = "https://cross-site.example/iframe.html";
  RenderFrameHost* cross_site_iframe =
      NavigationSimulator::NavigateAndCommitFromDocument(
          GURL(kCrossSiteIframeUrl),
          AppendChildFrameWithIdentityCredentialsGetPolicy(
              "cross_site_iframe"));

  mojo::Remote<FederatedRequestService> iframe_service_remote;
  mojo::Remote<FederatedRequest> iframe_request_remote;
  TestDialogController::State iframe_dialog_state;
  TestIdpNetworkRequestManager* network_manager = nullptr;
  CreateRequest(*cross_site_iframe, iframe_service_remote,
                TestDialogController::AccountsDialogAction::kSelectAccount,
                &iframe_dialog_state, &network_manager);
  network_manager->SetSendClientIsThirdPartyToTopFrameOrigin(true);

  RequestCallbackHelper iframe_callback_helper;
  DoRequestTokenAndWait(iframe_service_remote, iframe_request_remote,
                        iframe_callback_helper);
  EXPECT_EQ(RequestTokenStatus::kSuccess, iframe_callback_helper.status());
  EXPECT_TRUE(iframe_dialog_state.did_show_accounts_dialog);
  EXPECT_EQ("cross-site.example", iframe_dialog_state.iframe_for_display);
}

}  // namespace content::webid
