chromium/chrome/android/javatests/src/org/chromium/chrome/browser/autofill/settings/AutofillServerCardEditorTest.java

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

package org.chromium.chrome.browser.autofill.settings;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.os.Bundle;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;

import androidx.test.espresso.Espresso;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.matcher.ViewMatchers.Visibility;
import androidx.test.filters.MediumTest;
import androidx.test.runner.lifecycle.Stage;

import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.ApplicationTestUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.autofill.AutofillEditorBase;
import org.chromium.chrome.browser.autofill.AutofillTestHelper;
import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.settings.SettingsActivity;
import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.components.autofill.VirtualCardEnrollmentLinkType;
import org.chromium.components.autofill.VirtualCardEnrollmentState;
import org.chromium.components.autofill.payments.LegalMessageLine;
import org.chromium.url.GURL;

import java.util.concurrent.TimeoutException;

/** Instrumentation tests for AutofillServerCardEditor. */
@RunWith(ChromeJUnit4ClassRunner.class)
public class AutofillServerCardEditorTest {
    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule public JniMocker mMocker = new JniMocker();
    @Rule public final AutofillTestRule rule = new AutofillTestRule();

    @Rule
    public final SettingsActivityTestRule<AutofillServerCardEditor> mSettingsActivityTestRule =
            new SettingsActivityTestRule<>(AutofillServerCardEditor.class);

    private static final long NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE = 100L;

    private static final CreditCard SAMPLE_VIRTUAL_CARD_ENROLLED_CARD =
            new CreditCard(
                    /* guid= */ "1",
                    /* origin= */ "",
                    /* isLocal= */ false,
                    /* isVirtual= */ false,
                    /* name= */ "John Doe",
                    /* number= */ "4444333322221111",
                    /* obfuscatedNumber= */ "",
                    /* month= */ "5",
                    AutofillTestHelper.nextYear(),
                    /* basicCardIssuerNetwork= */ "visa",
                    /* issuerIconDrawableId= */ 0,
                    /* billingAddressId= */ "",
                    /* serverId= */ "",
                    /* instrumentId= */ 123,
                    /* cardLabel= */ "",
                    /* nickname= */ "",
                    /* cardArtUrl= */ null,
                    /* virtualCardEnrollmentState= */ VirtualCardEnrollmentState.ENROLLED,
                    /* productDescription= */ "",
                    /* cardNameForAutofillDisplay= */ "",
                    /* obfuscatedLastFourDigits= */ "",
                    /* cvc= */ "");

    private static final CreditCard SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD =
            new CreditCard(
                    /* guid= */ "2",
                    /* origin= */ "",
                    /* isLocal= */ false,
                    /* isVirtual= */ false,
                    /* name= */ "John Doe",
                    /* number= */ "4444333322221111",
                    /* obfuscatedNumber= */ "",
                    /* month= */ "5",
                    AutofillTestHelper.nextYear(),
                    /* basicCardIssuerNetwork= */ "visa",
                    /* issuerIconDrawableId= */ 0,
                    /* billingAddressId= */ "",
                    /* serverId= */ "",
                    /* instrumentId= */ 234,
                    /* cardLabel= */ "",
                    /* nickname= */ "",
                    /* cardArtUrl= */ null,
                    /* virtualCardEnrollmentState= */ VirtualCardEnrollmentState
                            .UNENROLLED_AND_ELIGIBLE,
                    /* productDescription= */ "",
                    /* cardNameForAutofillDisplay= */ "",
                    /* obfuscatedLastFourDigits= */ "",
                    /* cvc= */ "");

    private static final CreditCard SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_NOT_ELIGIBLE_CARD =
            new CreditCard(
                    /* guid= */ "3",
                    /* origin= */ "",
                    /* isLocal= */ false,
                    /* isVirtual= */ false,
                    /* name= */ "John Doe",
                    /* number= */ "4444333322221111",
                    /* obfuscatedNumber= */ "",
                    /* month= */ "5",
                    AutofillTestHelper.nextYear(),
                    /* basicCardIssuerNetwork= */ "visa",
                    /* issuerIconDrawableId= */ 0,
                    /* billingAddressId= */ "",
                    /* serverId= */ "",
                    /* instrumentId= */ 345,
                    /* cardLabel= */ "",
                    /* nickname= */ "",
                    /* cardArtUrl= */ null,
                    /* virtualCardEnrollmentState= */ VirtualCardEnrollmentState
                            .UNENROLLED_AND_NOT_ELIGIBLE,
                    /* productDescription= */ "",
                    /* cardNameForAutofillDisplay= */ "",
                    /* obfuscatedLastFourDigits= */ "",
                    /* cvc= */ "");

