// 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 "third_party/blink/renderer/modules/payments/payment_request_event.h"

#include <utility>

#include "third_party/blink/public/mojom/payments/payment_request.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_address_errors.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_payment_currency_amount.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_payment_details_modifier.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_payment_handler_response.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_payment_item.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_payment_method_data.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_payment_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_payment_request_details_update.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_payment_shipping_option.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/workers/worker_global_scope.h"
#include "third_party/blink/renderer/core/workers/worker_location.h"
#include "third_party/blink/renderer/modules/payments/address_init_type_converter.h"
#include "third_party/blink/renderer/modules/payments/payment_request_respond_with_observer.h"
#include "third_party/blink/renderer/modules/payments/payments_validators.h"
#include "third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h"
#include "third_party/blink/renderer/modules/service_worker/service_worker_window_client.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"

namespace blink {

class PaymentRequestRespondWithFulfill final
    : public ThenCallable<PaymentHandlerResponse,
                          PaymentRequestRespondWithFulfill> {
 public:
  explicit PaymentRequestRespondWithFulfill(
      PaymentRequestRespondWithObserver* observer)
      : observer_(observer) {}

  void Trace(Visitor* visitor) const override {
    visitor->Trace(observer_);
    ThenCallable<PaymentHandlerResponse,
                 PaymentRequestRespondWithFulfill>::Trace(visitor);
  }

  void React(ScriptState* script_state, PaymentHandlerResponse* response) {
    DCHECK(observer_);
    observer_->OnResponseFulfilled(script_state, response);
  }

 private:
  Member<PaymentRequestRespondWithObserver> observer_;
};

class PaymentRequestRespondWithReject final
    : public ThenCallable<IDLAny,
                          PaymentRequestRespondWithReject,
                          IDLPromise<IDLAny>> {
 public:
  explicit PaymentRequestRespondWithReject(
      PaymentRequestRespondWithObserver* observer)
      : observer_(observer) {}

  void Trace(Visitor* visitor) const override {
    visitor->Trace(observer_);
    ThenCallable<IDLAny, PaymentRequestRespondWithReject,
                 IDLPromise<IDLAny>>::Trace(visitor);
  }

  ScriptPromise<IDLAny> React(ScriptState* script_state, ScriptValue value) {
    DCHECK(observer_);
    payments::mojom::blink::PaymentEventResponseType response_type =
        payments::mojom::blink::PaymentEventResponseType::PAYMENT_EVENT_REJECT;

    auto* isolate = script_state->GetIsolate();
    if (V8DOMException::HasInstance(isolate, value.V8Value())) {
      DOMException* dom_exception =
          V8DOMException::ToWrappable(isolate, value.V8Value());
      if (dom_exception && dom_exception->name() == "OperationError") {
        response_type = payments::mojom::blink::PaymentEventResponseType::
            PAYMENT_EVENT_INTERNAL_ERROR;
      }
    }

    observer_->OnResponseRejected(
        mojom::blink::ServiceWorkerResponseError::kPromiseRejected,
        response_type);
    return ScriptPromise<IDLAny>::Reject(script_state, value);
  }

 private:
  Member<PaymentRequestRespondWithObserver> observer_;
};

PaymentRequestEvent* PaymentRequestEvent::Create(
    const AtomicString& type,
    const PaymentRequestEventInit* initializer,
    mojo::PendingRemote<payments::mojom::blink::PaymentHandlerHost> host,
    PaymentRequestRespondWithObserver* respond_with_observer,
    WaitUntilObserver* wait_until_observer,
    ExecutionContext* execution_context) {
  return MakeGarbageCollected<PaymentRequestEvent>(
      type, initializer, std::move(host), respond_with_observer,
      wait_until_observer, execution_context);
}

// TODO(crbug.com/1070871): Use fooOr() in members' initializers.
PaymentRequestEvent::PaymentRequestEvent(
    const AtomicString& type,
    const PaymentRequestEventInit* initializer,
    mojo::PendingRemote<payments::mojom::blink::PaymentHandlerHost> host,
    PaymentRequestRespondWithObserver* respond_with_observer,
    WaitUntilObserver* wait_until_observer,
    ExecutionContext* execution_context)
    : ExtendableEvent(type, initializer, wait_until_observer),
      top_origin_(initializer->hasTopOrigin() ? initializer->topOrigin()
                                              : String()),
      payment_request_origin_(initializer->hasPaymentRequestOrigin()
                                  ? initializer->paymentRequestOrigin()
                                  : String()),
      payment_request_id_(initializer->hasPaymentRequestId()
                              ? initializer->paymentRequestId()
                              : String()),
      method_data_(initializer->hasMethodData()
                       ? initializer->methodData()
                       : HeapVector<Member<PaymentMethodData>>()),
      total_(initializer->hasTotal() ? initializer->total()
                                     : PaymentCurrencyAmount::Create()),
      modifiers_(initializer->hasModifiers()
                     ? initializer->modifiers()
                     : HeapVector<Member<PaymentDetailsModifier>>()),
      instrument_key_(initializer->hasInstrumentKey()
                          ? initializer->instrumentKey()
                          : String()),
      payment_options_(initializer->hasPaymentOptions()
                           ? initializer->paymentOptions()
                           : PaymentOptions::Create()),
      shipping_options_(initializer->hasShippingOptions()
                            ? initializer->shippingOptions()
                            : HeapVector<Member<PaymentShippingOption>>()),
      observer_(respond_with_observer),
      payment_handler_host_(execution_context) {
  if (!host.is_valid())
    return;

  if (execution_context) {
    payment_handler_host_.Bind(
        std::move(host),
        execution_context->GetTaskRunner(TaskType::kMiscPlatformAPI));
    payment_handler_host_.set_disconnect_handler(BindOnce(
        &PaymentRequestEvent::OnHostConnectionError, WrapWeakPersistent(this)));
  }
}

PaymentRequestEvent::~PaymentRequestEvent() = default;

const AtomicString& PaymentRequestEvent::InterfaceName() const {
  return event_interface_names::kPaymentRequestEvent;
}

const String& PaymentRequestEvent::topOrigin() const {
  return top_origin_;
}

const String& PaymentRequestEvent::paymentRequestOrigin() const {
  return payment_request_origin_;
}

const String& PaymentRequestEvent::paymentRequestId() const {
  return payment_request_id_;
}

const HeapVector<Member<PaymentMethodData>>& PaymentRequestEvent::methodData()
    const {
  return method_data_;
}

const ScriptObject PaymentRequestEvent::total(ScriptState* script_state) const {
  return ScriptObject::From(script_state, total_.Get());
}

const HeapVector<Member<PaymentDetailsModifier>>&
PaymentRequestEvent::modifiers() const {
  return modifiers_;
}

const String& PaymentRequestEvent::instrumentKey() const {
  return instrument_key_;
}

const ScriptObject PaymentRequestEvent::paymentOptions(
    ScriptState* script_state) const {
  v8::Isolate* isolate = script_state->GetIsolate();
  if (!payment_options_) {
    return ScriptObject::CreateNull(isolate);
  }
  return ScriptObject::From(script_state, payment_options_.Get());
}

std::optional<HeapVector<Member<PaymentShippingOption>>>
PaymentRequestEvent::shippingOptions() const {
  if (shipping_options_.empty())
    return std::nullopt;
  return shipping_options_;
}

ScriptPromise<IDLNullable<ServiceWorkerWindowClient>>
PaymentRequestEvent::openWindow(ScriptState* script_state, const String& url) {
  auto* resolver = MakeGarbageCollected<
      ScriptPromiseResolver<IDLNullable<ServiceWorkerWindowClient>>>(
      script_state);
  auto promise = resolver->Promise();
  ExecutionContext* context = ExecutionContext::From(script_state);

  if (!isTrusted()) {
    resolver->Reject(MakeGarbageCollected<DOMException>(
        DOMExceptionCode::kInvalidStateError,
        "Cannot open a window when the event is not trusted"));
    return promise;
  }

  KURL parsed_url_to_open = context->CompleteURL(url);
  if (!parsed_url_to_open.IsValid()) {
    resolver->Reject(V8ThrowException::CreateTypeError(
        script_state->GetIsolate(),
        StrCat({"'", url, "' is not a valid URL."})));
    return promise;
  }

  if (!context->GetSecurityOrigin()->IsSameOriginWith(
          SecurityOrigin::Create(parsed_url_to_open).get())) {
    resolver->Resolve(nullptr);
    return promise;
  }

  if (!context->IsWindowInteractionAllowed()) {
    resolver->Reject(MakeGarbageCollected<DOMException>(
        DOMExceptionCode::kNotAllowedError,
        "Not allowed to open a window without user activation"));
    return promise;
  }
  context->ConsumeWindowInteraction();

  To<ServiceWorkerGlobalScope>(context)
      ->GetServiceWorkerHost()
      ->OpenPaymentHandlerWindow(
          parsed_url_to_open,
          ServiceWorkerWindowClient::CreateResolveWindowClientCallback(
              resolver));
  return promise;
}

ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>
PaymentRequestEvent::changePaymentMethod(ScriptState* script_state,
                                         const String& method_name,
                                         const ScriptObject& method_details,
                                         ExceptionState& exception_state) {
  if (change_payment_request_details_resolver_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      "Waiting for response to the previous "
                                      "payment request details change");
    return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }

