/*
 * Copyright (C) 2015-2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "ApplePaySession.h"

#if ENABLE(APPLE_PAY)

#include "ApplePayLineItem.h"
#include "ApplePayPaymentAuthorizationResult.h"
#include "ApplePayPaymentAuthorizedEvent.h"
#include "ApplePayPaymentMethodSelectedEvent.h"
#include "ApplePayPaymentMethodUpdate.h"
#include "ApplePayPaymentRequest.h"
#include "ApplePayShippingContactSelectedEvent.h"
#include "ApplePayShippingContactUpdate.h"
#include "ApplePayShippingMethod.h"
#include "ApplePayShippingMethodSelectedEvent.h"
#include "ApplePayShippingMethodUpdate.h"
#include "ApplePayValidateMerchantEvent.h"
#include "DOMWindow.h"
#include "Document.h"
#include "DocumentLoader.h"
#include "EventNames.h"
#include "JSDOMPromiseDeferred.h"
#include "LinkIconCollector.h"
#include "LinkIconType.h"
#include "MainFrame.h"
#include "Page.h"
#include "PageConsoleClient.h"
#include "PaymentAuthorizationStatus.h"
#include "PaymentContact.h"
#include "PaymentCoordinator.h"
#include "PaymentMerchantSession.h"
#include "PaymentMethod.h"
#include "PaymentRequestValidator.h"
#include "SecurityOrigin.h"
#include "Settings.h"
#include "UserGestureIndicator.h"

namespace WebCore {

// The amount follows the regular expression -?[0-9]+(\.[0-9][0-9])?.
static bool validateAmount(const String& amountString)
{
    enum class State {
        Start,
        Sign,
        Digit,
        Dot,
        DotDigit,
        End,
    };

    State state = State::Start;

    for (unsigned i = 0; i < amountString.length(); ++i) {
        UChar c = amountString[i];

        switch (state) {
        case State::Start:
            if (c == '-') {
                state = State::Sign;
                break;
            }

            if (!isASCIIDigit(c))
                return false;
            state = State::Digit;
            break;

        case State::Sign:
            if (!isASCIIDigit(c))
                return false;
            state = State::Digit;
            break;

        case State::Digit:
            if (c == '.') {
                state = State::Dot;
                break;
            }

            if (!isASCIIDigit(c))
                return false;
            break;

        case State::Dot:
            if (!isASCIIDigit(c))
                return false;

            state = State::DotDigit;
            break;

        case State::DotDigit:
            if (!isASCIIDigit(c))
                return false;

            state = State::End;
            break;

        case State::End:
            return false;
        }
    }

    return state == State::Digit || state == State::DotDigit || state == State::End;
}

static ExceptionOr<ApplePaySessionPaymentRequest::LineItem> convertAndValidateTotal(ApplePayLineItem&& lineItem)
{
    if (!validateAmount(lineItem.amount))
        return Exception { TypeError, makeString("\"" + lineItem.amount, "\" is not a valid amount.") };

    ApplePaySessionPaymentRequest::LineItem result;
    result.amount = lineItem.amount;
    result.type = lineItem.type;
    result.label = lineItem.label;

    return WTFMove(result);
}

static ExceptionOr<ApplePaySessionPaymentRequest::LineItem> convertAndValidate(ApplePayLineItem&& lineItem)
{
    ApplePaySessionPaymentRequest::LineItem result;

    // It is OK for pending types to not have an amount.
    if (lineItem.type != ApplePaySessionPaymentRequest::LineItem::Type::Pending) {
        if (!validateAmount(lineItem.amount))
            return Exception { TypeError, makeString("\"" + lineItem.amount, "\" is not a valid amount.") };

        result.amount = lineItem.amount;
    }

    result.type = lineItem.type;
    result.label = lineItem.label;

    return WTFMove(result);
}

static ExceptionOr<Vector<ApplePaySessionPaymentRequest::LineItem>> convertAndValidate(std::optional<Vector<ApplePayLineItem>>&& lineItems)
{
    Vector<ApplePaySessionPaymentRequest::LineItem> result;
    if (!lineItems)
        return WTFMove(result);

    result.reserveInitialCapacity(lineItems->size());

    for (auto lineItem : lineItems.value()) {
        auto convertedLineItem = convertAndValidate(WTFMove(lineItem));
        if (convertedLineItem.hasException())
            return convertedLineItem.releaseException();
        result.uncheckedAppend(convertedLineItem.releaseReturnValue());
    }

    return WTFMove(result);
}

static ExceptionOr<ApplePaySessionPaymentRequest::ShippingMethod> convertAndValidate(ApplePayShippingMethod&& shippingMethod)
{
    if (!validateAmount(shippingMethod.amount))
        return Exception { TypeError, makeString("\"" + shippingMethod.amount, "\" is not a valid amount.") };

    ApplePaySessionPaymentRequest::ShippingMethod result;
    result.amount = shippingMethod.amount;
    result.label = shippingMethod.label;
    result.detail = shippingMethod.detail;
    result.identifier = shippingMethod.identifier;

    return WTFMove(result);
}

static ExceptionOr<Vector<ApplePaySessionPaymentRequest::ShippingMethod>> convertAndValidate(Vector<ApplePayShippingMethod>&& shippingMethods)
{
    Vector<ApplePaySessionPaymentRequest::ShippingMethod> result;
    result.reserveInitialCapacity(shippingMethods.size());

    for (auto& shippingMethod : shippingMethods) {
        auto convertedShippingMethod = convertAndValidate(WTFMove(shippingMethod));
        if (convertedShippingMethod.hasException())
            return convertedShippingMethod.releaseException();
        result.uncheckedAppend(convertedShippingMethod.releaseReturnValue());
    }

    return WTFMove(result);
}

static ExceptionOr<ApplePaySessionPaymentRequest> convertAndValidate(unsigned version, ApplePayPaymentRequest&& paymentRequest, const PaymentCoordinator& paymentCoordinator)
{
    auto convertedRequest = convertAndValidate(version, paymentRequest, paymentCoordinator);
    if (convertedRequest.hasException())
        return convertedRequest.releaseException();

    auto result = convertedRequest.releaseReturnValue();
    result.setRequester(ApplePaySessionPaymentRequest::Requester::ApplePayJS);
    result.setCurrencyCode(paymentRequest.currencyCode);

    auto total = convertAndValidateTotal(WTFMove(paymentRequest.total));
    if (total.hasException())
        return total.releaseException();
    result.setTotal(total.releaseReturnValue());

    auto lineItems = convertAndValidate(WTFMove(paymentRequest.lineItems));
    if (lineItems.hasException())
        return lineItems.releaseException();
    result.setLineItems(lineItems.releaseReturnValue());

    if (paymentRequest.requiredShippingContactFields) {
        auto requiredShippingContactFields = convertAndValidate(version, WTFMove(*paymentRequest.requiredShippingContactFields));
        if (requiredShippingContactFields.hasException())
            return requiredShippingContactFields.releaseException();
        result.setRequiredShippingContactFields(requiredShippingContactFields.releaseReturnValue());
    }

    result.setShippingType(paymentRequest.shippingType);

    if (paymentRequest.shippingMethods) {
        auto shippingMethods = convertAndValidate(WTFMove(*paymentRequest.shippingMethods));
        if (shippingMethods.hasException())
            return shippingMethods.releaseException();
        result.setShippingMethods(shippingMethods.releaseReturnValue());
    }

    // FIXME: Merge this validation into the validation we are doing above.
    auto validatedPaymentRequest = PaymentRequestValidator::validate(result);
    if (validatedPaymentRequest.hasException())
        return validatedPaymentRequest.releaseException();

    return WTFMove(result);
}


static Vector<PaymentError> convert(const Vector<RefPtr<ApplePayError>>& errors)
{
    Vector<PaymentError> convertedErrors;

    for (auto& error : errors) {
        PaymentError convertedError;

        convertedError.code = error->code();
        convertedError.message = error->message();
        convertedError.contactField = error->contactField();

        convertedErrors.append(convertedError);
    }

    return convertedErrors;
}

static ExceptionOr<PaymentAuthorizationResult> convertAndValidate(ApplePayPaymentAuthorizationResult&& result)
{
    PaymentAuthorizationResult convertedResult;

    std::optional<ApplePayError::Code> errorCode;
    std::optional<ApplePayError::ContactField> contactField;

    switch (result.status) {
    case ApplePaySession::STATUS_SUCCESS:
        convertedResult.status = PaymentAuthorizationStatus::Success;
        break;

    case ApplePaySession::STATUS_FAILURE:
        convertedResult.status = PaymentAuthorizationStatus::Failure;
        break;

    case ApplePaySession::STATUS_INVALID_BILLING_POSTAL_ADDRESS:
        convertedResult.status = PaymentAuthorizationStatus::Failure;
        convertedResult.errors.append({ PaymentError::Code::BillingContactInvalid, { }, std::nullopt });
        break;

    case ApplePaySession::STATUS_INVALID_SHIPPING_POSTAL_ADDRESS:
        convertedResult.status = PaymentAuthorizationStatus::Failure;
        convertedResult.errors.append({ PaymentError::Code::ShippingContactInvalid, { }, PaymentError::ContactField::PostalAddress });
        break;

    case ApplePaySession::STATUS_INVALID_SHIPPING_CONTACT:
        convertedResult.status = PaymentAuthorizationStatus::Failure;
        convertedResult.errors.append({ PaymentError::Code::ShippingContactInvalid, { }, std::nullopt });
        break;

    case ApplePaySession::STATUS_PIN_REQUIRED:
        convertedResult.status = PaymentAuthorizationStatus::PINRequired;
        break;

    case ApplePaySession::STATUS_PIN_INCORRECT:
        convertedResult.status = PaymentAuthorizationStatus::PINIncorrect;
        break;

    case ApplePaySession::STATUS_PIN_LOCKOUT:
        convertedResult.status = PaymentAuthorizationStatus::PINLockout;
        break;

    default:
        return Exception { InvalidAccessError };
    }

    convertedResult.errors.appendVector(convert(result.errors));

    return WTFMove(convertedResult);
}

static ExceptionOr<PaymentMethodUpdate> convertAndValidate(ApplePayPaymentMethodUpdate&& update)
{
    PaymentMethodUpdate convertedUpdate;

    auto convertedNewTotal = convertAndValidateTotal(WTFMove(update.newTotal));
    if (convertedNewTotal.hasException())
        return convertedNewTotal.releaseException();
    convertedUpdate.newTotalAndLineItems.total = convertedNewTotal.releaseReturnValue();

    // FIXME: Merge this validation into the validation we are doing above.
    auto validatedTotal = PaymentRequestValidator::validateTotal(convertedUpdate.newTotalAndLineItems.total);
    if (validatedTotal.hasException())
        return validatedTotal.releaseException();

    auto convertedNewLineItems = convertAndValidate(WTFMove(update.newLineItems));
    if (convertedNewLineItems.hasException())
        return convertedNewLineItems.releaseException();

    convertedUpdate.newTotalAndLineItems.lineItems = convertedNewLineItems.releaseReturnValue();

    return WTFMove(convertedUpdate);
}

static ExceptionOr<ShippingContactUpdate> convertAndValidate(ApplePayShippingContactUpdate&& update)
{
    ShippingContactUpdate convertedUpdate;

    convertedUpdate.errors = convert(update.errors);

    auto convertedNewShippingMethods = convertAndValidate(WTFMove(update.newShippingMethods));
    if (convertedNewShippingMethods.hasException())
        return convertedNewShippingMethods.releaseException();
    convertedUpdate.newShippingMethods = convertedNewShippingMethods.releaseReturnValue();

    auto convertedNewTotal = convertAndValidateTotal(WTFMove(update.newTotal));
    if (convertedNewTotal.hasException())
        return convertedNewTotal.releaseException();
    convertedUpdate.newTotalAndLineItems.total = convertedNewTotal.releaseReturnValue();

    // FIXME: Merge this validation into the validation we are doing above.
    auto validatedTotal = PaymentRequestValidator::validateTotal(convertedUpdate.newTotalAndLineItems.total);
    if (validatedTotal.hasException())
        return validatedTotal.releaseException();

    auto convertedNewLineItems = convertAndValidate(WTFMove(update.newLineItems));
    if (convertedNewLineItems.hasException())
        return convertedNewLineItems.releaseException();
    convertedUpdate.newTotalAndLineItems.lineItems = convertedNewLineItems.releaseReturnValue();

    return WTFMove(convertedUpdate);
}

static ExceptionOr<ShippingMethodUpdate> convertAndValidate(ApplePayShippingMethodUpdate&& update)
{
    ShippingMethodUpdate convertedUpdate;

    auto convertedNewTotal = convertAndValidateTotal(WTFMove(update.newTotal));
    if (convertedNewTotal.hasException())
        return convertedNewTotal.releaseException();

    convertedUpdate.newTotalAndLineItems.total = convertedNewTotal.releaseReturnValue();

    // FIXME: Merge this validation into the validation we are doing above.
    auto validatedTotal = PaymentRequestValidator::validateTotal(convertedUpdate.newTotalAndLineItems.total);
    if (validatedTotal.hasException())
        return validatedTotal.releaseException();

    auto convertedNewLineItems = convertAndValidate(WTFMove(update.newLineItems));
    if (convertedNewLineItems.hasException())
        return convertedNewLineItems.releaseException();

    convertedUpdate.newTotalAndLineItems.lineItems = convertedNewLineItems.releaseReturnValue();

    return WTFMove(convertedUpdate);
}

ExceptionOr<Ref<ApplePaySession>> ApplePaySession::create(Document& document, unsigned version, ApplePayPaymentRequest&& paymentRequest)
{
    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    if (!UserGestureIndicator::processingUserGesture())
        return Exception { InvalidAccessError, "Must create a new ApplePaySession from a user gesture handler." };

    auto& paymentCoordinator = document.frame()->mainFrame().paymentCoordinator();

    if (!version || !paymentCoordinator.supportsVersion(version))
        return Exception { InvalidAccessError, makeString("\"" + String::number(version), "\" is not a supported version.") };

    auto convertedPaymentRequest = convertAndValidate(version, WTFMove(paymentRequest), paymentCoordinator);
    if (convertedPaymentRequest.hasException())
        return convertedPaymentRequest.releaseException();

    return adoptRef(*new ApplePaySession(document, version, convertedPaymentRequest.releaseReturnValue()));
}

ApplePaySession::ApplePaySession(Document& document, unsigned version, ApplePaySessionPaymentRequest&& paymentRequest)
    : ActiveDOMObject { &document }
    , m_paymentRequest { WTFMove(paymentRequest) }
    , m_version { version }
{
    ASSERT(document.frame()->mainFrame().paymentCoordinator().supportsVersion(version));
    suspendIfNeeded();
}

ApplePaySession::~ApplePaySession() = default;

ExceptionOr<bool> ApplePaySession::supportsVersion(ScriptExecutionContext& scriptExecutionContext, unsigned version)
{
    if (!version)
        return Exception { InvalidAccessError };

    auto& document = downcast<Document>(scriptExecutionContext);

    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    return document.frame()->mainFrame().paymentCoordinator().supportsVersion(version);
}

static bool shouldDiscloseApplePayCapability(Document& document)
{
    auto* page = document.page();
    if (!page || page->usesEphemeralSession())
        return false;

    return document.settings().applePayCapabilityDisclosureAllowed();
}

ExceptionOr<bool> ApplePaySession::canMakePayments(ScriptExecutionContext& scriptExecutionContext)
{
    auto& document = downcast<Document>(scriptExecutionContext);

    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    return document.frame()->mainFrame().paymentCoordinator().canMakePayments();
}

ExceptionOr<void> ApplePaySession::canMakePaymentsWithActiveCard(ScriptExecutionContext& scriptExecutionContext, const String& merchantIdentifier, Ref<DeferredPromise>&& passedPromise)
{
    auto& document = downcast<Document>(scriptExecutionContext);

    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    RefPtr<DeferredPromise> promise(WTFMove(passedPromise));
    if (!shouldDiscloseApplePayCapability(document)) {
        auto& paymentCoordinator = document.frame()->mainFrame().paymentCoordinator();
        bool canMakePayments = paymentCoordinator.canMakePayments();

        RunLoop::main().dispatch([promise, canMakePayments]() mutable {
            promise->resolve<IDLBoolean>(canMakePayments);
        });
        return { };
    }

    auto& paymentCoordinator = document.frame()->mainFrame().paymentCoordinator();

    paymentCoordinator.canMakePaymentsWithActiveCard(merchantIdentifier, document.domain(), [promise](bool canMakePayments) mutable {
        promise->resolve<IDLBoolean>(canMakePayments);
    });
    return { };
}

ExceptionOr<void> ApplePaySession::openPaymentSetup(ScriptExecutionContext& scriptExecutionContext, const String& merchantIdentifier, Ref<DeferredPromise>&& passedPromise)
{
    auto& document = downcast<Document>(scriptExecutionContext);

    auto canCall = canCreateSession(document);
    if (canCall.hasException())
        return canCall.releaseException();

    if (!UserGestureIndicator::processingUserGesture())
        return Exception { InvalidAccessError, "Must call ApplePaySession.openPaymentSetup from a user gesture handler." };

    RefPtr<DeferredPromise> promise(WTFMove(passedPromise));
    auto& paymentCoordinator = document.frame()->mainFrame().paymentCoordinator();

    paymentCoordinator.openPaymentSetup(merchantIdentifier, document.domain(), [promise](bool result) mutable {
        promise->resolve<IDLBoolean>(result);
    });

    return { };
}

ExceptionOr<void> ApplePaySession::begin()
{
    if (!canBegin())
        return Exception { InvalidAccessError, "Payment session is already active." };

    if (paymentCoordinator().hasActiveSession())
        return Exception { InvalidAccessError, "Page already has an active payment session." };

    auto& document = *downcast<Document>(scriptExecutionContext());

    Vector<URL> linkIconURLs;
    for (auto& icon : LinkIconCollector { document }.iconsOfTypes({ LinkIconType::TouchIcon, LinkIconType::TouchPrecomposedIcon }))
        linkIconURLs.append(icon.url);

    if (!paymentCoordinator().beginPaymentSession(*this, document.url(), linkIconURLs, m_paymentRequest))
        return Exception { InvalidAccessError, "There is already has an active payment session." };

    m_state = State::Active;

    setPendingActivity(this);

    return { };
}

ExceptionOr<void> ApplePaySession::abort()
{
    if (!canAbort())
        return Exception { InvalidAccessError };

    m_state = State::Aborted;
    paymentCoordinator().abortPaymentSession();

    didReachFinalState();

    return { };
}

ExceptionOr<void> ApplePaySession::completeMerchantValidation(JSC::ExecState& state, JSC::JSValue merchantSessionValue)
{
    if (!canCompleteMerchantValidation())
        return Exception { InvalidAccessError };

    if (!merchantSessionValue.isObject())
        return Exception { TypeError };

    auto& document = *downcast<Document>(scriptExecutionContext());
    auto& window = *document.domWindow();

    String errorMessage;
    auto merchantSession = PaymentMerchantSession::fromJS(state, asObject(merchantSessionValue), errorMessage);
    if (!merchantSession) {
        window.printErrorMessage(errorMessage);
        return Exception { InvalidAccessError };
    }

    m_merchantValidationState = MerchantValidationState::ValidationComplete;
    paymentCoordinator().completeMerchantValidation(*merchantSession);

    return { };
}

ExceptionOr<void> ApplePaySession::completeShippingMethodSelection(ApplePayShippingMethodUpdate&& update)
{
    if (!canCompleteShippingMethodSelection())
        return Exception { InvalidAccessError };

    auto convertedUpdate = convertAndValidate(WTFMove(update));
    if (convertedUpdate.hasException())
        return convertedUpdate.releaseException();

    m_state = State::Active;
    paymentCoordinator().completeShippingMethodSelection(convertedUpdate.releaseReturnValue());

    return { };
}

ExceptionOr<void> ApplePaySession::completeShippingContactSelection(ApplePayShippingContactUpdate&& update)
{
    if (!canCompleteShippingContactSelection())
        return Exception { InvalidAccessError };

    auto convertedUpdate = convertAndValidate(WTFMove(update));
    if (convertedUpdate.hasException())
        return convertedUpdate.releaseException();

    m_state = State::Active;
    paymentCoordinator().completeShippingContactSelection(convertedUpdate.releaseReturnValue());

    return { };
}

ExceptionOr<void> ApplePaySession::completePaymentMethodSelection(ApplePayPaymentMethodUpdate&& update)
{
    if (!canCompletePaymentMethodSelection())
        return Exception { InvalidAccessError };

    auto convertedUpdate = convertAndValidate(WTFMove(update));
    if (convertedUpdate.hasException())
        return convertedUpdate.releaseException();

    m_state = State::Active;
    paymentCoordinator().completePaymentMethodSelection(convertedUpdate.releaseReturnValue());

    return { };
}

ExceptionOr<void> ApplePaySession::completePayment(ApplePayPaymentAuthorizationResult&& result)
{
    if (!canCompletePayment())
        return Exception { InvalidAccessError };

    auto convertedResultOrException = convertAndValidate(WTFMove(result));
    if (convertedResultOrException.hasException())
        return convertedResultOrException.releaseException();

    auto&& convertedResult = convertedResultOrException.releaseReturnValue();
    bool isFinalState = isFinalStateResult(convertedResult);

    paymentCoordinator().completePaymentSession(WTFMove(convertedResult));

    if (!isFinalState) {
        m_state = State::Active;
        return { };
    }

    m_state = State::Completed;
    unsetPendingActivity(this);

    return { };
}

ExceptionOr<void> ApplePaySession::completeShippingMethodSelection(unsigned short status, ApplePayLineItem&& newTotal, Vector<ApplePayLineItem>&& newLineItems)
{
    ApplePayShippingMethodUpdate update;

    switch (status) {
    case ApplePaySession::STATUS_SUCCESS:
        break;

    case ApplePaySession::STATUS_FAILURE:
    case ApplePaySession::STATUS_INVALID_BILLING_POSTAL_ADDRESS:
    case ApplePaySession::STATUS_INVALID_SHIPPING_POSTAL_ADDRESS:
    case ApplePaySession::STATUS_INVALID_SHIPPING_CONTACT:
    case ApplePaySession::STATUS_PIN_REQUIRED:
    case ApplePaySession::STATUS_PIN_INCORRECT:
    case ApplePaySession::STATUS_PIN_LOCKOUT:
        // This is a fatal error. Cancel the request.
        m_state = State::CancelRequested;
        paymentCoordinator().cancelPaymentSession();
        return { };

    default:
        return Exception { InvalidAccessError };
    }

    update.newTotal = WTFMove(newTotal);
    update.newLineItems = WTFMove(newLineItems);

    return completeShippingMethodSelection(WTFMove(update));
}

ExceptionOr<void> ApplePaySession::completeShippingContactSelection(unsigned short status, Vector<ApplePayShippingMethod>&& newShippingMethods, ApplePayLineItem&& newTotal, Vector<ApplePayLineItem>&& newLineItems)
{
    ApplePayShippingContactUpdate update;

    std::optional<ApplePayError::Code> errorCode;
    std::optional<ApplePayError::ContactField> contactField;

    switch (status) {
    case ApplePaySession::STATUS_SUCCESS:
        break;

    case ApplePaySession::STATUS_FAILURE:
    case ApplePaySession::STATUS_PIN_REQUIRED:
    case ApplePaySession::STATUS_PIN_INCORRECT:
    case ApplePaySession::STATUS_PIN_LOCKOUT:
        errorCode = ApplePayError::Code::Unknown;
        break;

    case ApplePaySession::STATUS_INVALID_BILLING_POSTAL_ADDRESS:
        errorCode = ApplePayError::Code::BillingContactInvalid;
        break;

    case ApplePaySession::STATUS_INVALID_SHIPPING_POSTAL_ADDRESS:
        errorCode = ApplePayError::Code::ShippingContactInvalid;
        contactField = ApplePayError::ContactField::PostalAddress;
        break;

    case ApplePaySession::STATUS_INVALID_SHIPPING_CONTACT:
        errorCode = ApplePayError::Code::ShippingContactInvalid;
        break;

    default:
        return Exception { InvalidAccessError };
    }

    if (errorCode)
        update.errors = { ApplePayError::create(*errorCode, contactField, { }) };

    update.newShippingMethods = WTFMove(newShippingMethods);
    update.newTotal = WTFMove(newTotal);
    update.newLineItems = WTFMove(newLineItems);

    return completeShippingContactSelection(WTFMove(update));
}

ExceptionOr<void> ApplePaySession::completePaymentMethodSelection(ApplePayLineItem&& newTotal, Vector<ApplePayLineItem>&& newLineItems)
{
    ApplePayPaymentMethodUpdate update;

    update.newTotal = WTFMove(newTotal);
    update.newLineItems = WTFMove(newLineItems);

    return completePaymentMethodSelection(WTFMove(update));
}

ExceptionOr<void> ApplePaySession::completePayment(unsigned short status)
{
    ApplePayPaymentAuthorizationResult result;
    result.status = status;

    return completePayment(WTFMove(result));
}

unsigned ApplePaySession::version() const
{
    return m_version;
}

void ApplePaySession::validateMerchant(const URL& validationURL)
{
    if (m_state == State::Aborted) {
        // ApplePaySession::abort has been called.
        return;
    }

    ASSERT(m_merchantValidationState == MerchantValidationState::Idle);
    ASSERT(m_state == State::Active);

    if (validationURL.isNull()) {
        // Something went wrong when getting the validation URL.
        // FIXME: Maybe we should send an error event here instead?
        return;
    }

    m_merchantValidationState = MerchantValidationState::ValidatingMerchant;

    auto event = ApplePayValidateMerchantEvent::create(eventNames().validatemerchantEvent, validationURL);
    dispatchEvent(event.get());
}

void ApplePaySession::didAuthorizePayment(const Payment& payment)
{
    ASSERT(m_state == State::Active);

    m_state = State::Authorized;

    auto event = ApplePayPaymentAuthorizedEvent::create(eventNames().paymentauthorizedEvent, version(), payment);
    dispatchEvent(event.get());
}

void ApplePaySession::didSelectShippingMethod(const ApplePaySessionPaymentRequest::ShippingMethod& shippingMethod)
{
    ASSERT(m_state == State::Active);

    if (!hasEventListeners(eventNames().shippingmethodselectedEvent)) {
        paymentCoordinator().completeShippingMethodSelection({ });
        return;
    }

    m_state = State::ShippingMethodSelected;
    auto event = ApplePayShippingMethodSelectedEvent::create(eventNames().shippingmethodselectedEvent, shippingMethod);
    dispatchEvent(event.get());
}

void ApplePaySession::didSelectShippingContact(const PaymentContact& shippingContact)
{
    ASSERT(m_state == State::Active);

    if (!hasEventListeners(eventNames().shippingcontactselectedEvent)) {
        paymentCoordinator().completeShippingContactSelection({ });
        return;
    }

    m_state = State::ShippingContactSelected;
    auto event = ApplePayShippingContactSelectedEvent::create(eventNames().shippingcontactselectedEvent, version(), shippingContact);
    dispatchEvent(event.get());
}

void ApplePaySession::didSelectPaymentMethod(const PaymentMethod& paymentMethod)
{
    ASSERT(m_state == State::Active);

    if (!hasEventListeners(eventNames().paymentmethodselectedEvent)) {
        paymentCoordinator().completePaymentMethodSelection({ });
        return;
    }

    m_state = State::PaymentMethodSelected;
    auto event = ApplePayPaymentMethodSelectedEvent::create(eventNames().paymentmethodselectedEvent, paymentMethod);
    dispatchEvent(event.get());
}

void ApplePaySession::didCancelPaymentSession()
{
    ASSERT(canCancel());

    m_state = State::Canceled;

    auto event = Event::create(eventNames().cancelEvent, false, false);
    dispatchEvent(event.get());

    didReachFinalState();
}

const char* ApplePaySession::activeDOMObjectName() const
{
    return "ApplePaySession";
}

bool ApplePaySession::canSuspendForDocumentSuspension() const
{
    switch (m_state) {
    case State::Idle:
    case State::Aborted:
    case State::Completed:
    case State::Canceled:
        return true;

    case State::Active:
    case State::Authorized:
    case State::ShippingMethodSelected:
    case State::ShippingContactSelected:
    case State::PaymentMethodSelected:
    case State::CancelRequested:
        return false;
    }
}

void ApplePaySession::stop()
{
    if (!canAbort())
        return;

    m_state = State::Aborted;
    paymentCoordinator().abortPaymentSession();

    didReachFinalState();
}

PaymentCoordinator& ApplePaySession::paymentCoordinator() const
{
    return downcast<Document>(*scriptExecutionContext()).frame()->mainFrame().paymentCoordinator();
}

bool ApplePaySession::canBegin() const
{
    switch (m_state) {
    case State::Idle:
        return true;

    case State::Active:
    case State::Aborted:
    case State::Authorized:
    case State::Completed:
    case State::Canceled:
    case State::ShippingMethodSelected:
    case State::ShippingContactSelected:
    case State::PaymentMethodSelected:
    case State::CancelRequested:
        return false;
    }
}

bool ApplePaySession::canAbort() const
{
    switch (m_state) {
    case State::Idle:
    case State::Aborted:
    case State::Completed:
    case State::Canceled:
        return false;

    case State::Active:
    case State::Authorized:
    case State::ShippingMethodSelected:
    case State::ShippingContactSelected:
    case State::PaymentMethodSelected:
    case State::CancelRequested:
        return true;
    }
}

bool ApplePaySession::canCancel() const
{
    switch (m_state) {
    case State::Idle:
    case State::Aborted:
    case State::Completed:
    case State::Canceled:
        return false;

    case State::Active:
    case State::Authorized:
    case State::ShippingMethodSelected:
    case State::ShippingContactSelected:
    case State::PaymentMethodSelected:
    case State::CancelRequested:
        return true;
    }
}

bool ApplePaySession::canCompleteMerchantValidation() const
{
    if (m_state != State::Active)
        return false;

    if (m_merchantValidationState != MerchantValidationState::ValidatingMerchant)
        return false;

    return true;
}

bool ApplePaySession::canCompleteShippingMethodSelection() const
{
    switch (m_state) {
    case State::Idle:
    case State::Aborted:
    case State::Active:
    case State::Completed:
    case State::Canceled:
    case State::Authorized:
    case State::PaymentMethodSelected:
    case State::ShippingContactSelected:
    case State::CancelRequested:
        return false;

    case State::ShippingMethodSelected:
        return true;
    }
}

bool ApplePaySession::canCompleteShippingContactSelection() const
{
    switch (m_state) {
    case State::Idle:
    case State::Aborted:
    case State::Active:
    case State::Completed:
    case State::Canceled:
    case State::Authorized:
    case State::PaymentMethodSelected:
    case State::ShippingMethodSelected:
    case State::CancelRequested:
        return false;

    case State::ShippingContactSelected:
        return true;
    }
}

bool ApplePaySession::canCompletePaymentMethodSelection() const
{
    switch (m_state) {
    case State::Idle:
    case State::Aborted:
    case State::Active:
    case State::Completed:
    case State::Canceled:
    case State::Authorized:
    case State::ShippingMethodSelected:
    case State::ShippingContactSelected:
    case State::CancelRequested:
        return false;

    case State::PaymentMethodSelected:
        return true;
    }
}

bool ApplePaySession::canCompletePayment() const
{
    switch (m_state) {
    case State::Idle:
    case State::Aborted:
    case State::Active:
    case State::Completed:
    case State::Canceled:
    case State::ShippingMethodSelected:
    case State::ShippingContactSelected:
    case State::PaymentMethodSelected:
    case State::CancelRequested:
        return false;

    case State::Authorized:
        return true;
    }
}

bool ApplePaySession::isFinalState() const
{
    switch (m_state) {
    case State::Idle:
    case State::Active:
    case State::ShippingMethodSelected:
    case State::ShippingContactSelected:
    case State::PaymentMethodSelected:
    case State::Authorized:
    case State::CancelRequested:
        return false;

    case State::Completed:
    case State::Aborted:
    case State::Canceled:
        return true;
    }
}

void ApplePaySession::didReachFinalState()
{
    ASSERT(isFinalState());
    unsetPendingActivity(this);
}

}

#endif