    @Mock private AutofillPaymentMethodsDelegate.Natives mNativeMock;
    @Mock private Callback<String> mServerCardEditLinkOpenerCallback;

    private AutofillTestHelper mAutofillTestHelper;

    @Before
    public void setUp() {
        reset(mNativeMock);
        mMocker.mock(AutofillPaymentMethodsDelegateJni.TEST_HOOKS, mNativeMock);
        when(mNativeMock.init(any(Profile.class)))
                .thenReturn(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE);
        mAutofillTestHelper = new AutofillTestHelper();
    }

    @After
    public void tearDown() throws TimeoutException {
        mAutofillTestHelper.clearAllDataForTesting();
        // Ensures that the native cleanup method is called before mocks are cleaned up
        // on failed cases.
        Activity activity = mSettingsActivityTestRule.getActivity();
        if (activity != null) {
            finishAndWaitForActivity(activity);
        }
    }

    @Test
    @MediumTest
    public void virtualCardEnrolled_virtualCardRemoveButtonShown() throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getGUID()));

        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)));
        // Ensure the activity is cleaned up.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    public void virtualCardUnenrolledAndEligible_virtualCardAddButtonShown() throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD.getGUID()));

        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)));
        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    public void virtualCardUnenrolledAndNotEligible_virtualCardLayoutNotShown() throws Exception {
        mAutofillTestHelper.addServerCreditCard(
                SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_NOT_ELIGIBLE_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(
                                SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_NOT_ELIGIBLE_CARD.getGUID()));

        onView(withId(R.id.virtual_card_ui))
                .check(matches(withEffectiveVisibility(Visibility.GONE)));
        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    @DisabledTest(message = "https://crbug.com/342333311")
    public void
            virtualCardUnenrolledAndEligible_virtualCardAddButtonClicked_enrollAccepted_enrollmentSuccessful()
                    throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD.getGUID()));
        // Verify that the native delegate was initialized properly.
        verify(mNativeMock).init(any(Profile.class));

        // Verify that the Virtual Card enrollment button is shown and allows enrollment.
        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(isEnabled()));

        // Press the enrollment button.
        // Verify that enrollment button click is recorded.
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Autofill.SettingsPage.ButtonClicked.VirtualCard.VirtualCardEnroll", true);
        onView(withId(R.id.virtual_card_enrollment_button)).perform(click());
        histogram.assertExpected();

        // Verify that the Virtual Card enrollment button still shows text for enrollment and that
        // the button is disabled.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(Matchers.not(isEnabled())));

        // Verify that the native enroll method was called with the correct parameters.
        ArgumentCaptor<Callback<VirtualCardEnrollmentFields>> callbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .initVirtualCardEnrollment(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        ArgumentMatchers.eq(234L),
                        callbackArgumentCaptor.capture());
        // Return VirtualCardEnrollmentFields via the callback to show the dialog.
        Callback<VirtualCardEnrollmentFields> virtualCardEnrollmentFieldsCallback =
                callbackArgumentCaptor.getValue();
        VirtualCardEnrollmentFields fakeVirtualCardEnrollmentFields =
                VirtualCardEnrollmentFields.create("Visa", "1234", 0, new GURL(""));
        fakeVirtualCardEnrollmentFields.mGoogleLegalMessages.add(new LegalMessageLine("google"));
        fakeVirtualCardEnrollmentFields.mIssuerLegalMessages.add(new LegalMessageLine("issuer"));
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        virtualCardEnrollmentFieldsCallback.onResult(
                                fakeVirtualCardEnrollmentFields));

        // Verify that the dialog was displayed.
        onView(withId(R.id.dialog_title)).inRoot(isDialog()).check(matches(isDisplayed()));

        // Click on the education link.
        // Verify that education text link click is recorded.
        histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Autofill.VirtualCard.SettingsPageEnrollment.LinkClicked",
                        VirtualCardEnrollmentLinkType.VIRTUAL_CARD_ENROLLMENT_LEARN_MORE_LINK);
        onView(withId(R.id.virtual_card_education)).perform(clickLink());
        histogram.assertExpected();

        // Go back to the settings page.
        Espresso.pressBack();

        // Click positive button on enrollment dialog.
        // Verify that enrollment dialog acceptance is recorded.
        histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Autofill.VirtualCard.SettingsPageEnrollment", true);
        onView(withId(R.id.positive_button)).perform(click());
        histogram.assertExpected();

        // Verify that the Virtual Card enrollment button still shows text for enrollment and that
        // the button is disabled while waiting for server response.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(Matchers.not(isEnabled())));

        // Verify that enrollment is called with the correct parameters when the user clicks the
        // positive button on the dialog.
        ArgumentCaptor<Callback<Boolean>> booleanCallbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .enrollOfferedVirtualCard(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        booleanCallbackArgumentCaptor.capture());
        // Return enrollment update status "successful" via the callback.
        Callback<Boolean> virtualCardEnrollmentUpdateResponseCallback =
                booleanCallbackArgumentCaptor.getValue();
        ThreadUtils.runOnUiThreadBlocking(
                () -> virtualCardEnrollmentUpdateResponseCallback.onResult(true));

        // Verify that the Virtual Card enrollment button now allows unenrollment.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)))
                .check(matches(isEnabled()));

        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    public void
            virtualCardUnenrolledAndEligible_virtualCardAddButtonClicked_enrollAccepted_enrollmentFailure()
                    throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD.getGUID()));
        // Verify that the native delegate was initialized properly.
        verify(mNativeMock).init(any(Profile.class));

        // Verify that the Virtual Card enrollment button is shown and allows enrollment.
        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(isEnabled()));

        // Press the enrollment button.
        onView(withId(R.id.virtual_card_enrollment_button)).perform(click());
        // Verify that the Virtual Card enrollment button still shows text for enrollment and that
        // the button is disabled.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(Matchers.not(isEnabled())));

        // Verify that the native enroll method was called with the correct parameters.
        ArgumentCaptor<Callback<VirtualCardEnrollmentFields>> callbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .initVirtualCardEnrollment(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        ArgumentMatchers.eq(234L),
                        callbackArgumentCaptor.capture());
        // Return VirtualCardEnrollmentFields via the callback to show the dialog.
        Callback<VirtualCardEnrollmentFields> virtualCardEnrollmentFieldsCallback =
                callbackArgumentCaptor.getValue();
        VirtualCardEnrollmentFields fakeVirtualCardEnrollmentFields =
                VirtualCardEnrollmentFields.create("Visa", "1234", 0, new GURL(""));
        fakeVirtualCardEnrollmentFields.mGoogleLegalMessages.add(new LegalMessageLine("google"));
        fakeVirtualCardEnrollmentFields.mIssuerLegalMessages.add(new LegalMessageLine("issuer"));
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        virtualCardEnrollmentFieldsCallback.onResult(
                                fakeVirtualCardEnrollmentFields));

        // Verify that the dialog was displayed.
        onView(withId(R.id.dialog_title)).inRoot(isDialog()).check(matches(isDisplayed()));

        // Click positive button on enrollment dialog.
        onView(withId(R.id.positive_button)).perform(click());

        // Verify that enrollment is called with the correct parameters when the user clicks the
        // positive button on the dialog.
        ArgumentCaptor<Callback<Boolean>> booleanCallbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .enrollOfferedVirtualCard(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        booleanCallbackArgumentCaptor.capture());
        // Return enrollment update status "failure" via the callback.
        Callback<Boolean> virtualCardEnrollmentUpdateResponseCallback =
                booleanCallbackArgumentCaptor.getValue();
        ThreadUtils.runOnUiThreadBlocking(
                () -> virtualCardEnrollmentUpdateResponseCallback.onResult(false));

        // Verify that the Virtual Card enrollment button again allows enrollment.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(isEnabled()));

        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    public void virtualCardUnenrolledAndEligible_virtualCardAddButtonClicked_enrollRejected()
            throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD.getGUID()));
        // Verify that the native delegate was initialized properly.
        verify(mNativeMock).init(any(Profile.class));

        // Verify that the Virtual Card enrollment button is shown and allows enrollment.
        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(isEnabled()));

        // Press the enrollment button.
        onView(withId(R.id.virtual_card_enrollment_button)).perform(click());
        // Verify that the Virtual Card enrollment button still shows text for enrollment and that
        // the button is disabled.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(Matchers.not(isEnabled())));

        // Verify that the native enroll method was called with the correct parameters.
        ArgumentCaptor<Callback<VirtualCardEnrollmentFields>> callbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .initVirtualCardEnrollment(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        ArgumentMatchers.eq(234L),
                        callbackArgumentCaptor.capture());
        // Return VirtualCardEnrollmentFields via the callback to show the dialog.
        Callback<VirtualCardEnrollmentFields> virtualCardEnrollmentFieldsCallback =
                callbackArgumentCaptor.getValue();
        VirtualCardEnrollmentFields fakeVirtualCardEnrollmentFields =
                VirtualCardEnrollmentFields.create("Visa", "1234", 0, new GURL(""));
        fakeVirtualCardEnrollmentFields.mGoogleLegalMessages.add(new LegalMessageLine("google"));
        fakeVirtualCardEnrollmentFields.mIssuerLegalMessages.add(new LegalMessageLine("issuer"));
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        virtualCardEnrollmentFieldsCallback.onResult(
                                fakeVirtualCardEnrollmentFields));

        // Verify that the dialog was displayed.
        onView(withId(R.id.dialog_title)).inRoot(isDialog()).check(matches(isDisplayed()));

        // Click negative button on enrollment dialog.
        // Verify that enrollment dialog rejection is recorded.
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Autofill.VirtualCard.SettingsPageEnrollment", false);
        onView(withId(R.id.negative_button)).perform(click());
        histogram.assertExpected();

        // Verify that the Virtual Card enrollment button again allows enrollment.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(isEnabled()));
        // Verify that enrollment is not called when the user does not click the positive button on
        // the dialog.
        verify(mNativeMock, times(0))
                .enrollOfferedVirtualCard(
                        eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE), any(Callback.class));

        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    @DisabledTest(message = "https://crbug.com/1368548")
    public void
            virtualCardUnenrolledAndEligible_virtualCardAddButtonClicked_enrollAccepted_editorExited()
                    throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_UNENROLLED_AND_ELIGIBLE_CARD.getGUID()));
        // Verify that the native delegate was initialized properly.
        verify(mNativeMock).init(any(Profile.class));

        // Verify that the Virtual Card enrollment button is shown and allows enrollment.
        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(isEnabled()));

        // Press the enrollment button.
        onView(withId(R.id.virtual_card_enrollment_button)).perform(click());

        // Verify that the native enroll method was called with the correct parameters.
        ArgumentCaptor<Callback<VirtualCardEnrollmentFields>> callbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .initVirtualCardEnrollment(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        ArgumentMatchers.eq(234L),
                        callbackArgumentCaptor.capture());
        // Return VirtualCardEnrollmentFields via the callback to show the dialog.
        Callback<VirtualCardEnrollmentFields> virtualCardEnrollmentFieldsCallback =
                callbackArgumentCaptor.getValue();
        VirtualCardEnrollmentFields fakeVirtualCardEnrollmentFields =
                VirtualCardEnrollmentFields.create("Visa", "1234", 0, new GURL(""));
        fakeVirtualCardEnrollmentFields.mGoogleLegalMessages.add(new LegalMessageLine("google"));
        fakeVirtualCardEnrollmentFields.mIssuerLegalMessages.add(new LegalMessageLine("issuer"));
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        virtualCardEnrollmentFieldsCallback.onResult(
                                fakeVirtualCardEnrollmentFields));

        // Verify that the dialog was displayed.
        onView(withId(R.id.dialog_title)).inRoot(isDialog()).check(matches(isDisplayed()));

        // Click positive button on enrollment dialog.
        onView(withId(R.id.positive_button)).perform(click());

        // Exit the editor.
        finishAndWaitForActivity(activity);

        // Verify that the native delegate is not cleaned up while the server call is in progress.
        verify(mNativeMock, never()).cleanup(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE);

        // Verify that enrollment is called with the correct parameters even after the editor is
        // closed.
        ArgumentCaptor<Callback<Boolean>> booleanCallbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .enrollOfferedVirtualCard(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        booleanCallbackArgumentCaptor.capture());
        // Return enrollment update status "successful" via the callback.
        Callback<Boolean> virtualCardEnrollmentUpdateResponseCallback =
                booleanCallbackArgumentCaptor.getValue();
        ThreadUtils.runOnUiThreadBlocking(
                () -> virtualCardEnrollmentUpdateResponseCallback.onResult(true));

        // Ensure that the callback is run after receiving the server response and that the native
        // delegate is cleaned up.
        verify(mNativeMock).cleanup(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE);
    }

    @Test
    @MediumTest
    public void virtualCardEnrolled_virtualCardRemoveButtonClicked_dialogShown() throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getGUID()));

        // Verify that the Virtual Card enrollment button is shown and allows unenrollment.
        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)))
                .check(matches(isEnabled()));

        // Press the unenrollment button.
        onView(withId(R.id.virtual_card_enrollment_button)).perform(click());
        // Verify that the unenroll dialog is shown.
        onView(withText(R.string.autofill_credit_card_editor_virtual_card_unenroll_dialog_title))
                .check(matches(isDisplayed()));
        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    public void virtualCardEnrolled_virtualCardRemoveButtonClicked_unenrollCancelled()
            throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getGUID()));

        // Verify that the Virtual Card enrollment button is shown and allows unenrollment.
        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)))
                .check(matches(isEnabled()));

        // Press the unenrollment button.
        // Verify that unenrollment button click is recorded.
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Autofill.SettingsPage.ButtonClicked.VirtualCard.VirtualCardUnenroll",
                        true);
        onView(withId(R.id.virtual_card_enrollment_button)).perform(click());
        histogram.assertExpected();

        // Verify that the unenroll dialog is shown.
        onView(withText(R.string.autofill_credit_card_editor_virtual_card_unenroll_dialog_title))
                .check(matches(isDisplayed()));

        // Click the Cancel button.
        // Verify that unenrollment dialog rejection is recorded.
        histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Autofill.VirtualCard.SettingsPageUnenrollment", false);
        onView(withText(android.R.string.cancel)).perform(click());
        histogram.assertExpected();

        // Verify that the Virtual card enrollment button still allows unenrollment.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)))
                .check(matches(isEnabled()));

        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    public void
            virtualCardEnrolled_virtualCardRemoveButtonClicked_unenrollAccepted_unenrollmentSuccessful()
                    throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getGUID()));

        // Verify that the native delegate was initialized properly.
        verify(mNativeMock).init(any(Profile.class));

        // Verify that the Virtual Card enrollment button is shown and allows unenrollment.
        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)))
                .check(matches(isEnabled()));

        // Press the unenrollment button.
        onView(withId(R.id.virtual_card_enrollment_button)).perform(click());

        // Verify that the unenroll dialog is shown.
        onView(withText(R.string.autofill_credit_card_editor_virtual_card_unenroll_dialog_title))
                .check(matches(isDisplayed()));

        // Click the positive button on unenrollment dialog.
        // Verify that unenrollment dialog acceptance is recorded.
        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Autofill.VirtualCard.SettingsPageUnenrollment", true);
        onView(
                        withText(
                                R.string
                                        .autofill_credit_card_editor_virtual_card_unenroll_dialog_positive_button_label))
                .perform(click());
        histogram.assertExpected();

        // Verify that the Virtual Card enrollment button still shows text for unenrollment and that
        // the button is disabled while waiting for server response.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)))
                .check(matches(Matchers.not(isEnabled())));

        // Verify that native unenroll method is called with the correct parameters when the user
        // clicks the positive button on the dialog.
        ArgumentCaptor<Callback<Boolean>> booleanCallbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .unenrollVirtualCard(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        ArgumentMatchers.eq(123L),
                        booleanCallbackArgumentCaptor.capture());
        // Return enrollment update status "successful" via the callback.
        Callback<Boolean> virtualCardEnrollmentUpdateResponseCallback =
                booleanCallbackArgumentCaptor.getValue();
        ThreadUtils.runOnUiThreadBlocking(
                () -> virtualCardEnrollmentUpdateResponseCallback.onResult(true));

        // Verify that the Virtual Card enrollment button now allows enrollment.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_on_button_label)))
                .check(matches(isEnabled()));

        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    public void
            virtualCardEnrolled_virtualCardRemoveButtonClicked_unenrollAccepted_unenrollmentFailure()
                    throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getGUID()));

        // Verify that the native delegate was initialized properly.
        verify(mNativeMock).init(any(Profile.class));

        // Verify that the Virtual Card enrollment button is shown and allows unenrollment.
        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)))
                .check(matches(isEnabled()));

        // Press the unenrollment button.
        onView(withId(R.id.virtual_card_enrollment_button)).perform(click());

        // Verify that the unenroll dialog is shown.
        onView(withText(R.string.autofill_credit_card_editor_virtual_card_unenroll_dialog_title))
                .check(matches(isDisplayed()));

        // Click the positive button on unenrollment dialog.
        onView(
                        withText(
                                R.string
                                        .autofill_credit_card_editor_virtual_card_unenroll_dialog_positive_button_label))
                .perform(click());

        // Verify that native unenroll method is called with the correct parameters when the user
        // clicks the positive button on the dialog.
        ArgumentCaptor<Callback<Boolean>> booleanCallbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .unenrollVirtualCard(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        ArgumentMatchers.eq(123L),
                        booleanCallbackArgumentCaptor.capture());
        // Return enrollment update status "failure" via the callback.
        Callback<Boolean> virtualCardEnrollmentUpdateResponseCallback =
                booleanCallbackArgumentCaptor.getValue();
        ThreadUtils.runOnUiThreadBlocking(
                () -> virtualCardEnrollmentUpdateResponseCallback.onResult(false));

        // Verify that the Virtual Card enrollment button still allows unenrollment.
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)))
                .check(matches(isEnabled()));

        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    public void virtualCardEnrolled_virtualCardRemoveButtonClicked_unenrollAccepted_editorExited()
            throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getGUID()));

        // Verify that the native delegate was initialized properly.
        verify(mNativeMock).init(any(Profile.class));

        // Verify that the Virtual Card enrollment button is shown and allows unenrollment.
        onView(withId(R.id.virtual_card_ui)).check(matches(isDisplayed()));
        onView(withId(R.id.virtual_card_enrollment_button))
                .check(
                        matches(
                                withText(
                                        R.string
                                                .autofill_card_editor_virtual_card_turn_off_button_label)))
                .check(matches(isEnabled()));

        // Press the unenrollment button.
        onView(withId(R.id.virtual_card_enrollment_button)).perform(click());

        // Verify that the unenroll dialog is shown.
        onView(withText(R.string.autofill_credit_card_editor_virtual_card_unenroll_dialog_title))
                .check(matches(isDisplayed()));

        // Click the positive button on unenrollment dialog.
        onView(
                        withText(
                                R.string
                                        .autofill_credit_card_editor_virtual_card_unenroll_dialog_positive_button_label))
                .perform(click());

        // Exit the editor.
        finishAndWaitForActivity(activity);

        // Verify that the native delegate is not cleaned up while the server call is in progress.
        verify(mNativeMock, never()).cleanup(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE);

        // Verify that unenrollment is called with the correct parameters even after the editor is
        // closed.
        ArgumentCaptor<Callback<Boolean>> booleanCallbackArgumentCaptor =
                ArgumentCaptor.forClass(Callback.class);
        verify(mNativeMock)
                .unenrollVirtualCard(
                        ArgumentMatchers.eq(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE),
                        ArgumentMatchers.eq(123L),
                        booleanCallbackArgumentCaptor.capture());
        // Return enrollment update status "successful" via the callback.
        Callback<Boolean> virtualCardEnrollmentUpdateResponseCallback =
                booleanCallbackArgumentCaptor.getValue();
        ThreadUtils.runOnUiThreadBlocking(
                () -> virtualCardEnrollmentUpdateResponseCallback.onResult(true));

        // Ensure that the callback is run after receiving the server response and that the native
        // delegate is cleaned up.
        verify(mNativeMock).cleanup(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE);
    }

    @Test
    @MediumTest
    public void testAutofillPaymentMethodsDelegateLifecycleEvents() throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getGUID()));

        // Verify that the native delegate was initialized properly.
        verify(mNativeMock).init(any(Profile.class));

        finishAndWaitForActivity(activity);

        // Ensure that the native delegate is cleaned up when the test has finished.
        verify(mNativeMock).cleanup(NATIVE_AUTOFILL_PAYMENTS_METHODS_DELEGATE);
    }

    @Test
    @MediumTest
    public void testCustomUrlForServerCardEditPage() throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getGUID()));
        mSettingsActivityTestRule
                .getFragment()
                .setServerCardEditLinkOpenerCallbackForTesting(mServerCardEditLinkOpenerCallback);

        // Verify that the edit card button is shown and enabled.
        onView(withId(R.id.edit_server_card)).check(matches(isDisplayed()));
        onView(withId(R.id.edit_server_card)).check(matches(isEnabled()));

        // Click the server card edit button.
        onView(withId(R.id.edit_server_card)).perform(click());

        // Verify that the callback to open the custom tab for editing card was called with the
        // expected URL.
        verify(mServerCardEditLinkOpenerCallback)
                .onResult(
                        eq(
                                "https://pay.google.com/pay?p=paymentmethods&utm_source=chrome&utm_medium=settings&utm_campaign=payment_methods&id="
                                        + SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getInstrumentId()));

        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    @Test
    @MediumTest
    @CommandLineFlags.Add({ChromeSwitches.USE_SANDBOX_WALLET_ENVIRONMENT})
    public void testCustomUrlForServerCardEditPage_sandboxEnabled() throws Exception {
        mAutofillTestHelper.addServerCreditCard(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD);

        SettingsActivity activity =
                mSettingsActivityTestRule.startSettingsActivity(
                        fragmentArgs(SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getGUID()));
        mSettingsActivityTestRule
                .getFragment()
                .setServerCardEditLinkOpenerCallbackForTesting(mServerCardEditLinkOpenerCallback);

        // Verify that the edit card button is shown and enabled.
        onView(withId(R.id.edit_server_card)).check(matches(isDisplayed()));
        onView(withId(R.id.edit_server_card)).check(matches(isEnabled()));

        // Click the server card edit button.
        onView(withId(R.id.edit_server_card)).perform(click());

        String kExpectedUrl =
                "https://pay.sandbox.google.com/pay?p=paymentmethods&utm_source=chrome&utm_medium=settings&utm_campaign=payment_methods&id=";
        // Verify that the callback to open the custom tab for editing card was called with the
        // expected URL.
        verify(mServerCardEditLinkOpenerCallback)
                .onResult(eq(kExpectedUrl + SAMPLE_VIRTUAL_CARD_ENROLLED_CARD.getInstrumentId()));

        // Ensure that the native delegate is cleaned up when the test has finished.
        finishAndWaitForActivity(activity);
    }

    private void finishAndWaitForActivity(Activity activity) {
        activity.finish();
        ApplicationTestUtils.waitForActivityState(activity, Stage.DESTROYED);
    }

    private Bundle fragmentArgs(String guid) {
        Bundle args = new Bundle();
        args.putString(AutofillEditorBase.AUTOFILL_GUID, guid);
        return args;
    }

    private ViewAction clickLink() {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                return Matchers.instanceOf(TextView.class);
            }

            @Override
            public String getDescription() {
                return "Clicks on the only link in the view.";
            }

            @Override
            public void perform(UiController uiController, View view) {
                TextView textView = (TextView) view;
                Spanned spannedString = (Spanned) textView.getText();
                ClickableSpan[] spans =
                        spannedString.getSpans(0, spannedString.length(), ClickableSpan.class);
                if (spans.length == 0) {
                    throw new NoMatchingViewException.Builder()
                            .includeViewHierarchy(true)
                            .withRootView(textView)
                            .build();
                }
                Assert.assertEquals("There should be only one clickable link", 1, spans.length);
                spans[0].onClick(view);
            }
        };
    }
}