/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.accounts.cts;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorDescription;
import android.accounts.AuthenticatorException;
import android.accounts.OnAccountsUpdateListener;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.test.AndroidTestCase;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * You can run those unit tests with the following command line:
 *
 *  adb shell am instrument
 *   -e debug false -w
 *   -e class android.accounts.cts.AccountManagerTest
 * android.accounts.cts/android.test.InstrumentationTestRunner
 */
public class AccountManagerTest extends AndroidTestCase {

    public static final String ACCOUNT_NAME = "android.accounts.cts.account.name";
    public static final String ACCOUNT_NAME_OTHER = "android.accounts.cts.account.name.other";

    public static final String ACCOUNT_TYPE = "android.accounts.cts.account.type";
    public static final String ACCOUNT_TYPE_OTHER = "android.accounts.cts.account.type.other";

    public static final String ACCOUNT_PASSWORD = "android.accounts.cts.account.password";

    public static final String AUTH_TOKEN = "mockAuthToken";
    public static final String AUTH_TOKEN_TYPE = "mockAuthTokenType";
    public static final String AUTH_TOKEN_LABEL = "mockAuthTokenLabel";

    public static final String FEATURE_1 = "feature.1";
    public static final String FEATURE_2 = "feature.2";
    public static final String NON_EXISTING_FEATURE = "feature.3";

    public static final String OPTION_NAME_1 = "option.name.1";
    public static final String OPTION_VALUE_1 = "option.value.1";

    public static final String OPTION_NAME_2 = "option.name.2";
    public static final String OPTION_VALUE_2 = "option.value.2";

    public static final String[] REQUIRED_FEATURES = new String[] { FEATURE_1, FEATURE_2 };

    public static final Activity ACTIVITY = new Activity();
    public static final Bundle OPTIONS_BUNDLE = new Bundle();

    public static final Bundle USERDATA_BUNDLE = new Bundle();

    public static final String USERDATA_NAME_1 = "user.data.name.1";
    public static final String USERDATA_NAME_2 = "user.data.name.2";
    public static final String USERDATA_VALUE_1 = "user.data.value.1";
    public static final String USERDATA_VALUE_2 = "user.data.value.2";

    public static final Account ACCOUNT = new Account(ACCOUNT_NAME, ACCOUNT_TYPE);
    public static final Account ACCOUNT_SAME_TYPE = new Account(ACCOUNT_NAME_OTHER, ACCOUNT_TYPE);

    private static MockAccountAuthenticator mockAuthenticator;
    private static final int LATCH_TIMEOUT_MS = 500;
    private static AccountManager am;

    public synchronized static MockAccountAuthenticator getMockAuthenticator(Context context) {
        if (null == mockAuthenticator) {
            mockAuthenticator = new MockAccountAuthenticator(context);
        }
        return mockAuthenticator;
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();

        OPTIONS_BUNDLE.putString(OPTION_NAME_1, OPTION_VALUE_1);
        OPTIONS_BUNDLE.putString(OPTION_NAME_2, OPTION_VALUE_2);

        USERDATA_BUNDLE.putString(USERDATA_NAME_1, USERDATA_VALUE_1);

        getMockAuthenticator(getContext());

        am = AccountManager.get(getContext());
    }

    @Override
    public void tearDown() throws Exception, AuthenticatorException, OperationCanceledException {
        mockAuthenticator.clearData();

        // Need to clean up created account
        assertTrue(removeAccount(am, ACCOUNT, null /* callback */));
        assertTrue(removeAccount(am, ACCOUNT_SAME_TYPE, null /* callback */));

        // need to clean up the authenticator cached data
        mockAuthenticator.clearData();

        super.tearDown();
    }

    private void validateAccountAndAuthTokenResult(Bundle result) {
        assertEquals(ACCOUNT_NAME, result.get(AccountManager.KEY_ACCOUNT_NAME));
        assertEquals(ACCOUNT_TYPE, result.get(AccountManager.KEY_ACCOUNT_TYPE));
        assertEquals(AUTH_TOKEN, result.get(AccountManager.KEY_AUTHTOKEN));
    }

    private void validateAccountAndNoAuthTokenResult(Bundle result) {
        assertEquals(ACCOUNT_NAME, result.get(AccountManager.KEY_ACCOUNT_NAME));
        assertEquals(ACCOUNT_TYPE, result.get(AccountManager.KEY_ACCOUNT_TYPE));
        assertNull(result.get(AccountManager.KEY_AUTHTOKEN));
    }

    private void validateNullResult(Bundle resultBundle) {
        assertNull(resultBundle.get(AccountManager.KEY_ACCOUNT_NAME));
        assertNull(resultBundle.get(AccountManager.KEY_ACCOUNT_TYPE));
        assertNull(resultBundle.get(AccountManager.KEY_AUTHTOKEN));
    }

    private void validateAccountAndAuthTokenType() {
        assertEquals(ACCOUNT_TYPE, mockAuthenticator.getAccountType());
        assertEquals(AUTH_TOKEN_TYPE, mockAuthenticator.getAuthTokenType());
    }

