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

#include "content/browser/payments/payment_app_provider_impl.h"

#include <cstddef>
#include <string>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/run_until.h"
#include "content/browser/payments/installed_payment_apps_finder_impl.h"
#include "content/browser/payments/payment_app_content_unittest_base.h"
#include "content/browser/payments/payment_app_installer.h"
#include "content/browser/payments/payment_handler_web_contents_observer.h"
#include "content/public/browser/payment_app_provider.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/mock_permission_manager.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_web_contents_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/payments/payment_app.mojom.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h"
#include "url/gurl.h"

MATCHER_P(PermissionTypeMatcher, id, "") {
  return ::testing::Matches(::testing::Eq(id))(
      blink::PermissionDescriptorToPermissionType(arg));
}

namespace content {

class PaymentManager;

namespace {

using ::payments::mojom::PaymentHandlerStatus;

void GetAllPaymentAppsCallback(
    InstalledPaymentAppsFinder::PaymentApps* out_apps,
    InstalledPaymentAppsFinder::PaymentApps apps) {
  *out_apps = std::move(apps);
}

void CaptureCanMakePaymentResult(
    base::OnceClosure callback,
    bool* out_payment_event_result,
    payments::mojom::CanMakePaymentResponsePtr response) {
  *out_payment_event_result = response->can_make_payment;
  std::move(callback).Run();
}

void InvokePaymentAppCallback(
    bool* called,
    payments::mojom::PaymentHandlerResponsePtr response) {
  *called = true;
}

void CaptureInvokePaymentAppResponse(
    bool* called,
    payments::mojom::PaymentHandlerResponsePtr* out_response,
    payments::mojom::PaymentHandlerResponsePtr response) {
  *called = true;
  *out_response = std::move(response);
}

void CaptureAbortResult(base::OnceClosure callback,
                        bool* out_payment_event_result,
                        bool payment_event_result) {
  *out_payment_event_result = payment_event_result;
  std::move(callback).Run();
}

}  // namespace

class PaymentAppProviderTest : public PaymentAppContentUnitTestBase {
 public:
  PaymentAppProviderTest() {
    std::unique_ptr<MockPermissionManager> mock_permission_manager(
        new testing::NiceMock<MockPermissionManager>());
    ON_CALL(*mock_permission_manager,
            GetPermissionResultForOriginWithoutContext(
                PermissionTypeMatcher(blink::PermissionType::PAYMENT_HANDLER),
                testing::_, testing::_))
        .WillByDefault(testing::Return(
            PermissionResult(blink::mojom::PermissionStatus::GRANTED,
                             PermissionStatusSource::UNSPECIFIED)));
    static_cast<TestBrowserContext*>(browser_context())
        ->SetPermissionControllerDelegate(std::move(mock_permission_manager));

    web_contents_ =
        test_web_contents_factory_.CreateWebContents(browser_context());
  }

  PaymentAppProviderTest(const PaymentAppProviderTest&) = delete;
  PaymentAppProviderTest& operator=(const PaymentAppProviderTest&) = delete;

  ~PaymentAppProviderTest() override {}

  void InstallPaymentApp(const GURL& scope,
                         const GURL& sw_url,
                         const std::string& method = "fake-method") {
    int64_t registration_id = RegisterAndActivateServiceWorker(scope, sw_url);
    base::RunLoop run_loop;
    payment_app_context()
        ->payment_app_database()
        ->SetPaymentAppInfoForRegisteredServiceWorker(
            registration_id, scope.spec(), "Test App", /*icon=*/"", method,
            SupportedDelegations(),
            base::BindOnce(
                [](base::OnceClosure quit,
                   payments::mojom::PaymentHandlerStatus status) {
                  EXPECT_EQ(payments::mojom::PaymentHandlerStatus::SUCCESS,
                            status);
                  std::move(quit).Run();
                },
                run_loop.QuitClosure()));
    run_loop.Run();
  }

