package com.braintreepayments.api;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;

import com.braintreepayments.api.exceptions.BraintreeException;
import com.braintreepayments.api.interfaces.HttpResponseCallback;
import com.braintreepayments.api.interfaces.PayPalApprovalCallback;
import com.braintreepayments.api.interfaces.PayPalApprovalHandler;
import com.braintreepayments.api.interfaces.PaymentMethodNonceCallback;
import com.braintreepayments.api.internal.BraintreeSharedPreferences;
import com.braintreepayments.api.models.Authorization;
import com.braintreepayments.api.models.BraintreeRequestCodes;
import com.braintreepayments.api.models.Configuration;
import com.braintreepayments.api.models.PayPalAccountBuilder;
import com.braintreepayments.api.models.PayPalAccountNonce;
import com.braintreepayments.api.models.PayPalProductAttributes;
import com.paypal.android.sdk.onetouch.core.PayPalLineItem;
import com.braintreepayments.api.models.PayPalRequest;
import com.braintreepayments.api.models.PaymentMethodBuilder;
import com.braintreepayments.api.models.PaymentMethodNonce;
import com.braintreepayments.api.models.PostalAddress;
import com.braintreepayments.testutils.TestConfigurationBuilder;
import com.braintreepayments.testutils.TestConfigurationBuilder.TestPayPalConfigurationBuilder;
import com.paypal.android.sdk.onetouch.core.BillingAgreementRequest;
import com.paypal.android.sdk.onetouch.core.CheckoutRequest;
import com.paypal.android.sdk.onetouch.core.PayPalOneTouchCore;
import com.paypal.android.sdk.onetouch.core.Request;
import com.paypal.android.sdk.onetouch.core.config.Recipe;
import com.paypal.android.sdk.onetouch.core.enums.ResultType;

import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.skyscreamer.jsonassert.JSONAssert;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

import static androidx.appcompat.app.AppCompatActivity.RESULT_OK;
import static androidx.appcompat.app.AppCompatActivity.RESULT_CANCELED;

import static com.braintreepayments.api.BraintreePowerMockHelper.*;
import static com.braintreepayments.testutils.FixturesHelper.stringFromFixture;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.contains;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.doAnswer;
import static org.powermock.api.mockito.PowerMockito.doReturn;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.spy;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(RobolectricTestRunner.class)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "javax.crypto.*" })
@PrepareForTest({ PayPal.class, Recipe.class, TokenizationClient.class, PayPalOneTouchCore.class })
public class PayPalUnitTest {

    @Rule
    public PowerMockRule mPowerMockRule = new PowerMockRule();

    private MockFragmentBuilder mMockFragmentBuilder;

    @Before
    public void setup() throws Exception {
        spy(PayPal.class);
        doReturn(true).when(PayPal.class, "isManifestValid", any(Context.class));

        spy(Recipe.class);
        doReturn(true).when(Recipe.class, "isValidBrowserTarget", any(Context.class), anyString(), anyString());

        Authorization authorization = mock(Authorization.class);
        when(authorization.getBearer()).thenReturn("authorization");
        when(authorization.toString()).thenReturn("authorization");

        Configuration configuration = new TestConfigurationBuilder()
                .withAnalytics()
                .paypal(new TestPayPalConfigurationBuilder(true)
                        .environment("offline")
                        .billingAgreementsEnabled(false))
                .buildConfiguration();

        mMockFragmentBuilder = new MockFragmentBuilder()
                .authorization(authorization)
                .configuration(configuration);
    }

    @Test
    public void requestBillingAgreement_postsExceptionWhenAmountIsIncluded() {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestBillingAgreement(fragment, new PayPalRequest("1"));

        ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
        verify(fragment).postCallback(captor.capture());
        assertTrue(captor.getValue() instanceof BraintreeException);
        assertEquals("There must be no amount specified for the Billing Agreement flow", captor.getValue().getMessage());
    }

    @Test
    public void requestBillingAgreement_startsBrowser() {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_billing_agreement_response.json"))
                .build();

        PayPal.requestBillingAgreement(fragment, new PayPalRequest());

        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
        verify(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), captor.capture());
        Intent intent = captor.getValue();

