// Copyright 2017 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 "base/command_line.h"
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/installed_payment_apps_finder.h"
#include "content/public/browser/payment_app_provider.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/payments/payment_app.mojom.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_registration.mojom.h"

namespace content {
namespace {

using ::payments::mojom::CanMakePaymentEventData;
using ::payments::mojom::CanMakePaymentEventDataPtr;
using ::payments::mojom::CanMakePaymentResponsePtr;
using ::payments::mojom::PaymentCurrencyAmount;
using ::payments::mojom::PaymentDetailsModifier;
using ::payments::mojom::PaymentDetailsModifierPtr;
using ::payments::mojom::PaymentHandlerResponsePtr;
using ::payments::mojom::PaymentItem;
using ::payments::mojom::PaymentMethodData;
using ::payments::mojom::PaymentRequestEventData;
using ::payments::mojom::PaymentRequestEventDataPtr;

void OnPaymentAppInstall(base::OnceClosure done_callback,
                         bool* out_success,
                         bool success) {
  *out_success = success;
  std::move(done_callback).Run();
}

void GetAllPaymentAppsCallback(
    base::OnceClosure done_callback,
    InstalledPaymentAppsFinder::PaymentApps* out_apps,
    InstalledPaymentAppsFinder::PaymentApps apps) {
  *out_apps = std::move(apps);
  std::move(done_callback).Run();
}

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

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

void InvokePaymentAppCallback(base::OnceClosure done_callback,
                              PaymentHandlerResponsePtr* out_response,
                              PaymentHandlerResponsePtr response) {
  *out_response = std::move(response);
  std::move(done_callback).Run();
}

}  // namespace

class PaymentAppBrowserTest : public ContentBrowserTest {
 public:
  PaymentAppBrowserTest() {}

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

  ~PaymentAppBrowserTest() override {}

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(
        switches::kEnableExperimentalWebPlatformFeatures);
  }

  void SetUpOnMainThread() override {
    https_server_ = std::make_unique<net::EmbeddedTestServer>(
        net::EmbeddedTestServer::TYPE_HTTPS);
    https_server_->ServeFilesFromSourceDirectory(GetTestDataFilePath());
    ASSERT_TRUE(https_server_->Start());
    ASSERT_TRUE(NavigateToURL(
        shell(),
        https_server_->GetURL("/payments/payment_app_invocation.html")));
    ContentBrowserTest::SetUpOnMainThread();
  }

  std::string RunScript(const std::string& script) {
    return EvalJs(shell()->web_contents(), script).ExtractString();
  }

  std::string PopConsoleString() {
    return RunScript("resultQueue.pop().then(result => String(result))");
  }

  void RegisterPaymentApp() {
    SkBitmap app_icon;
    constexpr int kBitmapDimension = 16;
    app_icon.allocN32Pixels(kBitmapDimension, kBitmapDimension);
    app_icon.eraseColor(SK_ColorRED);
    GURL service_worker_javascript_file_url =
        https_server_->GetURL("/payments/payment_app.js");
    base::RunLoop run_loop;
    bool success = false;
    PaymentAppProvider::GetOrCreateForWebContents(shell()->web_contents())
        ->InstallPaymentAppForTesting(
            app_icon, service_worker_javascript_file_url,
            /*service_worker_scope=*/
            service_worker_javascript_file_url.GetWithoutFilename(),
            /*payment_method_identifier=*/"https://bobpay.com",
            shell()->web_contents()->GetPrimaryMainFrame()->GetGlobalId(),
            base::BindOnce(&OnPaymentAppInstall, run_loop.QuitClosure(),
                           &success));
    run_loop.Run();
    ASSERT_TRUE(success);
  }

  url::Origin GetTestServerOrigin() {
    return url::Origin::Create(https_server_->GetURL("/"));
  }

  std::vector<int64_t> GetAllPaymentAppRegistrationIDs() {
    base::RunLoop run_loop;
    InstalledPaymentAppsFinder::PaymentApps apps;
    InstalledPaymentAppsFinder::GetInstance(
        shell()->web_contents()->GetBrowserContext())
        ->GetAllPaymentApps(base::BindOnce(&GetAllPaymentAppsCallback,
                                           run_loop.QuitClosure(), &apps));
    run_loop.Run();

    std::vector<int64_t> registrationIds;
    for (const auto& app_info : apps) {
      registrationIds.push_back(app_info.second->registration_id);
    }

    return registrationIds;
  }

  bool AbortPayment(int64_t registration_id,
                    const url::Origin& sw_origin,
                    const std::string& payment_request_id) {
    base::RunLoop run_loop;
    bool payment_aborted = false;
    PaymentAppProvider::GetOrCreateForWebContents(shell()->web_contents())
        ->AbortPayment(
            registration_id, sw_origin, payment_request_id,
            base::BindOnce(&CaptureAbortResult, run_loop.QuitClosure(),
                           &payment_aborted));
    run_loop.Run();

    return payment_aborted;
  }