    private void validateFeatures() {
        assertEquals(REQUIRED_FEATURES[0], mockAuthenticator.getRequiredFeatures()[0]);
        assertEquals(REQUIRED_FEATURES[1], mockAuthenticator.getRequiredFeatures()[1]);
    }

    private void validateOptions(Bundle expectedOptions, Bundle actualOptions) {
        if (expectedOptions == null) {
            assertNull(actualOptions);
        } else {
            assertNotNull(actualOptions);
            assertEquals(expectedOptions.get(OPTION_NAME_1), actualOptions.get(OPTION_NAME_1));
            assertEquals(expectedOptions.get(OPTION_NAME_2), actualOptions.get(OPTION_NAME_2));
        }
    }

    private void validateCredentials() {
        assertEquals(ACCOUNT, mockAuthenticator.getAccount());
    }

    private int getAccountsCount() {
        Account[] accounts = am.getAccounts();
        assertNotNull(accounts);
        return accounts.length;
    }

    private Bundle addAccount(AccountManager am, String accountType, String authTokenType,
            String[] requiredFeatures, Bundle options, Activity activity,
            AccountManagerCallback<Bundle> callback, Handler handler) throws
                IOException, AuthenticatorException, OperationCanceledException {

        AccountManagerFuture<Bundle> futureBundle = am.addAccount(
                accountType,
                authTokenType,
                requiredFeatures,
                options,
                activity,
                callback,
                handler);

        Bundle resultBundle = futureBundle.getResult();
        assertTrue(futureBundle.isDone());
        assertNotNull(resultBundle);

        return resultBundle;
    }

    private boolean removeAccount(AccountManager am, Account account,
            AccountManagerCallback<Boolean> callback) throws IOException, AuthenticatorException,
                OperationCanceledException {

        AccountManagerFuture<Boolean> futureBoolean = am.removeAccount(account,
                callback,
                null /* handler */);
        Boolean resultBoolean = futureBoolean.getResult();
        assertTrue(futureBoolean.isDone());

        return resultBoolean;
    }

    private void addAccountExplicitly(Account account, String password, Bundle userdata) {
        assertTrue(am.addAccountExplicitly(account, password, userdata));
    }

    private Bundle getAuthTokenByFeature(String[] features, Activity activity)
            throws IOException, AuthenticatorException, OperationCanceledException {

        AccountManagerFuture<Bundle> futureBundle = am.getAuthTokenByFeatures(ACCOUNT_TYPE,
                AUTH_TOKEN_TYPE,
                features,
                activity,
                OPTIONS_BUNDLE,
                OPTIONS_BUNDLE,
                null /* no callback */,
                null /* no handler */
        );

        Bundle resultBundle = futureBundle.getResult();

        assertTrue(futureBundle.isDone());
        assertNotNull(resultBundle);

        return resultBundle;
    }

    private boolean isAccountPresent(Account[] accounts, Account accountToCheck) {
        if (null == accounts || null == accountToCheck) {
            return false;
        }
        boolean result = false;
        int length = accounts.length;
        for (int n=0; n<length; n++) {
            if(accountToCheck.equals(accounts[n])) {
                result = true;
                break;
            }
        }
        return result;
    }

    /**
     * Test singleton
     */
    public void testGet() {
        assertNotNull(AccountManager.get(getContext()));
    }

    /**
     * Test a basic addAccount()
     */
    public void testAddAccount() throws IOException, AuthenticatorException,
            OperationCanceledException {

        Bundle resultBundle = addAccount(am,
                ACCOUNT_TYPE,
                AUTH_TOKEN_TYPE,
                REQUIRED_FEATURES,
                OPTIONS_BUNDLE,
                ACTIVITY,
                null /* callback */,
                null /* handler */);

        // Assert parameters has been passed correctly
        validateAccountAndAuthTokenType();
        validateFeatures();
        validateOptions(OPTIONS_BUNDLE, mockAuthenticator.mOptionsAddAccount);
        validateOptions(null, mockAuthenticator.mOptionsUpdateCredentials);
        validateOptions(null, mockAuthenticator.mOptionsConfirmCredentials);
        validateOptions(null, mockAuthenticator.mOptionsGetAuthToken);

        // Assert returned result
        validateAccountAndNoAuthTokenResult(resultBundle);
    }

    /**
     * Test addAccount() with callback and handler
     */
    public void testAddAccountWithCallbackAndHandler() throws IOException,
            AuthenticatorException, OperationCanceledException {

        testAddAccountWithCallbackAndHandler(null /* handler */);
        testAddAccountWithCallbackAndHandler(new Handler());
    }

