// 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 "chrome/browser/ui/autofill/payments/desktop_payments_window_manager.h"

#include "base/check_deref.h"
#include "base/functional/callback_helpers.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/autofill/payments/payments_view_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.h"
#include "chrome/browser/ui/navigator/browser_navigator_tab_modal.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "components/autofill/content/browser/content_autofill_client.h"
#include "components/autofill/core/browser/metrics/payments/bnpl_metrics.h"
#include "components/autofill/core/browser/metrics/payments/payments_window_metrics.h"
#include "components/autofill/core/browser/payments/card_unmask_challenge_option.h"
#include "components/autofill/core/browser/payments/payments_autofill_client.h"
#include "components/autofill/core/browser/payments/payments_network_interface.h"
#include "components/autofill/core/browser/payments/payments_requests/unmask_card_request.h"
#include "components/autofill/core/browser/payments/payments_util.h"
#include "components/autofill/core/browser/payments/payments_window_manager_util.h"
#include "components/autofill/core/browser/ui/payments/autofill_progress_ui_type.h"
#include "components/autofill/core/browser/ui/payments/payments_window_user_consent_dialog_controller_impl.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "ui/gfx/geometry/size.h"
#include "url/gurl.h"

namespace autofill::payments {

namespace {

using Vcn3dsFlowEvent = autofill_metrics::Vcn3dsFlowEvent;

gfx::Size GetPopupSizeForVcn3ds() {
  // The first two arguments do not matter as position gets overridden by
  // the tab modal pop-up code. The 600x640 size of the pop-up was decided as
  // the ideal size for user experience. This decision largely factored in how
  // to minimize scrolling while maintaining a presentable pop-up.
  return gfx::Size(/*width=*/600, /*height=*/640);
}

gfx::Size GetPopupSizeForBnpl() {
  // The first two arguments do not matter as position gets overridden by
  // the tab modal pop-up code. The 600x840 size of the pop-up was decided as
  // the ideal size for user experience. This decision largely factored in how
  // to minimize scrolling while maintaining a presentable pop-up.
  return gfx::Size(/*width=*/600, /*height=*/840);
}

}  // namespace

DesktopPaymentsWindowManager::DesktopPaymentsWindowManager(
    ContentAutofillClient* client)
    : client_(CHECK_DEREF(client)) {
#if BUILDFLAG(IS_LINUX)
  scoped_observation_.Observe(
      ProfileBrowserCollection::GetForProfile(Profile::FromBrowserContext(
          client_->GetWebContents().GetBrowserContext())));
#endif  // BUILDFLAG(IS_LINUX)
}

DesktopPaymentsWindowManager::~DesktopPaymentsWindowManager() = default;

void DesktopPaymentsWindowManager::InitVcn3dsAuthentication(
    Vcn3dsContext context) {
  CHECK(!flow_state_.has_value());
  flow_state_ = FlowState();
  CHECK_EQ(context.card.record_type(), CreditCard::RecordType::kVirtualCard);
  CHECK(!context.completion_callback.is_null());

  // The VCN 3DS metadata fields are returned from the Payments server. They
  // must always be present, so that Chrome knows what params to look for on
  // navigation. Since they are outside of Chrome's control, unexpected values
  // must be gracefully handled by displaying an error dialog.
  if (const std::optional<Vcn3dsChallengeOptionMetadata>& metadata =
          context.challenge_option.vcn_3ds_metadata;
      !metadata.has_value() || metadata->url_to_open.is_empty() ||
      !metadata->url_to_open.SchemeIsHTTPOrHTTPS() ||
      metadata->success_query_param_name.empty() ||
      metadata->failure_query_param_name.empty()) {
    client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog(
        AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError(
            /*is_permanent_error=*/false));
    return;
  }

  flow_state_->flow_type = FlowType::kVcn3ds;
  flow_state_->vcn_3ds_context = std::move(context);
  autofill_metrics::LogVcn3dsFlowEvent(
      Vcn3dsFlowEvent::kFlowStarted,
      /*user_consent_already_given=*/flow_state_->vcn_3ds_context
          ->user_consent_already_given);
  if (flow_state_->vcn_3ds_context->user_consent_already_given) {
    autofill_metrics::LogVcn3dsFlowEvent(
        Vcn3dsFlowEvent::kUserConsentDialogSkipped,
        /*user_consent_already_given=*/flow_state_->vcn_3ds_context
            ->user_consent_already_given);
    CreatePopup(flow_state_->vcn_3ds_context->challenge_option.vcn_3ds_metadata
                    ->url_to_open,
                GetPopupSizeForVcn3ds());
  } else {
    ShowVcn3dsConsentDialog();
  }
}

void DesktopPaymentsWindowManager::InitBnplFlow(BnplContext context) {
  CHECK(!flow_state_.has_value());
  flow_state_ = FlowState();

  flow_state_->flow_type = FlowType::kBnpl;
  flow_state_->bnpl_context = std::move(context);
  CreatePopup(flow_state_->bnpl_context->initial_url, GetPopupSizeForBnpl());
  autofill_metrics::LogBnplPopupWindowShown(
      flow_state_->bnpl_context->issuer_id);
}

void DesktopPaymentsWindowManager::DidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  CHECK(flow_state_.has_value());
  flow_state_->most_recent_url_navigation = navigation_handle->GetURL();
  if (flow_state_->flow_type == FlowType::kVcn3ds) {
    OnDidFinishNavigationForVcn3ds();
  } else if (flow_state_->flow_type == FlowType::kBnpl) {
    OnDidFinishNavigationForBnpl();
  }
}