  if (!payment_handler_host_.is_bound()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "No corresponding PaymentRequest object found");
    return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }

  auto method_data = payments::mojom::blink::PaymentHandlerMethodData::New();
  if (!method_details.IsNull()) {
    PaymentsValidators::ValidateAndStringifyObject(
        script_state->GetIsolate(), method_details,
        method_data->stringified_data, exception_state);
    if (exception_state.HadException())
      return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }

  method_data->method_name = method_name;
  payment_handler_host_->ChangePaymentMethod(
      std::move(method_data),
      BindOnce(&PaymentRequestEvent::OnChangePaymentRequestDetailsResponse,
               WrapWeakPersistent(this)));
  change_payment_request_details_resolver_ = MakeGarbageCollected<
      ScriptPromiseResolver<IDLNullable<PaymentRequestDetailsUpdate>>>(
      script_state);
  return change_payment_request_details_resolver_->Promise();
}

ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>
PaymentRequestEvent::changeShippingAddress(ScriptState* script_state,
                                           AddressInit* shipping_address,
                                           ExceptionState& exception_state) {
  if (change_payment_request_details_resolver_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      "Waiting for response to the previous "
                                      "payment request details change");
    return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }

  if (!payment_handler_host_.is_bound()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "No corresponding PaymentRequest object found");
    return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }
  if (!shipping_address) {
    exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
                                      "Shipping address cannot be null");
    return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }

  auto shipping_address_ptr =
      payments::mojom::blink::PaymentAddress::From(shipping_address);
  String shipping_address_error;
  if (!PaymentsValidators::IsValidShippingAddress(script_state->GetIsolate(),
                                                  shipping_address_ptr,
                                                  &shipping_address_error)) {
    exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
                                      shipping_address_error);
    return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }

  payment_handler_host_->ChangeShippingAddress(
      std::move(shipping_address_ptr),
      BindOnce(&PaymentRequestEvent::OnChangePaymentRequestDetailsResponse,
               WrapWeakPersistent(this)));
  change_payment_request_details_resolver_ = MakeGarbageCollected<
      ScriptPromiseResolver<IDLNullable<PaymentRequestDetailsUpdate>>>(
      script_state);
  return change_payment_request_details_resolver_->Promise();
}

ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>
PaymentRequestEvent::changeShippingOption(ScriptState* script_state,
                                          const String& shipping_option_id,
                                          ExceptionState& exception_state) {
  if (change_payment_request_details_resolver_) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "Waiting for response to the previous payment request details change");
    return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }

  if (!payment_handler_host_.is_bound()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "No corresponding PaymentRequest object found");
    return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }

  bool shipping_option_id_is_valid = false;
  for (const auto& option : shipping_options_) {
    if (option->id() == shipping_option_id) {
      shipping_option_id_is_valid = true;
      break;
    }
  }
  if (!shipping_option_id_is_valid) {
    exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
                                      "Shipping option identifier is invalid");
    return ScriptPromise<IDLNullable<PaymentRequestDetailsUpdate>>();
  }

  payment_handler_host_->ChangeShippingOption(
      shipping_option_id,
      BindOnce(&PaymentRequestEvent::OnChangePaymentRequestDetailsResponse,
               WrapWeakPersistent(this)));
  change_payment_request_details_resolver_ = MakeGarbageCollected<
      ScriptPromiseResolver<IDLNullable<PaymentRequestDetailsUpdate>>>(
      script_state);
  return change_payment_request_details_resolver_->Promise();
}