  bool CanMakePaymentWithTestData(int64_t registration_id,
                                  const url::Origin& sw_origin,
                                  const std::string& payment_request_id,
                                  const std::string& supported_method) {
    CanMakePaymentEventDataPtr event_data =
        CreateCanMakePaymentEventData(supported_method);

    base::RunLoop run_loop;
    bool can_make_payment = false;
    PaymentAppProvider::GetOrCreateForWebContents(shell()->web_contents())
        ->CanMakePayment(
            registration_id, sw_origin, payment_request_id,
            std::move(event_data),
            base::BindOnce(&CaptureCanMakePaymentResult, run_loop.QuitClosure(),
                           &can_make_payment));
    run_loop.Run();

    return can_make_payment;
  }

  PaymentHandlerResponsePtr InvokePaymentAppWithTestData(
      int64_t registration_id,
      const url::Origin& sw_origin,
      const std::string& supported_method,
      const std::string& instrument_key) {
    base::RunLoop run_loop;
    PaymentHandlerResponsePtr response;
    PaymentAppProvider::GetOrCreateForWebContents(shell()->web_contents())
        ->InvokePaymentApp(
            registration_id, sw_origin,
            CreatePaymentRequestEventData(supported_method, instrument_key),
            base::BindOnce(&InvokePaymentAppCallback, run_loop.QuitClosure(),
                           &response));
    run_loop.Run();

    return response;
  }

  void ClearStoragePartitionData() {
    // Clear data from the storage partition. Parameters are set to clear data
    // for service workers, for all origins, for an unbounded time range.
    base::RunLoop run_loop;

    static_cast<StoragePartitionImpl*>(shell()
                                           ->web_contents()
                                           ->GetBrowserContext()
                                           ->GetDefaultStoragePartition())
        ->ClearData(StoragePartition::REMOVE_DATA_MASK_SERVICE_WORKERS,
                    blink::StorageKey(), base::Time(), base::Time::Max(),
                    run_loop.QuitClosure());

    run_loop.Run();
  }

 private:
  CanMakePaymentEventDataPtr CreateCanMakePaymentEventData(
      const std::string& supported_method) {
    CanMakePaymentEventDataPtr event_data = CanMakePaymentEventData::New();

    event_data->top_origin = GURL("https://example.test");

    event_data->payment_request_origin = GURL("https://example.test");

    event_data->method_data.push_back(PaymentMethodData::New());
    event_data->method_data[0]->supported_method = supported_method;

    PaymentDetailsModifierPtr modifier = PaymentDetailsModifier::New();
    modifier->total = PaymentItem::New();
    modifier->total->amount = PaymentCurrencyAmount::New();
    modifier->total->amount->currency = "USD";
    modifier->total->amount->value = "55";
    modifier->method_data = PaymentMethodData::New();
    modifier->method_data->supported_method = supported_method;
    event_data->modifiers.push_back(std::move(modifier));

    return event_data;
  }

  PaymentRequestEventDataPtr CreatePaymentRequestEventData(
      const std::string& supported_method,
      const std::string& instrument_key) {
    PaymentRequestEventDataPtr event_data = PaymentRequestEventData::New();

    event_data->top_origin = GURL("https://example.test");

    event_data->payment_request_origin = GURL("https://example.test");

    event_data->payment_request_id = "payment-request-id";

    event_data->method_data.push_back(PaymentMethodData::New());
    event_data->method_data[0]->supported_method = supported_method;

    event_data->total = PaymentCurrencyAmount::New();
    event_data->total->currency = "USD";
    event_data->total->value = "55";

    PaymentDetailsModifierPtr modifier = PaymentDetailsModifier::New();
    modifier->total = PaymentItem::New();
    modifier->total->amount = PaymentCurrencyAmount::New();
    modifier->total->amount->currency = "USD";
    modifier->total->amount->value = "55";
    modifier->method_data = PaymentMethodData::New();
    modifier->method_data->supported_method = supported_method;
    event_data->modifiers.push_back(std::move(modifier));

    event_data->instrument_key = instrument_key;

    return event_data;
  }

  std::unique_ptr<net::EmbeddedTestServer> https_server_;
};

IN_PROC_BROWSER_TEST_F(PaymentAppBrowserTest,
                       AbortPaymentWithInvalidRegistrationId) {
  RegisterPaymentApp();

  std::vector<int64_t> registrationIds = GetAllPaymentAppRegistrationIDs();
  ASSERT_EQ(1U, registrationIds.size());

  bool payment_aborted =
      AbortPayment(blink::mojom::kInvalidServiceWorkerRegistrationId,
                   GetTestServerOrigin(), "id");
  ASSERT_FALSE(payment_aborted);

  ClearStoragePartitionData();
}

IN_PROC_BROWSER_TEST_F(PaymentAppBrowserTest, AbortPayment) {
  RegisterPaymentApp();

  std::vector<int64_t> registrationIds = GetAllPaymentAppRegistrationIDs();
  ASSERT_EQ(1U, registrationIds.size());

  bool payment_aborted =
      AbortPayment(registrationIds[0], GetTestServerOrigin(), "id");
  ASSERT_TRUE(payment_aborted);

  ClearStoragePartitionData();
}