void DesktopPaymentsWindowManager::WebContentsDestroyed() {
  // Accessing the observed web contents should be avoided at this point,
  // because it is unsafe to access during its destruction. Instead, set class
  // variables earlier for context that needs to be known upon web contents
  // destruction. `flow_state_->most_recent_url_navigation` is an example that
  // can be followed.
  // TODO(crbug.com/388088113): Refactor the VCN 3DS flow to not access the
  // observed web contents.
  CHECK(flow_state_.has_value());
  if (flow_state_->flow_type == FlowType::kVcn3ds) {
    OnWebContentsDestroyedForVcn3ds();
  } else if (flow_state_->flow_type == FlowType::kBnpl) {
    TriggerCompletionCallbackAndLogMetricsForBnpl(
        std::move(flow_state_.value()));
    flow_state_.reset();
  }

  if (popup_closed_closure_for_testing_) {
    popup_closed_closure_for_testing_.Run();
    popup_closed_closure_for_testing_.Reset();
  }
}

#if BUILDFLAG(IS_LINUX)
void DesktopPaymentsWindowManager::OnBrowserActivated(
    BrowserWindowInterface* browser) {
  // If there is an ongoing payments window manager pop-up flow, and the
  // original tab's WebContents become active, activate the pop-up's
  // WebContents. This functionality is only required on Linux, as on
  // other desktop platforms the pop-up will always be the top-most browser
  // window due to differences in window management on these platforms.
  if (web_contents()) {
    CHECK(flow_state_.has_value());
    CHECK_NE(flow_state_->flow_type, FlowType::kNoFlow);
    if (browser->GetTabStripModel()->GetActiveWebContents() ==
        &client_->GetWebContents()) {
      web_contents()->GetDelegate()->ActivateContents(web_contents());
    }
  }
}
#endif  // BUILDFLAG(IS_LINUX)