  void GetAllPaymentApps(
      InstalledPaymentAppsFinder::GetAllPaymentAppsCallback callback) {
    InstalledPaymentAppsFinderImpl::GetInstance(browser_context())
        ->GetAllPaymentApps(std::move(callback));
    base::RunLoop().RunUntilIdle();
  }

  void InvokePaymentApp(int64_t registration_id,
                        const url::Origin& sw_origin,
                        payments::mojom::PaymentRequestEventDataPtr event_data,
                        PaymentAppProvider::InvokePaymentAppCallback callback) {
    PaymentAppProvider::GetOrCreateForWebContents(web_contents_)
        ->InvokePaymentApp(registration_id, sw_origin, std::move(event_data),
                           std::move(callback));
    base::RunLoop().RunUntilIdle();
  }

  void CanMakePayment(int64_t registration_id,
                      const url::Origin& sw_origin,
                      const std::string& payment_request_id,
                      payments::mojom::CanMakePaymentEventDataPtr event_data,
                      PaymentAppProvider::CanMakePaymentCallback callback) {
    PaymentAppProvider::GetOrCreateForWebContents(web_contents_)
        ->CanMakePayment(registration_id, sw_origin, payment_request_id,
                         std::move(event_data), std::move(callback));
  }

  void AbortPayment(int64_t registration_id,
                    const url::Origin& sw_origin,
                    const std::string& payment_request_id,
                    PaymentAppProvider::AbortCallback callback) {
    PaymentAppProvider::GetOrCreateForWebContents(web_contents_)
        ->AbortPayment(registration_id, sw_origin, payment_request_id,
                       std::move(callback));
  }

  void OnClosingOpenedWindow() {
    PaymentAppProvider::GetOrCreateForWebContents(web_contents_)
        ->OnClosingOpenedWindow(payments::mojom::PaymentEventResponseType::
                                    PAYMENT_HANDLER_WINDOW_CLOSING);
    base::RunLoop().RunUntilIdle();
  }

  void SetOpenedWindow() {
    WebContents* payment_handler_web_contents =
        test_web_contents_factory_.CreateWebContents(browser_context());
    PaymentAppProvider::GetOrCreateForWebContents(web_contents_)
        ->SetOpenedWindow(payment_handler_web_contents);
  }

  PaymentHandlerWebContentsObserver* GetPaymentHandlerWebContentsObserver() {
    PaymentAppProvider* provider =
        PaymentAppProvider::GetOrCreateForWebContents(web_contents_);
    return static_cast<PaymentAppProviderImpl*>(provider)
        ->payment_handler_web_contents_observer_.get();
  }

  void SetRegistrationId(int64_t registration_id) {
    PaymentAppProvider* provider =
        PaymentAppProvider::GetOrCreateForWebContents(web_contents_);
    provider->SetRegistrationId(registration_id);
  }

  bool PaymentHandlerDisconnected() {
    PaymentAppProvider* provider =
        PaymentAppProvider::GetOrCreateForWebContents(web_contents_);
    return static_cast<PaymentAppProviderImpl*>(provider)
        ->payment_handler_disconnected_for_test_;
  }

 private:
  TestWebContentsFactory test_web_contents_factory_;
  raw_ptr<WebContents> web_contents_;
};

TEST_F(PaymentAppProviderTest, AbortPaymentTest) {
  InstallPaymentApp(GURL("https://example.test"),
                    GURL("https://example.test/script.js"));

  InstalledPaymentAppsFinder::PaymentApps apps;
  GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));
  ASSERT_EQ(1U, apps.size());

  bool payment_aborted = false;
  base::RunLoop loop;
  AbortPayment(last_sw_registration_id(),
               url::Origin::Create(apps[last_sw_registration_id()]->scope),
               "id",
               base::BindOnce(&CaptureAbortResult, loop.QuitClosure(),
                              &payment_aborted));
  loop.Run();
  ASSERT_TRUE(payment_aborted);
}