IN_PROC_BROWSER_TEST_F(PaymentAppBrowserTest, CanMakePayment) {
  RegisterPaymentApp();

  std::vector<int64_t> registrationIds = GetAllPaymentAppRegistrationIDs();
  ASSERT_EQ(1U, registrationIds.size());

  bool can_make_payment = CanMakePaymentWithTestData(
      registrationIds[0], GetTestServerOrigin(), "id", "basic-card");
  ASSERT_TRUE(can_make_payment);

  EXPECT_EQ("undefined", PopConsoleString() /* topOrigin */);
  EXPECT_EQ("undefined", PopConsoleString() /* paymentRequestOrigin */);
  EXPECT_EQ("undefined", PopConsoleString() /* methodData */);
  EXPECT_EQ("undefined", PopConsoleString() /* modifiers */);

  ClearStoragePartitionData();
}

IN_PROC_BROWSER_TEST_F(PaymentAppBrowserTest, PaymentAppInvocationAndFailed) {
  RegisterPaymentApp();

  std::vector<int64_t> registrationIds = GetAllPaymentAppRegistrationIDs();
  ASSERT_EQ(1U, registrationIds.size());

  // Remove all payment apps and service workers to cause error.
  ClearStoragePartitionData();

  PaymentHandlerResponsePtr response(
      InvokePaymentAppWithTestData(registrationIds[0], GetTestServerOrigin(),
                                   "basic-card", "basic-card-payment-app-id"));
  ASSERT_EQ("", response->method_name);

  ClearStoragePartitionData();
}

IN_PROC_BROWSER_TEST_F(PaymentAppBrowserTest, PaymentAppInvocation) {
  RegisterPaymentApp();

  std::vector<int64_t> registrationIds = GetAllPaymentAppRegistrationIDs();
  ASSERT_EQ(1U, registrationIds.size());

  PaymentHandlerResponsePtr response(
      InvokePaymentAppWithTestData(registrationIds[0], GetTestServerOrigin(),
                                   "basic-card", "basic-card-payment-app-id"));
  ASSERT_EQ("test", response->method_name);

  EXPECT_EQ("https://example.test/", PopConsoleString() /* topOrigin */);
  EXPECT_EQ("https://example.test/",
            PopConsoleString() /* paymentRequestOrigin */);
  EXPECT_EQ("payment-request-id", PopConsoleString() /* paymentRequestId */);
  EXPECT_EQ("[{\"supportedMethods\":\"basic-card\"}]",
            PopConsoleString() /* methodData */);
  EXPECT_EQ(
      "{\"currency\":\"USD\","
      "\"value\":\"55\"}",
      PopConsoleString() /* total */);
  EXPECT_EQ(
      "[{\"additionalDisplayItems\":[],\"supportedMethods\":\"basic-card\","
      "\"total\":{\"amount\":{\"currency\":\"USD\","
      "\"value\":\"55\"},\"label\":\"\",\"pending\":false}}"
      "]",
      PopConsoleString() /* modifiers */);
  EXPECT_EQ("basic-card-payment-app-id",
            PopConsoleString() /* instrumentKey */);

  ClearStoragePartitionData();

  registrationIds = GetAllPaymentAppRegistrationIDs();
  ASSERT_EQ(0U, registrationIds.size());
}

IN_PROC_BROWSER_TEST_F(PaymentAppBrowserTest, PaymentAppOpenWindowFailed) {
  RegisterPaymentApp();

  std::vector<int64_t> registrationIds = GetAllPaymentAppRegistrationIDs();
  ASSERT_EQ(1U, registrationIds.size());

  PaymentHandlerResponsePtr response(InvokePaymentAppWithTestData(
      registrationIds[0], GetTestServerOrigin(), "https://bobpay.test",
      "bobpay-payment-app-id"));
  // InvokePaymentAppCallback returns empty method_name in case of failure, like
  // in PaymentRequestRespondWithObserver::OnResponseRejected.
  ASSERT_EQ("", response->method_name);

  EXPECT_EQ("https://example.test/", PopConsoleString() /* topOrigin */);
  EXPECT_EQ("https://example.test/",
            PopConsoleString() /* paymentRequestOrigin */);
  EXPECT_EQ("payment-request-id", PopConsoleString() /* paymentRequestId */);
  EXPECT_EQ("[{\"supportedMethods\":\"https://bobpay.test\"}]",
            PopConsoleString() /* methodData */);
  EXPECT_EQ("{\"currency\":\"USD\",\"value\":\"55\"}",
            PopConsoleString() /* total */);
  EXPECT_EQ(
      "[{\"additionalDisplayItems\":[],\"supportedMethods\":\"https://"
      "bobpay.test\","
      "\"total\":{\"amount\":{\"currency\":\"USD\","
      "\"value\":\"55\"},\"label\":\"\",\"pending\":false}}"
      "]",
      PopConsoleString() /* modifiers */);
  EXPECT_EQ("bobpay-payment-app-id", PopConsoleString() /* instrumentKey */);

  ClearStoragePartitionData();

  registrationIds = GetAllPaymentAppRegistrationIDs();
  ASSERT_EQ(0U, registrationIds.size());
}
}  // namespace content