void DesktopPaymentsWindowManager::CreatePopup(const GURL& url,
                                               gfx::Size popup_size) {
  // Create a pop-up window. The created pop-up will not have any relationship
  // to the underlying tab, because `params.opener` is not set. Ensuring the
  // original tab is not a related site instance to the pop-up is critical for
  // security reasons.
  CHECK(flow_state_.has_value());
  content::WebContents& source_contents = client_->GetWebContents();

  if (base::WeakPtr<content::NavigationHandle> navigation_handle =
          BrowserNavigatorTabModal::Show(
              url, source_contents, popup_size,
              base::PassKey<DesktopPaymentsWindowManager>())) {
    switch (flow_state_->flow_type) {
      case FlowType::kVcn3ds:
        flow_state_->vcn_3ds_popup_shown_timestamp = base::TimeTicks::Now();
        break;
      case FlowType::kBnpl:
        flow_state_->bnpl_popup_shown_timestamp = base::TimeTicks::Now();
        break;
      default:
        NOTREACHED();
    }
    content::WebContentsObserver::Observe(navigation_handle->GetWebContents());
  } else {
    if (flow_state_->vcn_3ds_context.has_value()) {
      autofill_metrics::LogVcn3dsFlowEvent(
          Vcn3dsFlowEvent::kPopupNotShown,
          /*user_consent_already_given=*/flow_state_->vcn_3ds_context
              ->user_consent_already_given);
      client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog(
          AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError(
              /*is_permanent_error=*/false));
    } else if (flow_state_->bnpl_context.has_value()) {
      client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog(
          AutofillErrorDialogContext::WithBnplPermanentOrTemporaryError(
              /*is_permanent_error=*/false));
    }
  }
}

void DesktopPaymentsWindowManager::OnDidFinishNavigationForVcn3ds() {
  CHECK(flow_state_.has_value());
  // TODO(crbug.com/388088113): Refactor the VCN 3DS flow to use
  // `flow_state_->most_recent_url_navigation`, similar to the BNPL flow.
  base::expected<RedirectCompletionResult, Vcn3dsAuthenticationResult> result =
      ParseUrlForVcn3ds(web_contents()->GetVisibleURL(),
                        flow_state_->vcn_3ds_context->challenge_option
                            .vcn_3ds_metadata.value());
  if (result.has_value() ||
      result.error() == Vcn3dsAuthenticationResult::kAuthenticationFailed) {
    // To safely close the pop-up during a navigation event, a task must be
    // posted to the current base::SequencedTaskRunner, as the web contents must
    // complete notifying all of its observers of the navigation event before
    // closing. Closing before this has finished can result in a use-after-free.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&content::WebContents::Close,
                                  web_contents()->GetWeakPtr()));
  }
}

void DesktopPaymentsWindowManager::OnDidFinishNavigationForBnpl() {
  CHECK(flow_state_.has_value());
  BnplPopupStatus status =
      ParseUrlForBnpl(flow_state_->most_recent_url_navigation,
                      flow_state_->bnpl_context.value());
  if (status != BnplPopupStatus::kNotFinished) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&content::WebContents::Close,
                                  web_contents()->GetWeakPtr()));
  }
}