        assertEquals(Intent.ACTION_VIEW, intent.getAction());
        assertEquals("checkout.paypal.com", intent.getData().getHost());
        assertEquals("/one-touch-login-sandbox/index.html", intent.getData().getPath());
        assertEquals("create_payment_resource", intent.getData().getQueryParameter("action"));
        assertEquals("63cc461306c35080ce674a3372bffe1580b4130c7fd33d33968aa76bb93cdd06|created_at=2015-10-13T18:49:48.371382792+0000&merchant_id=dcpspy2brwdjr3qn&public_key=9wwrzqk3vr3t4nc8",
                intent.getData().getQueryParameter("authorization_fingerprint"));
        assertFalse(intent.getData().getQueryParameterNames().contains("amount"));
        assertFalse(intent.getData().getQueryParameterNames().contains("currency_iso_code"));
        assertEquals("false", intent.getData().getQueryParameter("experience_profile[address_override]"));
        assertEquals("false", intent.getData().getQueryParameter("experience_profile[no_shipping]"));
        assertEquals("dcpspy2brwdjr3qn", intent.getData().getQueryParameter("merchant_id"));
        assertEquals("com.braintreepayments.api.test.braintree://onetouch/v1/success", intent.getData().getQueryParameter("return_url"));
        assertEquals("com.braintreepayments.api.test.braintree://onetouch/v1/cancel", intent.getData().getQueryParameter("cancel_url"));
    }

    @Test
    public void requestBillingAgreement_startsBrowserWithPayPalCredit() {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_billing_agreement_response.json"))
                .build();

        PayPal.requestBillingAgreement(fragment, new PayPalRequest().offerCredit(true));

        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
        verify(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), captor.capture());
        Intent intent = captor.getValue();

        assertEquals(Intent.ACTION_VIEW, intent.getAction());
        assertEquals("checkout.paypal.com", intent.getData().getHost());
        assertEquals("/one-touch-login-sandbox/index.html", intent.getData().getPath());
        assertEquals("create_payment_resource", intent.getData().getQueryParameter("action"));
        assertEquals("63cc461306c35080ce674a3372bffe1580b4130c7fd33d33968aa76bb93cdd06|created_at=2015-10-13T18:49:48.371382792+0000&merchant_id=dcpspy2brwdjr3qn&public_key=9wwrzqk3vr3t4nc8",
                intent.getData().getQueryParameter("authorization_fingerprint"));
        assertFalse(intent.getData().getQueryParameterNames().contains("amount"));
        assertEquals("true", intent.getData().getQueryParameter("offer_paypal_credit"));
        assertFalse(intent.getData().getQueryParameterNames().contains("currency_iso_code"));
        assertEquals("false", intent.getData().getQueryParameter("experience_profile[address_override]"));
        assertEquals("false", intent.getData().getQueryParameter("experience_profile[no_shipping]"));
        assertEquals("dcpspy2brwdjr3qn", intent.getData().getQueryParameter("merchant_id"));
        assertEquals("com.braintreepayments.api.test.braintree://onetouch/v1/success", intent.getData().getQueryParameter("return_url"));
        assertEquals("com.braintreepayments.api.test.braintree://onetouch/v1/cancel", intent.getData().getQueryParameter("cancel_url"));
    }

    @Test
    public void requestBillingAgreement_fragmentNotActive_notStartsBrowser() {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_billing_agreement_response.json"))
                .isActive(false)
                .build();

        PayPal.requestBillingAgreement(fragment, new PayPalRequest());

        verify(fragment, never()).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), any(Intent.class));
    }

    @Test
    public void requestBillingAgreement_defaultPostParamsIncludeCorrectValues() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestBillingAgreement(fragment, new PayPalRequest());

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/setup_billing_agreement"), dataCaptor.capture(),
                any(HttpResponseCallback.class));

        JSONObject expected = new JSONObject()
                .put("client_key", "authorization")
                .put("return_url", "com.braintreepayments.api.braintree://onetouch/v1/success")
                .put("cancel_url", "com.braintreepayments.api.braintree://onetouch/v1/cancel")
                .put("experience_profile", new JSONObject()
                        .put("address_override", false)
                        .put("brand_name", "displayName")
                        .put("no_shipping", true))
                .put("offer_paypal_credit", false);
        JSONObject actual = new JSONObject(dataCaptor.getValue());
        JSONAssert.assertEquals(expected, actual, true);
    }

    @Test
    public void requestBillingAgreement_sendsPayPalCreditOfferedAnalyticsEvent() {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestBillingAgreement(fragment, new PayPalRequest().offerCredit(true));

        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.credit.offered");
    }

    @Test
    public void requestBillingAgreement_containsOfferPayPalCreditParam() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestBillingAgreement(fragment, new PayPalRequest().offerCredit(true));

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/setup_billing_agreement"), dataCaptor.capture(),
                any(HttpResponseCallback.class));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertTrue(json.getBoolean("offer_paypal_credit"));
    }

    @Test
    public void requestBillingAgreement_containsPayPalTwoFactorAuthParams() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPalProductAttributes productAttributes = new PayPalProductAttributes()
                .name("name")
                .chargePattern("chargePattern")
                .productCode("productCode");

        PayPal.requestBillingAgreement(fragment, new PayPalRequest().productAttributes(productAttributes));

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/setup_billing_agreement"), dataCaptor.capture(),
                any(HttpResponseCallback.class));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        JSONObject productAttributesJSON = json.getJSONObject("product_attributes");
        assertEquals(productAttributesJSON.getString("name"), "name");
        assertEquals(productAttributesJSON.getString("product_code"), "productCode");
        assertEquals(productAttributesJSON.getString("charge_pattern"), "chargePattern");
    }

    @Test
    public void authorizeAccount_whenSuccessfulBrowserSwitch_sendsAnalyticsEvents() throws Exception {
        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_billing_agreement_response.json"))
                .build();
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                Intent intent = new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&ba_token=EC-HERMES-SANDBOX-EC-TOKEN"));
                PayPal.onActivityResult(fragment, RESULT_OK, intent);
                return null;
            }
        }).when(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), any(Intent.class));

        MockStaticTokenizationClient.mockTokenizeSuccess(new PayPalAccountNonce());

        PayPal.requestBillingAgreement(fragment, new PayPalRequest());

        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.selected");
        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.browser-switch.started");
        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.browser-switch.succeeded");
    }

    @Test
    public void requestBillingAgreement_whenSuccessfulBrowserSwitch_sendsAnalyticsEvents() {
        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_billing_agreement_response.json"))
                .build();
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                Intent intent = new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&ba_token=EC-HERMES-SANDBOX-EC-TOKEN"));
                PayPal.onActivityResult(fragment, RESULT_OK, intent);
                return null;
            }
        }).when(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), any(Intent.class));

        MockStaticTokenizationClient.mockTokenizeSuccess(new PayPalAccountNonce());

        PayPal.requestBillingAgreement(fragment, new PayPalRequest());

        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.selected");
        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.browser-switch.started");
        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.browser-switch.succeeded");
    }

    @Test
    public void requestBillingAgreement_paypalCreditReturnedInResponse() throws Exception {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_billing_agreement_response.json"))
                .build();

        MockStaticTokenizationClient.mockTokenizeSuccess(PayPalAccountNonce.fromJson(stringFromFixture("payment_methods/paypal_account_response.json")));

        PayPal.requestBillingAgreement(fragment, new PayPalRequest().offerCredit(true), new PayPalApprovalHandler() {
            @Override
            public void handleApproval(Request request, PayPalApprovalCallback paypalApprovalCallback) {
                paypalApprovalCallback.onComplete(new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&ba_token=EC-HERMES-SANDBOX-EC-TOKEN"))
                );
            }
        });

        ArgumentCaptor<PaymentMethodNonce> nonceCaptor = ArgumentCaptor.forClass(PaymentMethodNonce.class);
        verify(fragment).postCallback(nonceCaptor.capture());
        verify(fragment).sendAnalyticsEvent("paypal.credit.accepted");

        assertTrue(nonceCaptor.getValue() instanceof PayPalAccountNonce);
        PayPalAccountNonce payPalAccountNonce = (PayPalAccountNonce) nonceCaptor.getValue();
        assertNotNull(payPalAccountNonce.getCreditFinancing());
        assertEquals(18, payPalAccountNonce.getCreditFinancing().getTerm());
    }

    @Test
    public void requestBillingAgreement_doesNotCallCancelListenerWhenSuccessful() {
        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                Intent intent = new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&ba_token=EC-HERMES-SANDBOX-EC-TOKEN"));
                PayPal.onActivityResult(fragment, RESULT_OK, intent);
                return null;
            }
        }).when(fragment).startActivity(any(Intent.class));

        MockStaticTokenizationClient.mockTokenizeSuccess(new PayPalAccountNonce());

        PayPal.requestBillingAgreement(fragment, new PayPalRequest());

        verify(fragment, never()).postCancelCallback(anyInt());
    }

    @Test
    public void requestBillingAgreement_cancelUrlTriggersCancelListener() {
        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                Intent intent = new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/cancel"));
                PayPal.onActivityResult(fragment, RESULT_OK, intent);
                return null;
            }
        }).when(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), any(Intent.class));

        PayPal.requestBillingAgreement(fragment, new PayPalRequest());

        verify(fragment).postCancelCallback(BraintreeRequestCodes.PAYPAL);
    }

    @Test
    public void requestBillingAgreement_persistsPayPalRequest() {
        BraintreeFragment braintreeFragment = mMockFragmentBuilder.build();
        SharedPreferences prefs = BraintreeSharedPreferences.getSharedPreferences(RuntimeEnvironment.application);

        PayPal.requestOneTimePayment(braintreeFragment, new PayPalRequest("1").intent(PayPalRequest.INTENT_SALE));

        assertNotNull(prefs.getString("com.braintreepayments.api.PayPal.PAYPAL_REQUEST_KEY", null));
    }

    @Test
    public void requestBillingAgreement_postParamsIncludeAddress() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PostalAddress address = new PostalAddress()
                .streetAddress("123 Fake St.")
                .extendedAddress("Apt. v.0")
                .locality("Oakland")
                .region("CA")
                .postalCode("12345")
                .countryCodeAlpha2("US");
        PayPalRequest request = new PayPalRequest()
                .shippingAddressRequired(true)
                .shippingAddressOverride(address);

        PayPal.requestBillingAgreement(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/setup_billing_agreement"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        JSONObject shippingAddress = json.getJSONObject("shipping_address");

        assertEquals("123 Fake St.", shippingAddress.get("line1"));
        assertEquals("Apt. v.0", shippingAddress.get("line2"));
        assertEquals("Oakland", shippingAddress.get("city"));
        assertEquals("CA", shippingAddress.get("state"));
        assertEquals("12345", shippingAddress.get("postal_code"));
        assertEquals("US", shippingAddress.get("country_code"));
    }

    @Test
    public void requestBillingAgreement_whenEditable_postsAddressOverrideFalse() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PostalAddress address = new PostalAddress()
                .streetAddress("123 Fake St.")
                .extendedAddress("Apt. v.0")
                .locality("Oakland")
                .region("CA")
                .postalCode("12345")
                .countryCodeAlpha2("US");
        PayPalRequest request = new PayPalRequest()
                .shippingAddressEditable(true)
                .shippingAddressOverride(address);

        PayPal.requestBillingAgreement(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/setup_billing_agreement"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        JSONObject shippingAddress = json.getJSONObject("shipping_address");
        JSONObject experienceProfile = json.getJSONObject("experience_profile");

        assertTrue(shippingAddress.length() > 0);
        assertEquals(false, experienceProfile.get("address_override"));
    }

    @Test
    public void requestBillingAgreement_whenShippingRequired_postsNoShippingTrue() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();
        PayPalRequest request = new PayPalRequest()
                .shippingAddressRequired(true);

        PayPal.requestBillingAgreement(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/setup_billing_agreement"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        JSONObject experienceProfile = json.getJSONObject("experience_profile");

        assertEquals(false, experienceProfile.get("no_shipping"));
    }

    @Test
    public void requestBillingAgreement_whenShippingRequiredFalse_postsNoShippingFalse() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();
        PayPalRequest request = new PayPalRequest();

        PayPal.requestBillingAgreement(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/setup_billing_agreement"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        JSONObject experienceProfile = json.getJSONObject("experience_profile");

        assertEquals(true, experienceProfile.get("no_shipping"));
    }

    @Test
    public void requestBillingAgreement_whenEditableFalse_postsAddressOverrideTrue() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PostalAddress address = new PostalAddress()
                .streetAddress("123 Fake St.")
                .extendedAddress("Apt. v.0")
                .locality("Oakland")
                .region("CA")
                .postalCode("12345")
                .countryCodeAlpha2("US");
        PayPalRequest request = new PayPalRequest()
                .shippingAddressOverride(address);

        PayPal.requestBillingAgreement(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/setup_billing_agreement"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        JSONObject shippingAddress = json.getJSONObject("shipping_address");
        JSONObject experienceProfile = json.getJSONObject("experience_profile");

        assertTrue(shippingAddress.length() > 0);
        assertEquals(true, experienceProfile.get("address_override"));
    }

    @Test
    public void requestBillingAgreement_whenMerchantAccountIdPresent_postsParamsIncludeMerchantAccountId() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPalRequest request = new PayPalRequest()
                .merchantAccountId("merchant_account_id");

        PayPal.requestBillingAgreement(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/setup_billing_agreement"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        String merchantAccountId = json.getString("merchant_account_id");

        assertEquals("merchant_account_id", merchantAccountId);
    }

    @Test
    public void requestBillingAgreement_whenMerchantAccountIdNotPresent_postsDoNotIncludeMerchantAccountId() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPalRequest request = new PayPalRequest();

        PayPal.requestBillingAgreement(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/setup_billing_agreement"));

        JSONObject json = new JSONObject(dataCaptor.getValue());

        assertFalse(json.has("merchant_account_id"));
    }

    @Test
    public void requestOneTimePayment_postsExceptionWhenNoAmountIsSet() {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest());

        ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
        verify(fragment).postCallback(captor.capture());
        assertTrue(captor.getValue() instanceof BraintreeException);
        assertEquals("An amount must be specified for the Single Payment flow.", captor.getValue().getMessage());
    }

    @Test(timeout = 1000)
    public void requestOneTimePayment_customHandlerIsCalledCorrectly() throws InterruptedException {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();

        final CountDownLatch latch = new CountDownLatch(1);
        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1"), new PayPalApprovalHandler() {
            @Override
            public void handleApproval(Request request, PayPalApprovalCallback paypalApprovalCallback) {
                latch.countDown();
            }
        });

        latch.await();
    }

    @Test
    public void requestOneTimePayment_customHandlerCancelCallbackIsInvoked() {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1"), new PayPalApprovalHandler() {
            @Override
            public void handleApproval(Request request, PayPalApprovalCallback paypalApprovalCallback) {
                paypalApprovalCallback.onCancel();
            }
        });

        verify(fragment).postCancelCallback(BraintreeRequestCodes.PAYPAL);
    }

    @Test
    public void requestOneTimePayment_paypalCreditReturnedInResponse() throws Exception {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();

        MockStaticTokenizationClient.mockTokenizeSuccess(PayPalAccountNonce.fromJson(stringFromFixture("payment_methods/paypal_account_response.json")));

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").offerCredit(true), new PayPalApprovalHandler() {
            @Override
            public void handleApproval(Request request, PayPalApprovalCallback paypalApprovalCallback) {
                paypalApprovalCallback.onComplete(new Intent()
                        .setData(Uri.parse("com.braintreepayments.demo.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN"))
                );
            }
        });

        ArgumentCaptor<PaymentMethodNonce> nonceCaptor = ArgumentCaptor.forClass(PaymentMethodNonce.class);
        verify(fragment).postCallback(nonceCaptor.capture());
        verify(fragment).sendAnalyticsEvent("paypal.credit.accepted");

        assertTrue(nonceCaptor.getValue() instanceof PayPalAccountNonce);
        PayPalAccountNonce payPalAccountNonce = (PayPalAccountNonce) nonceCaptor.getValue();
        assertNotNull(payPalAccountNonce.getCreditFinancing());
        assertEquals(18, payPalAccountNonce.getCreditFinancing().getTerm());
    }

    @Test
    public void requestOneTimePayment_customHandlerSuccessCallbackIsInvoked() {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();

        MockStaticTokenizationClient.mockTokenizeSuccess(new PayPalAccountNonce());

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1"), new PayPalApprovalHandler() {
            @Override
            public void handleApproval(Request request, PayPalApprovalCallback paypalApprovalCallback) {
                paypalApprovalCallback.onComplete(new Intent()
                        .setData(Uri.parse("com.braintreepayments.demo.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN"))
                );
            }
        });

        ArgumentCaptor<PaymentMethodNonce> nonceCaptor = ArgumentCaptor.forClass(PaymentMethodNonce.class);
        verify(fragment).postCallback(nonceCaptor.capture());

        assertTrue(nonceCaptor.getValue() instanceof PayPalAccountNonce);
    }

    @Test
    public void requestOneTimePayment_sendsPayPalCreditOfferedAnalyticsEvent() {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").offerCredit(true));

        verify(fragment).sendAnalyticsEvent("paypal.single-payment.credit.offered");
    }

    @Test
    public void requestOneTimePayment_defaultPostParamsIncludeCorrectValues() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1"));

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject actual = new JSONObject(dataCaptor.getValue());
        JSONObject expected = new JSONObject()
                .put("client_key", "authorization")
                .put("return_url", "com.braintreepayments.api.braintree://onetouch/v1/success")
                .put("cancel_url", "com.braintreepayments.api.braintree://onetouch/v1/cancel")
                .put("amount", "1")
                .put("experience_profile", new JSONObject()
                        .put("address_override", false)
                        .put("brand_name", "displayName")
                        .put("no_shipping", true))
                .put("intent", "authorize")
                .put("offer_paypal_credit", false);

        JSONAssert.assertEquals(expected, actual, true);
    }

    @Test
    public void requestOneTimePayment_containsOfferPayPalCreditParam() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").offerCredit(true));

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/create_payment_resource"),
                dataCaptor.capture(), any(HttpResponseCallback.class));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertTrue(json.getBoolean("offer_paypal_credit"));
    }

    @Test
    public void requestOneTimePayment_containsLineItemParams() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPalRequest request = new PayPalRequest("2");
        ArrayList<PayPalLineItem> lineItems = new ArrayList<PayPalLineItem>();
        lineItems.add(new PayPalLineItem(PayPalLineItem.KIND_DEBIT, "An Item", "1", "2"));
        request.lineItems(lineItems);
        PayPal.requestOneTimePayment(fragment, request);

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/create_payment_resource"),
                dataCaptor.capture(), any(HttpResponseCallback.class));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertNotNull(json.getJSONArray("line_items"));

        JSONObject itemJson = json.getJSONArray("line_items").getJSONObject(0);
        assertEquals("debit", itemJson.getString("kind"));
        assertEquals("An Item", itemJson.getString("name"));
        assertEquals("1", itemJson.getString("quantity"));
        assertEquals("2", itemJson.getString("unit_amount"));
    }

    @Test
    public void requestOneTimePayment_displayName_canBeSet() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").displayName("Test Name"));

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/create_payment_resource"), dataCaptor.capture(),
                any(HttpResponseCallback.class));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("Test Name", json.getJSONObject("experience_profile").get("brand_name"));
    }

    @Test
    public void requestOneTimePayment_intent_canBeSetToSale() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").intent(PayPalRequest.INTENT_SALE));

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/create_payment_resource"), dataCaptor.capture(),
                any(HttpResponseCallback.class));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("1", json.get("amount"));
        assertEquals(true, json.getJSONObject("experience_profile").get("no_shipping"));
        assertEquals(false, json.getJSONObject("experience_profile").get("address_override"));
        assertEquals(PayPalRequest.INTENT_SALE, json.get("intent"));
    }

    @Test
    public void requestOneTimePayment_intent_canBeSetToOrder() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").intent(PayPalRequest.INTENT_ORDER));

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/create_payment_resource"), dataCaptor.capture(),
                any(HttpResponseCallback.class));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("1", json.get("amount"));
        assertEquals(true, json.getJSONObject("experience_profile").get("no_shipping"));
        assertEquals(false, json.getJSONObject("experience_profile").get("address_override"));
        assertEquals(PayPalRequest.INTENT_ORDER, json.get("intent"));
    }

    @Test
    public void requestOneTimePayment_landingPageType_canBeSetToBilling() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").landingPageType(PayPalRequest.LANDING_PAGE_TYPE_BILLING));

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/create_payment_resource"), dataCaptor.capture(),
                any(HttpResponseCallback.class));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("1", json.get("amount"));
        assertEquals(true, json.getJSONObject("experience_profile").get("no_shipping"));
        assertEquals(false, json.getJSONObject("experience_profile").get("address_override"));
        assertEquals(PayPalRequest.LANDING_PAGE_TYPE_BILLING, json.getJSONObject("experience_profile").get("landing_page_type"));
    }

    @Test
    public void requestOneTimePayment_landingPageType_canBeSetToLogin() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").landingPageType(PayPalRequest.LANDING_PAGE_TYPE_LOGIN));

        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(contains("/paypal_hermes/create_payment_resource"), dataCaptor.capture(),
                any(HttpResponseCallback.class));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("1", json.get("amount"));
        assertEquals(true, json.getJSONObject("experience_profile").get("no_shipping"));
        assertEquals(false, json.getJSONObject("experience_profile").get("address_override"));
        assertEquals(PayPalRequest.LANDING_PAGE_TYPE_LOGIN, json.getJSONObject("experience_profile").get("landing_page_type"));
    }

    @Test
    public void requestOneTimePayment_userAction_setsUserActionToBlankStringonDefault() {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").userAction(PayPalRequest.USER_ACTION_DEFAULT));

        ArgumentCaptor<Intent> dataCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), dataCaptor.capture());

        Uri uri = dataCaptor.getValue().getData();
        assertEquals("", uri.getQueryParameter("useraction"));
    }

    @Test
    public void requestOneTimePayment_userAction_canBeSetToCommit() {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").userAction(PayPalRequest.USER_ACTION_COMMIT));

        ArgumentCaptor<Intent> dataCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), dataCaptor.capture());

        Uri uri = dataCaptor.getValue().getData();
        assertEquals("commit", uri.getQueryParameter("useraction"));
    }

    @Test
    public void requestOneTimePayment_postParamsIncludeNoShipping() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").shippingAddressRequired(false));

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("1", json.get("amount"));
        assertEquals(true, json.getJSONObject("experience_profile").get("no_shipping"));
        assertEquals(false, json.getJSONObject("experience_profile").get("address_override"));
    }

    @Test
    public void requestOneTimePayment_postParamsIncludeAddress() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PostalAddress address = new PostalAddress()
                .streetAddress("123 Fake St.")
                .extendedAddress("Apt. v.0")
                .locality("Oakland")
                .region("CA")
                .postalCode("12345")
                .countryCodeAlpha2("US");
        PayPalRequest request = new PayPalRequest("3.43")
                .shippingAddressOverride(address);

        PayPal.requestOneTimePayment(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("3.43", json.get("amount"));
        assertEquals("123 Fake St.", json.get("line1"));
        assertEquals("Apt. v.0", json.get("line2"));
        assertEquals("Oakland", json.get("city"));
        assertEquals("CA", json.get("state"));
        assertEquals("12345", json.get("postal_code"));
        assertEquals("US", json.get("country_code"));
    }

    @Test
    public void requestOnetimePayment_whenShippingRequired_postsNoShippingFalse() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPalRequest request = new PayPalRequest("3.43")
                .shippingAddressRequired(true);

        PayPal.requestOneTimePayment(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        JSONObject experienceProfile = json.getJSONObject("experience_profile");

        assertFalse(experienceProfile.getBoolean("no_shipping"));
    }

    @Test
    public void requestOnetimePayment_whenShippingRequiredFalse_postsNoShippingTrue() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPalRequest request = new PayPalRequest("3.43")
                .shippingAddressRequired(false);

        PayPal.requestOneTimePayment(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        JSONObject experienceProfile = json.getJSONObject("experience_profile");

        assertTrue(experienceProfile.getBoolean("no_shipping"));
    }

    @Test
    public void requestOneTimePayment_whenEditable_postAddressOverrideFalse() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PostalAddress address = new PostalAddress()
                .streetAddress("123 Fake St.")
                .extendedAddress("Apt. v.0")
                .locality("Oakland")
                .region("CA")
                .postalCode("12345")
                .countryCodeAlpha2("US");
        PayPalRequest request = new PayPalRequest("3.43")
                .shippingAddressEditable(true)
                .shippingAddressOverride(address);

        PayPal.requestOneTimePayment(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("US", json.get("country_code"));
        assertEquals(false, json.getJSONObject("experience_profile").get("address_override"));
    }

    @Test
    public void requestOneTimePayment_whenMerchantAccountIdPresent_postIncludeMerchantAccountId() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPalRequest request = new PayPalRequest("3.43")
                .merchantAccountId("merchant_account_id");

        PayPal.requestOneTimePayment(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        String merchantAccountId = json.getString("merchant_account_id");

        assertEquals("merchant_account_id", merchantAccountId);
    }

    @Test
    public void requestOneTimePayment_whenMerchantAccountIdNotPresent_postDoesNotIncludeMerchantAccountId() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPalRequest request = new PayPalRequest("3.43");

        PayPal.requestOneTimePayment(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject json = new JSONObject(dataCaptor.getValue());

        assertFalse(json.has("merchant_account_id"));
    }

    @Test
    public void requestOneTimePayment_whenEditableFalse_postAddressOverrideTrue() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PostalAddress address = new PostalAddress()
                .streetAddress("123 Fake St.")
                .extendedAddress("Apt. v.0")
                .locality("Oakland")
                .region("CA")
                .postalCode("12345")
                .countryCodeAlpha2("US");
        PayPalRequest request = new PayPalRequest("3.43")
                .shippingAddressOverride(address);

        PayPal.requestOneTimePayment(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("US", json.get("country_code"));
        assertEquals(true, json.getJSONObject("experience_profile").get("address_override"));
    }

    @Test
    public void requestOneTimePayment_postParamsIncludeNoShippingAndAddressAndAddressOverride() throws JSONException {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PostalAddress address = new PostalAddress()
                .streetAddress("123 Fake St.")
                .extendedAddress("Apt. v.0")
                .locality("Oakland")
                .region("CA")
                .postalCode("12345")
                .countryCodeAlpha2("US");
        PayPalRequest request = new PayPalRequest("3.43")
                .shippingAddressRequired(false)
                .shippingAddressOverride(address);

        PayPal.requestOneTimePayment(fragment, request);

        ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
        verify(fragment.getHttpClient()).post(pathCaptor.capture(), dataCaptor.capture(),
                any(HttpResponseCallback.class));
        assertTrue(pathCaptor.getValue().contains("/paypal_hermes/create_payment_resource"));

        JSONObject json = new JSONObject(dataCaptor.getValue());
        assertEquals("3.43", json.get("amount"));
        assertEquals("123 Fake St.", json.get("line1"));
        assertEquals("Apt. v.0", json.get("line2"));
        assertEquals("Oakland", json.get("city"));
        assertEquals("CA", json.get("state"));
        assertEquals("12345", json.get("postal_code"));
        assertEquals("US", json.get("country_code"));
        assertEquals(true, json.getJSONObject("experience_profile").get("no_shipping"));
        assertEquals(true, json.getJSONObject("experience_profile").get("address_override"));
    }

    @Test
    public void requestOneTimePayment_startsBrowser() {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1.00"));

        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
        verify(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), captor.capture());
        Intent intent = captor.getValue();

        assertEquals(Intent.ACTION_VIEW, intent.getAction());
        assertEquals("checkout.paypal.com", intent.getData().getHost());
        assertEquals("/one-touch-login-sandbox/index.html", intent.getData().getPath());
        assertEquals("create_payment_resource", intent.getData().getQueryParameter("action"));
        assertEquals("63cc461306c35080ce674a3372bffe1580b4130c7fd33d33968aa76bb93cdd06|created_at=2015-10-13T18:49:48.371382792+0000&merchant_id=dcpspy2brwdjr3qn&public_key=9wwrzqk3vr3t4nc8",
                intent.getData().getQueryParameter("authorization_fingerprint"));
        assertEquals("USD", intent.getData().getQueryParameter("currency_iso_code"));
        assertEquals("false", intent.getData().getQueryParameter("experience_profile[address_override]"));
        assertEquals("false", intent.getData().getQueryParameter("experience_profile[no_shipping]"));
        assertEquals("dcpspy2brwdjr3qn", intent.getData().getQueryParameter("merchant_id"));
        assertEquals("com.braintreepayments.api.test.braintree://onetouch/v1/success", intent.getData().getQueryParameter("return_url"));
        assertEquals("com.braintreepayments.api.test.braintree://onetouch/v1/cancel", intent.getData().getQueryParameter("cancel_url"));
    }

    @Test
    public void requestOneTimePayment_startsBrowserWithPayPalCredit() {
        BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1.00").offerCredit(true));

        ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
        verify(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), captor.capture());
        Intent intent = captor.getValue();

        assertEquals(Intent.ACTION_VIEW, intent.getAction());
        assertEquals("checkout.paypal.com", intent.getData().getHost());
        assertEquals("/one-touch-login-sandbox/index.html", intent.getData().getPath());
        assertEquals("create_payment_resource", intent.getData().getQueryParameter("action"));
        assertEquals("63cc461306c35080ce674a3372bffe1580b4130c7fd33d33968aa76bb93cdd06|created_at=2015-10-13T18:49:48.371382792+0000&merchant_id=dcpspy2brwdjr3qn&public_key=9wwrzqk3vr3t4nc8",
                intent.getData().getQueryParameter("authorization_fingerprint"));
        assertEquals("USD", intent.getData().getQueryParameter("currency_iso_code"));
        assertEquals("true", intent.getData().getQueryParameter("offer_paypal_credit"));
        assertEquals("false", intent.getData().getQueryParameter("experience_profile[address_override]"));
        assertEquals("false", intent.getData().getQueryParameter("experience_profile[no_shipping]"));
        assertEquals("dcpspy2brwdjr3qn", intent.getData().getQueryParameter("merchant_id"));
        assertEquals("com.braintreepayments.api.test.braintree://onetouch/v1/success", intent.getData().getQueryParameter("return_url"));
        assertEquals("com.braintreepayments.api.test.braintree://onetouch/v1/cancel", intent.getData().getQueryParameter("cancel_url"));
    }

    @Test
    public void requestOneTimePayment_isSuccessful() {
        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                Intent intent = new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN"));
                PayPal.onActivityResult(fragment, RESULT_OK, intent);
                return null;
            }
        }).when(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), any(Intent.class));

        MockStaticTokenizationClient.mockTokenizeSuccess(new PayPalAccountNonce());

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1"));

        verify(fragment).postCallback(any(PayPalAccountNonce.class));

        SharedPreferences prefs = BraintreeSharedPreferences.getSharedPreferences(RuntimeEnvironment.application);
        assertNull(prefs.getString("com.braintreepayments.api.PayPal.REQUEST_KEY", null));
        assertNull(prefs.getString("com.braintreepayments.api.PayPal.REQUEST_TYPE_KEY", null));
    }

    @Test
    public void requestOneTimePayment_whenSuccessfulAppSwitchStart_sendsAnalyticsEvents() {
        MockStaticPayPalOneTouch.getStartIntent("app-switch");

        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1"));

        verify(fragment).sendAnalyticsEvent("paypal.single-payment.selected");
        verify(fragment).sendAnalyticsEvent("paypal.single-payment.app-switch.started");
    }

    @Test
    public void requestOneTimePayment_whenBrowserSwitchSuccessful_sendsAnalyticsEvents() {
        MockStaticTokenizationClient.mockTokenizeSuccess(new PayPalAccountNonce());

        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                Intent intent = new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN"));
                PayPal.onActivityResult(fragment, RESULT_OK, intent);
                return null;
            }
        }).when(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), any(Intent.class));

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1"));

        verify(fragment).sendAnalyticsEvent("paypal.single-payment.selected");
        verify(fragment).sendAnalyticsEvent("paypal.single-payment.browser-switch.started");
        verify(fragment).sendAnalyticsEvent("paypal.single-payment.browser-switch.succeeded");
    }

    @Test
    public void requestOneTimePayment_containsPaymentIntent() throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);
        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                Intent intent = new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN"));
                PayPal.onActivityResult(fragment, RESULT_OK, intent);
                return null;
            }
        }).when(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), any(Intent.class));

        mockStatic(TokenizationClient.class);
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                PayPalAccountBuilder payPalAccountBuilder = (PayPalAccountBuilder) invocation.getArguments()[1];
                JSONObject payload = new JSONObject(payPalAccountBuilder.build());
                assertEquals(PayPalRequest.INTENT_SALE, payload.getJSONObject("paypalAccount").getString("intent"));
                ((PaymentMethodNonceCallback) invocation.getArguments()[2]).success(new PayPalAccountNonce());
                latch.countDown();
                return null;
            }
        }).when(TokenizationClient.class);
        TokenizationClient.tokenize(any(BraintreeFragment.class), any(PaymentMethodBuilder.class),
                any(PaymentMethodNonceCallback.class));
        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1").intent(PayPalRequest.INTENT_SALE));

        SharedPreferences prefs = BraintreeSharedPreferences.getSharedPreferences(RuntimeEnvironment.application);
        assertNull(prefs.getString("com.braintreepayments.api.PayPal.PAYPAL_REQUEST_KEY", null));
        latch.await();
    }

    @Test
    public void requestOneTimePayment_doesNotCallCancelListenerWhenSuccessful() {
        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                Intent intent = new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/success?PayerID=HERMES-SANDBOX-PAYER-ID&paymentId=HERMES-SANDBOX-PAYMENT-ID&token=EC-HERMES-SANDBOX-EC-TOKEN"));
                PayPal.onActivityResult(fragment, RESULT_OK, intent);
                return null;
            }
        }).when(fragment).startActivity(any(Intent.class));

        MockStaticTokenizationClient.mockTokenizeSuccess(new PayPalAccountNonce());

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1"));

        verify(fragment, never()).postCancelCallback(anyInt());
    }

    @Test
    public void requestOneTimePayment_persistsPayPalRequest() {
        BraintreeFragment braintreeFragment = mMockFragmentBuilder.build();
        SharedPreferences prefs = BraintreeSharedPreferences.getSharedPreferences(RuntimeEnvironment.application);

        PayPal.requestOneTimePayment(braintreeFragment, new PayPalRequest("1").intent(PayPalRequest.INTENT_SALE));

        assertNotNull(prefs.getString("com.braintreepayments.api.PayPal.PAYPAL_REQUEST_KEY", null));
    }

    @Test
    public void checkout_cancelUrlTriggersCancelListener() {
        final BraintreeFragment fragment = mMockFragmentBuilder
                .successResponse(stringFromFixture("paypal_hermes_response.json"))
                .build();
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                Intent intent = new Intent()
                        .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/cancel"));
                PayPal.onActivityResult(fragment, RESULT_OK, intent);
                return null;
            }
        }).when(fragment).browserSwitch(eq(BraintreeRequestCodes.PAYPAL), any(Intent.class));

        PayPal.requestOneTimePayment(fragment, new PayPalRequest("1"));

        verify(fragment).postCancelCallback(BraintreeRequestCodes.PAYPAL);
    }

    @Test
    public void onActivityResult_postsCancelWhenResultIsCanceled() {
        BraintreeFragment fragment = mMockFragmentBuilder.build();
        Intent intent = new Intent()
                .setData(Uri.parse("com.braintreepayments.api.test.braintree://onetouch/v1/cancel"));

        PayPal.onActivityResult(fragment, RESULT_OK, intent);

        verify(fragment).postCancelCallback(BraintreeRequestCodes.PAYPAL);
    }

    @Test
    public void onActivityResult_postsCancelWhenIntentIsNull() {
        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.onActivityResult(fragment, RESULT_OK, null);

        verify(fragment).postCancelCallback(BraintreeRequestCodes.PAYPAL);
    }

    @Test
    public void onActivityResult_whenBillingAgreemeentAppSwitchSuccessful_sendsAnalyticsEvent() {
        persistRequest(new BillingAgreementRequest());
        MockStaticPayPalOneTouch.parseResponse(ResultType.Success);

        BraintreeFragment fragment = mMockFragmentBuilder.build();
        Intent data = new Intent();

        PayPal.onActivityResult(fragment, RESULT_OK, data);

        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.app-switch.succeeded");
    }

    @Test
    public void onActivityResult_whenSinglePaymentAppSwitchSuccessful_sendsAnalyticsEvent() {
        persistRequest(new CheckoutRequest());
        MockStaticPayPalOneTouch.parseResponse(ResultType.Success);

        BraintreeFragment fragment = mMockFragmentBuilder.build();
        Intent data = new Intent();

        PayPal.onActivityResult(fragment, RESULT_OK, data);

        verify(fragment).sendAnalyticsEvent("paypal.single-payment.app-switch.succeeded");
    }

    @Test
    public void onActivityResult_NotOkAndBillingAgreementAndAppSwitch_sendsAnalyticsEvent() {
        persistRequest(new BillingAgreementRequest());
        Intent data = dataForSwitch("app-switch");

        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.onActivityResult(fragment, RESULT_CANCELED, data);

        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.app-switch.canceled");
    }

    @Test
    public void onActivityResult_NotOkAndBillingAgreementAndBrowserSwitch_sendsAnalyticsEvent() {
        persistRequest(new BillingAgreementRequest());
        Intent data = dataForSwitch("browser-switch");

        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.onActivityResult(fragment, RESULT_CANCELED, data);

        verify(fragment).sendAnalyticsEvent("paypal.billing-agreement.browser-switch.canceled");
    }

    @Test
    public void onActivityResult_NotOkAndCheckoutAndAppSwitch_sendsAnalyticsEvent() {
        persistRequest(new CheckoutRequest());
        Intent data = dataForSwitch("app-switch");

        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.onActivityResult(fragment, RESULT_CANCELED, data);

        verify(fragment).sendAnalyticsEvent("paypal.single-payment.app-switch.canceled");
    }

    @Test
    public void onActivityResult_NotOkAndCheckoutAndBrowserSwitch_sendsAnalyticsEvent() {
        persistRequest(new CheckoutRequest());
        Intent data = dataForSwitch("browser-switch");

        BraintreeFragment fragment = mMockFragmentBuilder.build();

        PayPal.onActivityResult(fragment, RESULT_CANCELED, data);

        verify(fragment).sendAnalyticsEvent("paypal.single-payment.browser-switch.canceled");
    }

    private void persistRequest(Request request) {
        try {
            Method method = PayPal.class.getDeclaredMethod("persistRequest",
                    Context.class, Request.class);
            method.setAccessible(true);
            method.invoke(null, RuntimeEnvironment.application, request);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Intent dataForSwitch(String switchType) {
        return new Intent().putExtra(BraintreeFragment.EXTRA_WAS_BROWSER_SWITCH_RESULT,
                        switchType.equals("browser-switch"));
    }
}