TEST_F(PaymentAppProviderTest, CanMakePaymentTest) {
  InstallPaymentApp(GURL("https://example.test"),
                    GURL("https://example.test/script.js"));

  InstalledPaymentAppsFinder::PaymentApps apps;
  GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));
  ASSERT_EQ(1U, apps.size());

  payments::mojom::CanMakePaymentEventDataPtr event_data =
      payments::mojom::CanMakePaymentEventData::New();
  payments::mojom::PaymentMethodDataPtr method_data =
      payments::mojom::PaymentMethodData::New();
  method_data->supported_method = "test-method";
  event_data->method_data.push_back(std::move(method_data));

  bool can_make_payment = false;
  base::RunLoop loop;
  CanMakePayment(last_sw_registration_id(),
                 url::Origin::Create(GURL("https://example.test")), "id",
                 std::move(event_data),
                 base::BindOnce(&CaptureCanMakePaymentResult,
                                loop.QuitClosure(), &can_make_payment));
  loop.Run();
  ASSERT_TRUE(can_make_payment);
}

TEST_F(PaymentAppProviderTest, InvokePaymentAppTest) {
  InstallPaymentApp(GURL("https://hellopay.test/a/"),
                    GURL("https://hellopay.test/a/script.js"));
  InstallPaymentApp(GURL("https://bobpay.test/b/"),
                    GURL("https://bobpay.test/b/script.js"));
  int64_t bobpay_registration_id = last_sw_registration_id();

  InstalledPaymentAppsFinder::PaymentApps apps;
  GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));
  ASSERT_EQ(2U, apps.size());

  EXPECT_EQ(apps[bobpay_registration_id]->scope.spec(),
            "https://bobpay.test/b/");

  payments::mojom::PaymentRequestEventDataPtr event_data =
      payments::mojom::PaymentRequestEventData::New();
  event_data->method_data.push_back(payments::mojom::PaymentMethodData::New());
  event_data->total = payments::mojom::PaymentCurrencyAmount::New();

  bool called = false;
  InvokePaymentApp(bobpay_registration_id,
                   url::Origin::Create(GURL("https://bobpay.test")),
                   std::move(event_data),
                   base::BindOnce(&InvokePaymentAppCallback, &called));
  ASSERT_TRUE(called);
}

TEST_F(PaymentAppProviderTest, GetAllPaymentAppsTest) {
  InstallPaymentApp(GURL("https://hellopay.test/a/"),
                    GURL("https://hellopay.test/a/script.js"), "hellopay");
  int64_t hellopay_registration_id = last_sw_registration_id();
  InstallPaymentApp(GURL("https://bobpay.test/b/"),
                    GURL("https://bobpay.test/b/script.js"), "bobpay");
  int64_t bobpay_registration_id = last_sw_registration_id();

  InstalledPaymentAppsFinder::PaymentApps apps;
  GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));

  ASSERT_EQ(2U, apps.size());
  ASSERT_EQ(1U, apps[hellopay_registration_id]->enabled_methods.size());
  ASSERT_EQ(1U, apps[bobpay_registration_id]->enabled_methods.size());
}

TEST_F(PaymentAppProviderTest, GetAllPaymentAppsFromTheSameOriginTest) {
  InstallPaymentApp(GURL("https://bobpay.test/a/"),
                    GURL("https://bobpay.test/a/script.js"), "hellopay");
  int64_t bobpay_a_registration_id = last_sw_registration_id();
  InstallPaymentApp(GURL("https://bobpay.test/b/"),
                    GURL("https://bobpay.test/b/script.js"), "bobpay");
  int64_t bobpay_b_registration_id = last_sw_registration_id();

  InstalledPaymentAppsFinder::PaymentApps apps;
  GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));

  ASSERT_EQ(2U, apps.size());
  ASSERT_EQ(1U, apps[bobpay_a_registration_id]->enabled_methods.size());
  ASSERT_EQ(1U, apps[bobpay_b_registration_id]->enabled_methods.size());
}