void DesktopPaymentsWindowManager::OnWebContentsDestroyedForVcn3ds() {
  CHECK(flow_state_.has_value());
  CHECK(flow_state_->vcn_3ds_popup_shown_timestamp.has_value());
  // TODO(crbug.com/388088113): Refactor the VCN 3DS flow to use
  // `flow_state_->most_recent_url_navigation`, similar to the BNPL flow.
  base::expected<RedirectCompletionResult, Vcn3dsAuthenticationResult> result =
      ParseUrlForVcn3ds(web_contents()->GetVisibleURL(),
                        flow_state_->vcn_3ds_context->challenge_option
                            .vcn_3ds_metadata.value());

  // If the result implies that the authentication inside of the pop-up was
  // successful, continue the flow without resetting.
  if (result.has_value()) {
    CHECK(!result.value()->empty());
    autofill_metrics::LogVcn3dsAuthLatency(
        base::TimeTicks::Now() -
            flow_state_->vcn_3ds_popup_shown_timestamp.value(),
        /*success=*/true);
    client_->GetPaymentsAutofillClient()->ShowAutofillProgressDialog(
        AutofillProgressUiType::k3dsFetchVcnProgressUi,
        base::BindOnce(&DesktopPaymentsWindowManager::
                           OnVcn3dsAuthenticationProgressDialogCancelled,
                       weak_ptr_factory_.GetWeakPtr()));
    return client_->GetPaymentsAutofillClient()->LoadRiskData(base::BindOnce(
        &DesktopPaymentsWindowManager::OnDidLoadRiskDataForVcn3ds,
        weak_ptr_factory_.GetWeakPtr(), std::move(result.value())));
  }

  // If the authentication was known to fail inside of the pop-up (for example,
  // a user retried too many times for the issuer or network's auth mechanism
  // inside of the pop-up browser window), trigger the error dialog. Otherwise,
  // it is assumed that the user manually closed the pop-up, so triggering an
  // error dialog would be a bad user experience. If the Payments Server
  // introduced invalid query parameters on the last redirect, this would fail
  // to handle that correctly, but it is not feasible to distinguish that from
  // the user closing the pop-up.
  if (result.error() == Vcn3dsAuthenticationResult::kAuthenticationFailed) {
    autofill_metrics::LogVcn3dsFlowEvent(
        Vcn3dsFlowEvent::kAuthenticationInsidePopupFailed,
        /*user_consent_already_given=*/flow_state_->vcn_3ds_context
            ->user_consent_already_given);
    autofill_metrics::LogVcn3dsAuthLatency(
        base::TimeTicks::Now() -
            flow_state_->vcn_3ds_popup_shown_timestamp.value(),
        /*success=*/false);
    client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog(
        AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError(
            /*is_permanent_error=*/true));
  } else {
    autofill_metrics::LogVcn3dsFlowEvent(
        Vcn3dsFlowEvent::kFlowCancelledUserClosedPopup,
        /*user_consent_already_given=*/flow_state_->vcn_3ds_context
            ->user_consent_already_given);
  }

  // The callback is always run at this point, which can be either when the user
  // closed the pop-up or an error occurred. This is so that the requester is
  // notified of the flow's completion.
  // TODO(crbug.com/334967738): Check whether the user closed the pop-up window
  // directly once an API for it is built.
  Vcn3dsAuthenticationResponse response;
  response.result = result.error();
  std::move(flow_state_->vcn_3ds_context->completion_callback)
      .Run(std::move(response));
  flow_state_.reset();
}

void DesktopPaymentsWindowManager::OnDidLoadRiskDataForVcn3ds(
    RedirectCompletionResult redirect_completion_result,
    const std::string& risk_data) {
  CHECK(flow_state_.has_value());
  flow_state_->vcn_3ds_context->risk_data = risk_data;
  client_->GetPaymentsAutofillClient()
      ->GetPaymentsNetworkInterface()
      ->UnmaskCard(CreateUnmaskRequestDetailsForVcn3ds(
                       *client_, flow_state_->vcn_3ds_context.value(),
                       std::move(redirect_completion_result)),
                   base::BindOnce(&DesktopPaymentsWindowManager::
                                      OnVcn3dsAuthenticationResponseReceived,
                                  weak_ptr_factory_.GetWeakPtr()));
}