void PaymentRequestEvent::respondWith(
    ScriptState* script_state,
    ScriptPromise<PaymentHandlerResponse> script_promise,
    ExceptionState& exception_state) {
  if (!isTrusted()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "Cannot respond with data when the event is not trusted");
    return;
  }

  stopImmediatePropagation();
  if (observer_) {
    observer_->RespondWith(
        script_state, script_promise,
        MakeGarbageCollected<PaymentRequestRespondWithFulfill>(observer_),
        MakeGarbageCollected<PaymentRequestRespondWithReject>(observer_),
        exception_state);
  }
}

void PaymentRequestEvent::Trace(Visitor* visitor) const {
  visitor->Trace(method_data_);
  visitor->Trace(total_);
  visitor->Trace(modifiers_);
  visitor->Trace(payment_options_);
  visitor->Trace(shipping_options_);
  visitor->Trace(change_payment_request_details_resolver_);
  visitor->Trace(observer_);
  visitor->Trace(payment_handler_host_);
  ExtendableEvent::Trace(visitor);
}

void PaymentRequestEvent::OnChangePaymentRequestDetailsResponse(
    payments::mojom::blink::PaymentRequestDetailsUpdatePtr response) {
  if (!change_payment_request_details_resolver_)
    return;

  auto* dictionary = MakeGarbageCollected<PaymentRequestDetailsUpdate>();
  if (!response->error.IsNull() && !response->error.empty()) {
    dictionary->setError(response->error);
  }

  if (response->total) {
    auto* total = MakeGarbageCollected<PaymentCurrencyAmount>();
    total->setCurrency(response->total->currency);
    total->setValue(response->total->value);
    dictionary->setTotal(total);
  }

  ScriptState* script_state =
      change_payment_request_details_resolver_->GetScriptState();
  ScriptState::Scope scope(script_state);

  if (response->modifiers) {
    HeapVector<Member<PaymentDetailsModifier>> modifiers;
    for (const auto& response_modifier : *response->modifiers) {
      if (!response_modifier)
        continue;

      auto* mod = MakeGarbageCollected<PaymentDetailsModifier>();
      mod->setSupportedMethod(response_modifier->method_data->method_name);

      if (response_modifier->total) {
        auto* amount = MakeGarbageCollected<PaymentCurrencyAmount>();
        amount->setCurrency(response_modifier->total->currency);
        amount->setValue(response_modifier->total->value);
        auto* total = MakeGarbageCollected<PaymentItem>();
        total->setAmount(amount);
        total->setLabel("");
        mod->setTotal(total);
      }

      if (!response_modifier->method_data->stringified_data.empty()) {
        v8::TryCatch try_catch(script_state->GetIsolate());
        v8::Local<v8::Value> parsed_value = FromJSONString(
            script_state, response_modifier->method_data->stringified_data);
        if (try_catch.HasCaught()) {
          change_payment_request_details_resolver_->Reject(
              try_catch.Exception());
          change_payment_request_details_resolver_.Clear();
          return;
        }
        CHECK(parsed_value->IsObject());
        mod->setData(ScriptObject(script_state->GetIsolate(), parsed_value));
        modifiers.emplace_back(mod);
      }
    }
    dictionary->setModifiers(modifiers);
  }

  if (response->shipping_options) {
    HeapVector<Member<PaymentShippingOption>> shipping_options;
    for (const auto& response_shipping_option : *response->shipping_options) {
      if (!response_shipping_option)
        continue;

      auto* shipping_option = MakeGarbageCollected<PaymentShippingOption>();
      auto* amount = MakeGarbageCollected<PaymentCurrencyAmount>();
      amount->setCurrency(response_shipping_option->amount->currency);
      amount->setValue(response_shipping_option->amount->value);
      shipping_option->setAmount(amount);
      shipping_option->setId(response_shipping_option->id);
      shipping_option->setLabel(response_shipping_option->label);
      shipping_option->setSelected(response_shipping_option->selected);
      shipping_options.emplace_back(shipping_option);
    }
    dictionary->setShippingOptions(shipping_options);
  }

  if (response->stringified_payment_method_errors &&
      !response->stringified_payment_method_errors.empty()) {
    v8::TryCatch try_catch(script_state->GetIsolate());
    v8::Local<v8::Value> parsed_value = FromJSONString(
        script_state, response->stringified_payment_method_errors);
    if (try_catch.HasCaught()) {
      change_payment_request_details_resolver_->Reject(try_catch.Exception());
      change_payment_request_details_resolver_.Clear();
      return;
    }
    CHECK(parsed_value->IsObject());
    dictionary->setPaymentMethodErrors(
        ScriptObject(script_state->GetIsolate(), parsed_value));
  }

  if (response->shipping_address_errors) {
    auto* shipping_address_errors = MakeGarbageCollected<AddressErrors>();
    shipping_address_errors->setAddressLine(
        response->shipping_address_errors->address_line);
    shipping_address_errors->setCity(response->shipping_address_errors->city);
    shipping_address_errors->setCountry(
        response->shipping_address_errors->country);
    shipping_address_errors->setDependentLocality(
        response->shipping_address_errors->dependent_locality);
    shipping_address_errors->setOrganization(
        response->shipping_address_errors->organization);
    shipping_address_errors->setPhone(response->shipping_address_errors->phone);
    shipping_address_errors->setPostalCode(
        response->shipping_address_errors->postal_code);
    shipping_address_errors->setRecipient(
        response->shipping_address_errors->recipient);
    shipping_address_errors->setRegion(
        response->shipping_address_errors->region);
    shipping_address_errors->setSortingCode(
        response->shipping_address_errors->sorting_code);
    dictionary->setShippingAddressErrors(shipping_address_errors);
  }

  change_payment_request_details_resolver_->Resolve(
      dictionary->hasError() || dictionary->hasTotal() ||
              dictionary->hasModifiers() ||
              dictionary->hasPaymentMethodErrors() ||
              dictionary->hasShippingOptions() ||
              dictionary->hasShippingAddressErrors()
          ? dictionary
          : nullptr);
  change_payment_request_details_resolver_.Clear();
}

void PaymentRequestEvent::OnHostConnectionError() {
  if (change_payment_request_details_resolver_) {
    change_payment_request_details_resolver_->Reject(
        MakeGarbageCollected<DOMException>(DOMExceptionCode::kAbortError,
                                           "Browser process disconnected"));
  }
  change_payment_request_details_resolver_.Clear();
  payment_handler_host_.reset();
}

}  // namespace blink