    private void testAddAccountWithCallbackAndHandler(Handler handler) throws IOException,
            AuthenticatorException, OperationCanceledException {

        final CountDownLatch latch = new CountDownLatch(1);

        AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
            public void run(AccountManagerFuture<Bundle> bundleFuture) {
                Bundle resultBundle = null;
                try {
                    resultBundle = bundleFuture.getResult();
                } catch (OperationCanceledException e) {
                    fail("should not throw an OperationCanceledException");
                } catch (IOException e) {
                    fail("should not throw an IOException");
                } catch (AuthenticatorException e) {
                    fail("should not throw an AuthenticatorException");
                }

                // Assert parameters has been passed correctly
                validateAccountAndAuthTokenType();
                validateFeatures();
                validateOptions(OPTIONS_BUNDLE, mockAuthenticator.mOptionsAddAccount);
                validateOptions(null, mockAuthenticator.mOptionsUpdateCredentials);
                validateOptions(null, mockAuthenticator.mOptionsConfirmCredentials);
                validateOptions(null, mockAuthenticator.mOptionsGetAuthToken);

                // Assert return result
                validateAccountAndNoAuthTokenResult(resultBundle);

                latch.countDown();
            }
        };

        addAccount(am,
                ACCOUNT_TYPE,
                AUTH_TOKEN_TYPE,
                REQUIRED_FEATURES,
                OPTIONS_BUNDLE,
                ACTIVITY,
                callback,
                handler);

        // Wait with timeout for the callback to do its work
        try {
            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    /**
     * Test addAccountExplicitly() and removeAccount()
     */
    public void testAddAccountExplicitlyAndRemoveAccount() throws IOException,
            AuthenticatorException, OperationCanceledException {

        final int accountsCount = getAccountsCount();

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        // Assert that we have one more account
        Account[] accounts = am.getAccounts();
        assertNotNull(accounts);
        assertEquals(1 + accountsCount, accounts.length);
        assertTrue(isAccountPresent(am.getAccounts(), ACCOUNT));

        // Need to clean up
        assertTrue(removeAccount(am, ACCOUNT, null /* callback */));

        // and verify that we go back to the initial state
        accounts = am.getAccounts();
        assertNotNull(accounts);
        assertEquals(accountsCount, accounts.length);
    }

    /**
     * Test getAccounts() and getAccountsByType()
     */
    public void testGetAccountsAndGetAccountsByType() throws IOException, AuthenticatorException,
            OperationCanceledException {

        assertEquals(false, isAccountPresent(am.getAccounts(), ACCOUNT));
        assertEquals(false, isAccountPresent(am.getAccounts(), ACCOUNT_SAME_TYPE));

        final int accountsCount = getAccountsCount();

        // Add a first account
        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        // Check that we have the new account
        Account[] accounts = am.getAccounts();
        assertEquals(1 + accountsCount, accounts.length);
        assertEquals(true, isAccountPresent(accounts, ACCOUNT));

        // Add another account
        addAccountExplicitly(ACCOUNT_SAME_TYPE, ACCOUNT_PASSWORD, null /* userData */);

        // Check that we have one more account again
        accounts = am.getAccounts();
        assertEquals(2 + accountsCount, accounts.length);
        assertEquals(true, isAccountPresent(accounts, ACCOUNT_SAME_TYPE));

        // Check if we have one from first type
        accounts = am.getAccountsByType(ACCOUNT_TYPE);
        assertEquals(2, accounts.length);

        // Check if we dont have any account from the other type
        accounts = am.getAccountsByType(ACCOUNT_TYPE_OTHER);
        assertEquals(0, accounts.length);
    }

    /**
     * Test getAuthenticatorTypes()
     */
    public void testGetAuthenticatorTypes() {
        AuthenticatorDescription[] types = am.getAuthenticatorTypes();
        for(AuthenticatorDescription description: types) {
            if (description.type.equals(ACCOUNT_TYPE)) {
                return;
            }
        }
        fail("should have found Authenticator type: " + ACCOUNT_TYPE);
    }

    /**
     * Test setPassword() and getPassword()
     */
    public void testSetAndGetAndClearPassword() {
        // Add a first account
        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        // Check that the password is the one we defined
        assertEquals(ACCOUNT_PASSWORD, am.getPassword(ACCOUNT));

        // Clear the password and check that it is cleared
        am.clearPassword(ACCOUNT);
        assertNull(am.getPassword(ACCOUNT));

        // Reset the password
        am.setPassword(ACCOUNT, ACCOUNT_PASSWORD);

        // Check that the password is the one we defined
        assertEquals(ACCOUNT_PASSWORD, am.getPassword(ACCOUNT));
    }

    /**
     * Test setUserData() and getUserData()
     */
    public void testSetAndGetUserData() {
        // Add a first account
        boolean result = am.addAccountExplicitly(ACCOUNT,
                                ACCOUNT_PASSWORD,
                                USERDATA_BUNDLE);

        assertTrue(result);

        // Check that the UserData is the one we defined
        assertEquals(USERDATA_VALUE_1, am.getUserData(ACCOUNT, USERDATA_NAME_1));

        am.setUserData(ACCOUNT, USERDATA_NAME_2, USERDATA_VALUE_2);

        // Check that the UserData is the one we defined
        assertEquals(USERDATA_VALUE_2, am.getUserData(ACCOUNT, USERDATA_NAME_2));
    }

    /**
     * Test getAccountsByTypeAndFeatures()
     */
    public void testGetAccountsByTypeAndFeatures() throws IOException,
            AuthenticatorException, OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        AccountManagerFuture<Account[]> futureAccounts = am.getAccountsByTypeAndFeatures(
                ACCOUNT_TYPE, REQUIRED_FEATURES, null, null);

        Account[] accounts = futureAccounts.getResult();

        assertNotNull(accounts);
        assertEquals(1, accounts.length);
        assertEquals(true, isAccountPresent(accounts, ACCOUNT));

        futureAccounts = am.getAccountsByTypeAndFeatures(ACCOUNT_TYPE,
                new String[] { NON_EXISTING_FEATURE },
                null /* callback*/,
                null /* handler */);
        accounts = futureAccounts.getResult();

        assertNotNull(accounts);
        assertEquals(0, accounts.length);
    }