void DesktopPaymentsWindowManager::OnVcn3dsAuthenticationResponseReceived(
    PaymentsAutofillClient::PaymentsRpcResult result,
    const UnmaskResponseDetails& response_details) {
  CHECK(flow_state_.has_value());
  Vcn3dsAuthenticationResponse response =
      CreateVcn3dsAuthenticationResponseFromServerResult(
          result, response_details,
          std::move(flow_state_->vcn_3ds_context->card));
  client_->GetPaymentsAutofillClient()->CloseAutofillProgressDialog(
      /*show_confirmation_before_closing=*/response.card.has_value(),
      /*no_interactive_authentication_callback=*/base::OnceClosure());
  if (!response.card.has_value()) {
    autofill_metrics::LogVcn3dsFlowEvent(
        Vcn3dsFlowEvent::kFlowFailedWhileRetrievingVCN,
        /*user_consent_already_given=*/flow_state_->vcn_3ds_context
            ->user_consent_already_given);
    client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog(
        AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError(
            /*is_permanent_error=*/false));
  }

  autofill_metrics::LogVcn3dsFlowEvent(
      Vcn3dsFlowEvent::kFlowSucceeded,
      /*user_consent_already_given=*/flow_state_->vcn_3ds_context
          ->user_consent_already_given);
  std::move(flow_state_->vcn_3ds_context->completion_callback)
      .Run(std::move(response));
  flow_state_.reset();
}

void DesktopPaymentsWindowManager::
    OnVcn3dsAuthenticationProgressDialogCancelled() {
  CHECK(flow_state_.has_value());
  autofill_metrics::LogVcn3dsFlowEvent(
      Vcn3dsFlowEvent::kProgressDialogCancelled,
      /*user_consent_already_given=*/flow_state_->vcn_3ds_context
          ->user_consent_already_given);
  client_->GetPaymentsAutofillClient()
      ->GetPaymentsNetworkInterface()
      ->CancelRequest();
  // In the case of the dialog cancelled, we still run the callback to let the
  // caller know the flow has finished unsuccessfully.
  Vcn3dsAuthenticationResponse response;
  response.result = Vcn3dsAuthenticationResult::kAuthenticationNotCompleted;
  std::move(flow_state_->vcn_3ds_context->completion_callback)
      .Run(std::move(response));
  flow_state_.reset();
}

void DesktopPaymentsWindowManager::ShowVcn3dsConsentDialog() {
  payments_window_user_consent_dialog_controller_ =
      std::make_unique<PaymentsWindowUserConsentDialogControllerImpl>(
          /*accept_callback=*/base::BindOnce(
              &DesktopPaymentsWindowManager::OnVcn3dsConsentDialogAccepted,
              weak_ptr_factory_.GetWeakPtr()),
          /*cancel_callback=*/base::BindOnce(
              &DesktopPaymentsWindowManager::OnVcn3dsConsentDialogCancelled,
              weak_ptr_factory_.GetWeakPtr()));
  payments_window_user_consent_dialog_controller_->ShowDialog(base::BindOnce(
      &CreateAndShowPaymentsWindowUserConsentDialog,
      payments_window_user_consent_dialog_controller_->GetWeakPtr(),
      base::Unretained(&client_->GetWebContents())));
}

void DesktopPaymentsWindowManager::OnVcn3dsConsentDialogAccepted() {
  CHECK(flow_state_.has_value());
  autofill_metrics::LogVcn3dsFlowEvent(
      Vcn3dsFlowEvent::kUserConsentDialogAccepted,
      /*user_consent_already_given=*/flow_state_->vcn_3ds_context
          ->user_consent_already_given);
  CreatePopup(flow_state_->vcn_3ds_context->challenge_option.vcn_3ds_metadata
                  ->url_to_open,
              GetPopupSizeForVcn3ds());
}

void DesktopPaymentsWindowManager::OnVcn3dsConsentDialogCancelled() {
  CHECK(flow_state_.has_value());
  autofill_metrics::LogVcn3dsFlowEvent(
      Vcn3dsFlowEvent::kUserConsentDialogDeclined,
      /*user_consent_already_given=*/flow_state_->vcn_3ds_context
          ->user_consent_already_given);
  // In the case of the dialog cancelled, we still run the callback to let the
  // caller know the flow has finished unsuccessfully.
  Vcn3dsAuthenticationResponse response;
  response.result = Vcn3dsAuthenticationResult::kAuthenticationNotCompleted;
  std::move(flow_state_->vcn_3ds_context->completion_callback)
      .Run(std::move(response));
  flow_state_.reset();
}

}  // namespace autofill::payments