TEST_F(PaymentAppProviderTest, AbortPaymentWhenClosingOpenedWindow) {
  InstallPaymentApp(GURL("https://hellopay.test/a/"),
                    GURL("https://hellopay.test/a/script.js"));
  InstallPaymentApp(GURL("https://bobpay.test/b/"),
                    GURL("https://bobpay.test/b/script.js"));
  int64_t bobpay_registration_id = last_sw_registration_id();

  InstalledPaymentAppsFinder::PaymentApps apps;
  GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));
  ASSERT_EQ(2U, apps.size());

  EXPECT_EQ(apps[bobpay_registration_id]->scope.spec(),
            "https://bobpay.test/b/");

  payments::mojom::PaymentRequestEventDataPtr event_data =
      payments::mojom::PaymentRequestEventData::New();
  event_data->method_data.push_back(payments::mojom::PaymentMethodData::New());
  event_data->total = payments::mojom::PaymentCurrencyAmount::New();

  SetNoPaymentRequestResponseImmediately();

  bool called = false;
  InvokePaymentApp(bobpay_registration_id,
                   url::Origin::Create(GURL("https://bobpay.test")),
                   std::move(event_data),
                   base::BindOnce(&InvokePaymentAppCallback, &called));
  ASSERT_FALSE(called);

  // Abort payment request as closing opened window.
  OnClosingOpenedWindow();
  ASSERT_TRUE(called);

  // Response after abort should not crash and take effect.
  called = false;
  auto response_after_abort = payments::mojom::PaymentHandlerResponse::New();
  response_after_abort->response_type =
      payments::mojom::PaymentEventResponseType::PAYMENT_EVENT_SUCCESS;
  RespondPendingPaymentRequest(std::move(response_after_abort));
  base::RunLoop().RunUntilIdle();
  ASSERT_FALSE(called);
}

TEST_F(PaymentAppProviderTest, InvokePaymentAppInternalErrorTest) {
  InstallPaymentApp(GURL("https://example.test"),
                    GURL("https://example.test/script.js"));

  InstalledPaymentAppsFinder::PaymentApps apps;
  GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback, &apps));
  ASSERT_EQ(1U, apps.size());

  payments::mojom::PaymentRequestEventDataPtr event_data =
      payments::mojom::PaymentRequestEventData::New();
  event_data->method_data.push_back(payments::mojom::PaymentMethodData::New());
  event_data->total = payments::mojom::PaymentCurrencyAmount::New();

  SetNoPaymentRequestResponseImmediately();

  bool called = false;
  payments::mojom::PaymentHandlerResponsePtr response;
  InvokePaymentApp(
      last_sw_registration_id(),
      url::Origin::Create(GURL("https://example.test")), std::move(event_data),
      base::BindOnce(&CaptureInvokePaymentAppResponse, &called, &response));
  ASSERT_FALSE(called);

  auto internal_error_response = payments::mojom::PaymentHandlerResponse::New();
  internal_error_response->response_type =
      payments::mojom::PaymentEventResponseType::PAYMENT_EVENT_INTERNAL_ERROR;
  RespondPendingPaymentRequest(std::move(internal_error_response));

  // TODO(crbug.com/493823429): Replace use of base::test::RunUntil with
  // explicitly waiting for an event.
  EXPECT_TRUE(base::test::RunUntil([&]() { return called; }));
  EXPECT_EQ(
      payments::mojom::PaymentEventResponseType::PAYMENT_EVENT_INTERNAL_ERROR,
      response->response_type);
}

TEST_F(PaymentAppProviderTest, OnPaymentHandlerDisconnectedTest) {
  InstallPaymentApp(GURL("https://example.test"),
                    GURL("https://example.test/script.js"));
  SetRegistrationId(last_sw_registration_id());
  SetOpenedWindow();
  ASSERT_FALSE(PaymentHandlerDisconnected());
  GetPaymentHandlerWebContentsObserver()->PrimaryMainFrameRenderProcessGone(
      base::TERMINATION_STATUS_PROCESS_CRASHED);
  ASSERT_TRUE(PaymentHandlerDisconnected());
}

}  // namespace content