    /**
     * Test getAccountsByTypeAndFeatures() with callback and handler
     */
    public void testGetAccountsByTypeAndFeaturesWithCallbackAndHandler() throws IOException,
            AuthenticatorException, OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        testGetAccountsByTypeAndFeaturesWithCallbackAndHandler(null /* handler */);
        testGetAccountsByTypeAndFeaturesWithCallbackAndHandler(new Handler());
    }

    private void testGetAccountsByTypeAndFeaturesWithCallbackAndHandler(Handler handler) throws
            IOException, AuthenticatorException, OperationCanceledException {

        final CountDownLatch latch1 = new CountDownLatch(1);

        AccountManagerCallback<Account[]> callback1 = new AccountManagerCallback<Account[]>() {
            public void run(AccountManagerFuture<Account[]> accountsFuture) {
                try {
                    Account[] accounts = accountsFuture.getResult();
                    assertNotNull(accounts);
                    assertEquals(1, accounts.length);
                    assertEquals(true, isAccountPresent(accounts, ACCOUNT));
                } catch (OperationCanceledException e) {
                    fail("should not throw an OperationCanceledException");
                } catch (IOException e) {
                    fail("should not throw an IOException");
                } catch (AuthenticatorException e) {
                    fail("should not throw an AuthenticatorException");
                } finally {
                  latch1.countDown();
                }
            }
        };

        AccountManagerFuture<Account[]> futureAccounts = am.getAccountsByTypeAndFeatures(
                ACCOUNT_TYPE,
                REQUIRED_FEATURES,
                callback1,
                handler);

        Account[] accounts = futureAccounts.getResult();

        assertNotNull(accounts);
        assertEquals(1, accounts.length);
        assertEquals(true, isAccountPresent(accounts, ACCOUNT));

        // Wait with timeout for the callback to do its work
        try {
            latch1.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }

        final CountDownLatch latch2 = new CountDownLatch(1);

        AccountManagerCallback<Account[]> callback2 = new AccountManagerCallback<Account[]>() {
            public void run(AccountManagerFuture<Account[]> accountsFuture) {
                try {
                    Account[] accounts = accountsFuture.getResult();
                    assertNotNull(accounts);
                    assertEquals(0, accounts.length);
                } catch (OperationCanceledException e) {
                    fail("should not throw an OperationCanceledException");
                } catch (IOException e) {
                    fail("should not throw an IOException");
                } catch (AuthenticatorException e) {
                    fail("should not throw an AuthenticatorException");
                } finally {
                  latch2.countDown();
                }
            }
        };

        accounts = null;

        futureAccounts = am.getAccountsByTypeAndFeatures(ACCOUNT_TYPE,
                new String[] { NON_EXISTING_FEATURE },
                callback2,
                handler);

        accounts = futureAccounts.getResult();
        assertNotNull(accounts);
        assertEquals(0, accounts.length);

        // Wait with timeout for the callback to do its work
        try {
            latch2.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    /**
     * Test setAuthToken() and peekAuthToken()
     */
    public void testSetAndPeekAndInvalidateAuthToken() {
        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        am.setAuthToken(ACCOUNT, AUTH_TOKEN_TYPE, AUTH_TOKEN);

        // Ask for the AuthToken
        String token = am.peekAuthToken(ACCOUNT, AUTH_TOKEN_TYPE);
        assertNotNull(token);
        assertEquals(AUTH_TOKEN, token);

        am.invalidateAuthToken(ACCOUNT_TYPE, AUTH_TOKEN);
        token = am.peekAuthToken(ACCOUNT, AUTH_TOKEN_TYPE);
        assertNull(token);
    }

    /**
     * Test blockingGetAuthToken()
     */
    public void testBlockingGetAuthToken() throws IOException, AuthenticatorException,
            OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null);

        String token = am.blockingGetAuthToken(ACCOUNT,
                AUTH_TOKEN_TYPE,
                false /* no failure notification */);

        // Ask for the AuthToken
        assertNotNull(token);
        assertEquals(AUTH_TOKEN, token);
    }

    /**
     * Test getAuthToken()
     */
    public void testGetAuthToken() throws IOException, AuthenticatorException,
            OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        AccountManagerFuture<Bundle> futureBundle = am.getAuthToken(ACCOUNT,
                AUTH_TOKEN_TYPE,
                false /* no failure notification */,
                null /* no callback */,
                null /* no handler */
        );

        Bundle resultBundle = futureBundle.getResult();

        assertTrue(futureBundle.isDone());
        assertNotNull(resultBundle);

        // Assert returned result
        validateAccountAndAuthTokenResult(resultBundle);
    }

    /**
     * Test getAuthToken() with callback and handler
     */
    public void testGetAuthTokenWithCallbackAndHandler() throws IOException, AuthenticatorException,
            OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        testGetAuthTokenWithCallbackAndHandler(null /* handler */);
        testGetAuthTokenWithCallbackAndHandler(new Handler());
    }

    private void testGetAuthTokenWithCallbackAndHandler(Handler handler) throws IOException,
            AuthenticatorException, OperationCanceledException {

        final CountDownLatch latch = new CountDownLatch(1);

        AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
            public void run(AccountManagerFuture<Bundle> bundleFuture) {

                Bundle resultBundle = null;
                try {
                    resultBundle = bundleFuture.getResult();

                    // Assert returned result
                    validateAccountAndAuthTokenResult(resultBundle);

                } catch (OperationCanceledException e) {
                    fail("should not throw an OperationCanceledException");
                } catch (IOException e) {
                    fail("should not throw an IOException");
                } catch (AuthenticatorException e) {
                    fail("should not throw an AuthenticatorException");
                }
                finally {
                    latch.countDown();
                }
            }
        };

        AccountManagerFuture<Bundle> futureBundle = am.getAuthToken(ACCOUNT,
                AUTH_TOKEN_TYPE,
                false /* no failure notification */,
                callback,
                handler
        );

        Bundle resultBundle = futureBundle.getResult();

        assertTrue(futureBundle.isDone());
        assertNotNull(resultBundle);

        // Wait with timeout for the callback to do its work
        try {
            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    /**
     * test getAuthToken() with options
     */
    public void testGetAuthTokenWithOptions() throws IOException, AuthenticatorException,
            OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        AccountManagerFuture<Bundle> futureBundle = am.getAuthToken(ACCOUNT,
                AUTH_TOKEN_TYPE,
                OPTIONS_BUNDLE,
                ACTIVITY,
                null /* no callback */,
                null /* no handler */
        );

        Bundle resultBundle = futureBundle.getResult();

        assertTrue(futureBundle.isDone());
        assertNotNull(resultBundle);

        // Assert returned result
        validateAccountAndAuthTokenResult(resultBundle);

        validateOptions(null, mockAuthenticator.mOptionsAddAccount);
        validateOptions(null, mockAuthenticator.mOptionsUpdateCredentials);
        validateOptions(null, mockAuthenticator.mOptionsConfirmCredentials);
        validateOptions(OPTIONS_BUNDLE, mockAuthenticator.mOptionsGetAuthToken);

    }

    /**
     * test getAuthToken() with options and callback and handler
     */
    public void testGetAuthTokenWithOptionsAndCallback() throws IOException,
            AuthenticatorException, OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        testGetAuthTokenWithOptionsAndCallbackAndHandler(null /* handler */);
        testGetAuthTokenWithOptionsAndCallbackAndHandler(new Handler());
    }

    private void testGetAuthTokenWithOptionsAndCallbackAndHandler(Handler handler) throws
            IOException, AuthenticatorException, OperationCanceledException {

        final CountDownLatch latch = new CountDownLatch(1);

        AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
            public void run(AccountManagerFuture<Bundle> bundleFuture) {

                Bundle resultBundle = null;
                try {
                    resultBundle = bundleFuture.getResult();

                    // Assert returned result
                    validateAccountAndAuthTokenResult(resultBundle);

                } catch (OperationCanceledException e) {
                    fail("should not throw an OperationCanceledException");
                } catch (IOException e) {
                    fail("should not throw an IOException");
                } catch (AuthenticatorException e) {
                    fail("should not throw an AuthenticatorException");
                }
                finally {
                    latch.countDown();
                }
            }
        };

        AccountManagerFuture<Bundle> futureBundle = am.getAuthToken(ACCOUNT,
                AUTH_TOKEN_TYPE,
                OPTIONS_BUNDLE,
                ACTIVITY,
                callback,
                handler
        );

        Bundle resultBundle = futureBundle.getResult();

        assertTrue(futureBundle.isDone());
        assertNotNull(resultBundle);

        // Wait with timeout for the callback to do its work
        try {
            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    /**
     * Test getAuthTokenByFeatures()
     */
    public void testGetAuthTokenByFeatures() throws IOException, AuthenticatorException,
            OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        Bundle resultBundle = getAuthTokenByFeature(
                new String[] { NON_EXISTING_FEATURE },
                null /* activity */
        );

        // Assert returned result
        validateNullResult(resultBundle);

        validateOptions(null, mockAuthenticator.mOptionsAddAccount);
        validateOptions(null, mockAuthenticator.mOptionsUpdateCredentials);
        validateOptions(null, mockAuthenticator.mOptionsConfirmCredentials);
        validateOptions(null, mockAuthenticator.mOptionsGetAuthToken);

        mockAuthenticator.clearData();

        // Now test with existing features and an activity
        resultBundle = getAuthTokenByFeature(
                new String[] { NON_EXISTING_FEATURE },
                ACTIVITY
        );

        // Assert returned result
        validateAccountAndAuthTokenResult(resultBundle);

        validateOptions(OPTIONS_BUNDLE, mockAuthenticator.mOptionsAddAccount);
        validateOptions(null, mockAuthenticator.mOptionsUpdateCredentials);
        validateOptions(null, mockAuthenticator.mOptionsConfirmCredentials);
        validateOptions(null, mockAuthenticator.mOptionsGetAuthToken);

        mockAuthenticator.clearData();

        // Now test with existing features and no activity
        resultBundle = getAuthTokenByFeature(
                REQUIRED_FEATURES,
                null /* activity */
        );

        // Assert returned result
        validateAccountAndAuthTokenResult(resultBundle);

        validateOptions(null, mockAuthenticator.mOptionsAddAccount);
        validateOptions(null, mockAuthenticator.mOptionsUpdateCredentials);
        validateOptions(null, mockAuthenticator.mOptionsConfirmCredentials);
        validateOptions(null, mockAuthenticator.mOptionsGetAuthToken);

        mockAuthenticator.clearData();

        // Now test with existing features and an activity
        resultBundle = getAuthTokenByFeature(
                REQUIRED_FEATURES,
                ACTIVITY
        );

        // Assert returned result
        validateAccountAndAuthTokenResult(resultBundle);

        validateOptions(null, mockAuthenticator.mOptionsAddAccount);
        validateOptions(null, mockAuthenticator.mOptionsUpdateCredentials);
        validateOptions(null, mockAuthenticator.mOptionsConfirmCredentials);
        validateOptions(null, mockAuthenticator.mOptionsGetAuthToken);

    }

    /**
     * Test confirmCredentials()
     */
    public void testConfirmCredentials() throws IOException, AuthenticatorException,
            OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        AccountManagerFuture<Bundle> futureBundle = am.confirmCredentials(ACCOUNT,
                OPTIONS_BUNDLE,
                ACTIVITY,
                null /* callback*/,
                null /* handler */);

        futureBundle.getResult();

        // Assert returned result
        validateCredentials();
    }

    /**
     * Test confirmCredentials() with callback
     */
    public void testConfirmCredentialsWithCallbackAndHandler() throws IOException,
            AuthenticatorException, OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        testConfirmCredentialsWithCallbackAndHandler(null /* handler */);
        testConfirmCredentialsWithCallbackAndHandler(new Handler());
    }

    private void testConfirmCredentialsWithCallbackAndHandler(Handler handler) throws IOException,
            AuthenticatorException, OperationCanceledException {

        final CountDownLatch latch = new CountDownLatch(1);

        AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
            public void run(AccountManagerFuture<Bundle> bundleFuture) {

                Bundle resultBundle = null;
                try {
                    resultBundle = bundleFuture.getResult();

                    // Assert returned result
                    validateCredentials();

                    assertNull(resultBundle.get(AccountManager.KEY_AUTHTOKEN));
                } catch (OperationCanceledException e) {
                    fail("should not throw an OperationCanceledException");
                } catch (IOException e) {
                    fail("should not throw an IOException");
                } catch (AuthenticatorException e) {
                    fail("should not throw an AuthenticatorException");
                }
                finally {
                    latch.countDown();
                }
            }
        };

        AccountManagerFuture<Bundle> futureBundle = am.confirmCredentials(ACCOUNT,
                OPTIONS_BUNDLE,
                ACTIVITY,
                callback,
                handler);

        futureBundle.getResult();

        // Wait with timeout for the callback to do its work
        try {
            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    /**
     * Test updateCredentials()
     */
    public void testUpdateCredentials() throws IOException, AuthenticatorException,
            OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        AccountManagerFuture<Bundle> futureBundle = am.updateCredentials(ACCOUNT,
                AUTH_TOKEN_TYPE,
                OPTIONS_BUNDLE,
                ACTIVITY,
                null /* callback*/,
                null /* handler */);

        Bundle result = futureBundle.getResult();

        validateAccountAndNoAuthTokenResult(result);

        // Assert returned result
        validateCredentials();
    }

    /**
     * Test updateCredentials() with callback and handler
     */
    public void testUpdateCredentialsWithCallbackAndHandler() throws IOException,
            AuthenticatorException, OperationCanceledException {

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        testUpdateCredentialsWithCallbackAndHandler(null /* handler */);
        testUpdateCredentialsWithCallbackAndHandler(new Handler());
    }

    private void testUpdateCredentialsWithCallbackAndHandler(Handler handler) throws IOException,
            AuthenticatorException, OperationCanceledException {

        final CountDownLatch latch = new CountDownLatch(1);

        AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
            public void run(AccountManagerFuture<Bundle> bundleFuture) {

                Bundle resultBundle = null;
                try {
                    resultBundle = bundleFuture.getResult();

                    // Assert returned result
                    validateCredentials();
                    assertNull(resultBundle.get(AccountManager.KEY_AUTHTOKEN));

                } catch (OperationCanceledException e) {
                    fail("should not throw an OperationCanceledException");
                } catch (IOException e) {
                    fail("should not throw an IOException");
                } catch (AuthenticatorException e) {
                    fail("should not throw an AuthenticatorException");
                }
                finally {
                    latch.countDown();
                }
            }
        };

        AccountManagerFuture<Bundle> futureBundle = am.updateCredentials(ACCOUNT,
                AUTH_TOKEN_TYPE,
                OPTIONS_BUNDLE,
                ACTIVITY,
                callback,
                handler);

        futureBundle.getResult();

        // Wait with timeout for the callback to do its work
        try {
            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    /**
     * Test editProperties()
     */
    public void testEditProperties() throws IOException, AuthenticatorException,
            OperationCanceledException {

        AccountManagerFuture<Bundle> futureBundle = am.editProperties(ACCOUNT_TYPE,
                ACTIVITY,
                null /* callback */,
                null /* handler*/);

        Bundle result = futureBundle.getResult();

        validateAccountAndNoAuthTokenResult(result);

        // Assert returned result
        assertEquals(ACCOUNT_TYPE, mockAuthenticator.getAccountType());
    }

    /**
     * Test editProperties() with callback and handler
     */
    public void testEditPropertiesWithCallbackAndHandler() {
        testEditPropertiesWithCallbackAndHandler(null /* handler */);
        testEditPropertiesWithCallbackAndHandler(new Handler());
    }

    private void testEditPropertiesWithCallbackAndHandler(Handler handler) {
        final CountDownLatch latch = new CountDownLatch(1);

        AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
            public void run(AccountManagerFuture<Bundle> bundleFuture) {
                try {
                    // Assert returned result
                    assertEquals(ACCOUNT_TYPE, mockAuthenticator.getAccountType());
                }
                finally {
                    latch.countDown();
                }
            }
        };

        AccountManagerFuture<Bundle> futureBundle = am.editProperties(ACCOUNT_TYPE,
                ACTIVITY,
                callback,
                handler);

        // Wait with timeout for the callback to do its work
        try {
            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    /**
     * Test addOnAccountsUpdatedListener() with handler
     */
    public void testAddOnAccountsUpdatedListenerWithHandler() throws IOException,
            AuthenticatorException, OperationCanceledException {

        testAddOnAccountsUpdatedListenerWithHandler(null /* handler */,
                false /* updateImmediately */);

        // Need to cleanup intermediate state
        assertTrue(removeAccount(am, ACCOUNT, null /* callback */));

        testAddOnAccountsUpdatedListenerWithHandler(null /* handler */,
                true /* updateImmediately */);

        // Need to cleanup intermediate state
        assertTrue(removeAccount(am, ACCOUNT, null /* callback */));

        testAddOnAccountsUpdatedListenerWithHandler(new Handler(),
                false /* updateImmediately */);

        // Need to cleanup intermediate state
        assertTrue(removeAccount(am, ACCOUNT, null /* callback */));

        testAddOnAccountsUpdatedListenerWithHandler(new Handler(),
                true /* updateImmediately */);
    }

    private void testAddOnAccountsUpdatedListenerWithHandler(Handler handler,
            boolean updateImmediately) {

        final CountDownLatch latch = new CountDownLatch(1);

        OnAccountsUpdateListener listener = new OnAccountsUpdateListener() {
            public void onAccountsUpdated(Account[] accounts) {
                latch.countDown();
            }
        };

        // Add a listener
        am.addOnAccountsUpdatedListener(listener,
                handler,
                updateImmediately);

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        // Wait with timeout for the callback to do its work
        try {
            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }

        // Cleanup
        am.removeOnAccountsUpdatedListener(listener);
    }

    /**
     * Test removeOnAccountsUpdatedListener() with handler
     */
    public void testRemoveOnAccountsUpdatedListener() throws IOException, AuthenticatorException,
            OperationCanceledException {

        testRemoveOnAccountsUpdatedListenerWithHandler(null /* handler */);

        // Need to cleanup intermediate state
        assertTrue(removeAccount(am, ACCOUNT, null /* callback */));

        testRemoveOnAccountsUpdatedListenerWithHandler(new Handler());
    }

    private void testRemoveOnAccountsUpdatedListenerWithHandler(Handler handler) {
        final CountDownLatch latch = new CountDownLatch(1);

        OnAccountsUpdateListener listener = new OnAccountsUpdateListener() {
            public void onAccountsUpdated(Account[] accounts) {
                fail("should not be called");
            }
        };

        // First add a listener
        am.addOnAccountsUpdatedListener(listener,
                handler,
                false /* updateImmediately */);

        // Then remove the listener
        am.removeOnAccountsUpdatedListener(listener);

        addAccountExplicitly(ACCOUNT, ACCOUNT_PASSWORD, null /* userData */);

        // Wait with timeout for the callback to do its work
        try {
            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    /**
     * Test hasFeature
     */
    public void testHasFeature()
            throws IOException, AuthenticatorException, OperationCanceledException {

        assertHasFeature(null /* handler */);
        assertHasFeature(new Handler());

        assertHasFeatureWithCallback(null /* handler */);
        assertHasFeatureWithCallback(new Handler());
    }

    private void assertHasFeature(Handler handler)
            throws IOException, AuthenticatorException, OperationCanceledException {
        Bundle resultBundle = addAccount(am,
                ACCOUNT_TYPE,
                AUTH_TOKEN_TYPE,
                REQUIRED_FEATURES,
                OPTIONS_BUNDLE,
                ACTIVITY,
                null /* callback */,
                null /* handler */);

        // Assert parameters has been passed correctly
        validateAccountAndAuthTokenType();
        validateFeatures();

        AccountManagerFuture<Boolean> booleanFuture = am.hasFeatures(ACCOUNT,
                new String[]{FEATURE_1},
                null /* callback */,
                handler);
        assertTrue(booleanFuture.getResult());

        booleanFuture = am.hasFeatures(ACCOUNT,
                new String[]{FEATURE_2},
                null /* callback */,
                handler);
        assertTrue(booleanFuture.getResult());

        booleanFuture = am.hasFeatures(ACCOUNT,
                new String[]{FEATURE_1, FEATURE_2},
                null /* callback */,
                handler);
        assertTrue(booleanFuture.getResult());

        booleanFuture = am.hasFeatures(ACCOUNT,
                new String[]{NON_EXISTING_FEATURE},
                null /* callback */,
                handler);
        assertFalse(booleanFuture.getResult());

        booleanFuture = am.hasFeatures(ACCOUNT,
                new String[]{NON_EXISTING_FEATURE, FEATURE_1},
                null /* callback */,
                handler);
        assertFalse(booleanFuture.getResult());

        booleanFuture = am.hasFeatures(ACCOUNT,
                new String[]{NON_EXISTING_FEATURE, FEATURE_1, FEATURE_2},
                null /* callback */,
                handler);
        assertFalse(booleanFuture.getResult());
    }

    private AccountManagerCallback<Boolean> getAssertTrueCallback(final CountDownLatch latch) {
        return new AccountManagerCallback<Boolean>() {
            public void run(AccountManagerFuture<Boolean> booleanFuture) {
                try {
                    // Assert returned result should be TRUE
                    assertTrue(booleanFuture.getResult());
                } catch (Exception e) {
                    fail("Exception: " + e);
                } finally {
                    latch.countDown();
                }
            }
        };
    }

    private AccountManagerCallback<Boolean> getAssertFalseCallback(final CountDownLatch latch) {
        return new AccountManagerCallback<Boolean>() {
            public void run(AccountManagerFuture<Boolean> booleanFuture) {
                try {
                    // Assert returned result should be FALSE
                    assertFalse(booleanFuture.getResult());
                } catch (Exception e) {
                    fail("Exception: " + e);
                } finally {
                    latch.countDown();
                }
            }
        };
    }

    private void waitForLatch(final CountDownLatch latch) {
        // Wait with timeout for the callback to do its work
        try {
            latch.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            fail("should not throw an InterruptedException");
        }
    }

    private void assertHasFeatureWithCallback(Handler handler)
            throws IOException, AuthenticatorException, OperationCanceledException {
        Bundle resultBundle = addAccount(am,
                ACCOUNT_TYPE,
                AUTH_TOKEN_TYPE,
                REQUIRED_FEATURES,
                OPTIONS_BUNDLE,
                ACTIVITY,
                null /* callback */,
                null /* handler */);

        // Assert parameters has been passed correctly
        validateAccountAndAuthTokenType();
        validateFeatures();

        CountDownLatch latch = new CountDownLatch(1);
        am.hasFeatures(ACCOUNT,
                new String[]{FEATURE_1},
                getAssertTrueCallback(latch),
                handler);
        waitForLatch(latch);

        latch = new CountDownLatch(1);
        am.hasFeatures(ACCOUNT,
                new String[]{FEATURE_2},
                getAssertTrueCallback(latch),
                handler);
        waitForLatch(latch);

        latch = new CountDownLatch(1);
        am.hasFeatures(ACCOUNT,
                new String[]{FEATURE_1, FEATURE_2},
                getAssertTrueCallback(latch),
                handler);
        waitForLatch(latch);

        latch = new CountDownLatch(1);
        am.hasFeatures(ACCOUNT,
                new String[]{NON_EXISTING_FEATURE},
                getAssertFalseCallback(latch),
                handler);
        waitForLatch(latch);

        latch = new CountDownLatch(1);
        am.hasFeatures(ACCOUNT,
                new String[]{NON_EXISTING_FEATURE, FEATURE_1},
                getAssertFalseCallback(latch),
                handler);
        waitForLatch(latch);

        latch = new CountDownLatch(1);
        am.hasFeatures(ACCOUNT,
                new String[]{NON_EXISTING_FEATURE, FEATURE_1, FEATURE_2},
                getAssertFalseCallback(latch),
                handler);
        waitForLatch(latch);
    }
}
