/*
 * Copyright (c) 2015-present, Parse, LLC.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
package com.parse;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;

import android.Manifest;
import android.os.Parcel;
import com.parse.boltsinternal.Capture;
import com.parse.boltsinternal.Continuation;
import com.parse.boltsinternal.Task;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.LooperMode;

// For ParseExecutors.main()
@RunWith(RobolectricTestRunner.class)
@LooperMode(PAUSED)
public class ParseUserTest extends ResetPluginsParseTest {

    @Rule public final ExpectedException thrown = ExpectedException.none();

    private static void setLazy(ParseUser user) {
        Map<String, String> anonymousAuthData = new HashMap<>();
        anonymousAuthData.put("anonymousToken", "anonymousTest");
        user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();
        ParseObject.registerSubclass(ParseUser.class);
        ParseObject.registerSubclass(ParseSession.class);
    }

    @After
    public void tearDown() throws Exception {
        super.tearDown();
        if (ParsePlugins.get() != null) {
            ParseCurrentInstallationController installationController =
                    ParseCorePlugins.getInstance().getCurrentInstallationController();
            if (installationController != null) {
                installationController.clearFromDisk();
            }
            ParseCurrentUserController userController =
                    ParseCorePlugins.getInstance().getCurrentUserController();
            if (userController != null) {
                userController.clearFromDisk();
            }
        }
        Parse.destroy();
    }

    // region Parcelable

    @Test
    public void testImmutableKeys() {
        ParseUser user = new ParseUser();
        user.put("foo", "bar");

        try {
            user.put("sessionToken", "blah");
        } catch (IllegalArgumentException e) {
            assertTrue(e.getMessage().contains("Cannot modify"));
        }

        try {
            user.remove("sessionToken");
        } catch (IllegalArgumentException e) {
            assertTrue(e.getMessage().contains("Cannot modify"));
        }

        try {
            user.removeAll("sessionToken", Collections.emptyList());
        } catch (IllegalArgumentException e) {
            assertTrue(e.getMessage().contains("Cannot modify"));
        }
    }

    @Test
    public void testOnSaveRestoreState() {
        ParseUser user = new ParseUser();
        user.setObjectId("objId");
        user.setIsCurrentUser(true);

        Parcel parcel = Parcel.obtain();
        user.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);
        user = (ParseUser) ParseObject.CREATOR.createFromParcel(parcel);
        assertTrue(user.isCurrentUser());
    }

    // endregion

    // region SignUpAsync

    @Test
    public void testParcelableState() {
        ParseUser.State state = new ParseUser.State.Builder().objectId("test").isNew(true).build();
        ParseUser user = ParseObject.from(state);
        assertTrue(user.isNew());

        Parcel parcel = Parcel.obtain();
        user.writeToParcel(parcel, 0);
        parcel.setDataPosition(0);
        user = (ParseUser) ParseObject.CREATOR.createFromParcel(parcel);
        assertTrue(user.isNew());
    }

    @Test
    public void testSignUpAsyncWithNoUserName() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();

        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Username cannot be missing or blank");

        ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null)));
    }

    @Test
    public void testSignUpAsyncWithNoPassword() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();
        user.setUsername("userName");

        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Password cannot be missing or blank");

        ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null)));
    }

    @Test
    public void testSignUpAsyncWithObjectIdSetAndAuthDataNotSet() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser.State userState = new ParseUser.State.Builder().objectId("test").build();
        ParseUser user = ParseObject.from(userState);
        user.setUsername("userName");
        user.setPassword("password");

        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Cannot sign up a user that has already signed up.");

        ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null)));
    }

    @Test
    public void testSignUpAsyncWithObjectIdSetAndAuthDataSet() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = mock(ParseUser.class);
        when(currentUser.getSessionToken()).thenReturn("sessionToken");
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser.State userState =
                new ParseUser.State.Builder()
                        .objectId("test")
                        .putAuthData(ParseAnonymousUtils.AUTH_TYPE, null)
                        .build();
        ParseUser user = ParseObject.from(userState);
        user.setUsername("userName");
        user.setPassword("password");
        // TODO (mengyan): Avoid using partial mock after we have ParseObjectInstanceController
        ParseUser partialMockUser = spy(user);
        doReturn(Task.<Void>forResult(null)).when(partialMockUser).saveAsync(anyString(), any());

        ParseTaskUtils.wait(partialMockUser.signUpAsync(Task.forResult(null)));

        // Verify user is saved
        verify(partialMockUser, times(1)).saveAsync(eq("sessionToken"), any());
    }

    @Test
    public void testSignUpAsyncWithAnotherSignUpAlreadyRunning() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();
        user.setUsername("userName");
        user.setPassword("password");
        user.startSave();

        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Cannot sign up a user that is already signing up.");

        ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null)));
    }

    @Test
    public void testSignUpAsyncWithSignUpSameAnonymousUser() throws Exception {
        ParseUser user = new ParseUser();
        user.setUsername("userName");
        user.setPassword("password");
        Map<String, String> anonymousAuthData = new HashMap<>();
        anonymousAuthData.put("key", "token");
        user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData);

        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(user));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Attempt to merge currentUser with itself.");

        ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null)));
    }

    @Test
    public void testSignUpAsyncWithMergeInDiskAnonymousUser() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = mock(ParseUser.class);
        when(currentUser.getUsername()).thenReturn("oldUserName");
        when(currentUser.getPassword()).thenReturn("oldPassword");
        when(currentUser.isLazy()).thenReturn(false);
        when(currentUser.isLinked(ParseAnonymousUtils.AUTH_TYPE)).thenReturn(true);
        when(currentUser.getSessionToken()).thenReturn("oldSessionToken");
        when(currentUser.getAuthData()).thenReturn(new HashMap<>());
        when(currentUser.saveAsync(anyString(), eq(false), any())).thenReturn(Task.forResult(null));
        ParseUser.State state = new ParseUser.State.Builder().put("oldKey", "oldValue").build();
        when(currentUser.getState()).thenReturn(state);
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();
        user.setUsername("userName");
        user.setPassword("password");
        Map<String, String> anonymousAuthData = new HashMap<>();
        anonymousAuthData.put("key", "token");
        user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData);

        Task<Void> signUpTask = user.signUpAsync(Task.forResult(null));
        signUpTask.waitForCompletion();

        // Make sure currentUser copy changes from user
        verify(currentUser, times(1)).copyChangesFrom(user);
        // Make sure we update currentUser username and password
        verify(currentUser, times(1)).setUsername("userName");
        verify(currentUser, times(1)).setPassword("password");
        // Make sure we save currentUser
        verify(currentUser, times(1)).saveAsync(eq("oldSessionToken"), eq(false), any());
        // Make sure we merge currentUser with user after save
        assertEquals("oldValue", user.get("oldKey"));
        // Make sure set currentUser
        verify(currentUserController, times(1)).setAsync(eq(user));
    }

    @Test
    public void testSignUpAsyncWithMergeInDiskAnonymousUserSaveFailure() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        Map<String, String> oldAnonymousAuthData = new HashMap<>();
        oldAnonymousAuthData.put("oldKey", "oldToken");
        currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, oldAnonymousAuthData);
        ParseUser partialMockCurrentUser = spy(currentUser); // Spy since we need mutex
        when(partialMockCurrentUser.getUsername()).thenReturn("oldUserName");
        when(partialMockCurrentUser.getPassword()).thenReturn("oldPassword");
        when(partialMockCurrentUser.getSessionToken()).thenReturn("oldSessionToken");
        when(partialMockCurrentUser.isLazy()).thenReturn(false);
        ParseException saveException = new ParseException(ParseException.OTHER_CAUSE, "");
        doReturn(Task.<Void>forError(saveException))
                .when(partialMockCurrentUser)
                .saveAsync(anyString(), eq(false), any());
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean()))
                .thenReturn(Task.forResult(partialMockCurrentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();
        user.setUsername("userName");
        user.setPassword("password");
        Map<String, String> anonymousAuthData = new HashMap<>();
        anonymousAuthData.put("key", "token");
        user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData);

        Task<Void> signUpTask = user.signUpAsync(Task.forResult(null));
        signUpTask.waitForCompletion();

        // Make sure we update currentUser username and password
        verify(partialMockCurrentUser, times(1)).setUsername("userName");
        verify(partialMockCurrentUser, times(1)).setPassword("password");
        // Make sure we sync user with currentUser
        verify(partialMockCurrentUser, times(1)).copyChangesFrom(eq(user));
        // Make sure we save currentUser
        verify(partialMockCurrentUser, times(1)).saveAsync(eq("oldSessionToken"), eq(false), any());
        // Make sure we restore old username and password after save fails
        verify(partialMockCurrentUser, times(1)).setUsername("oldUserName");
        verify(partialMockCurrentUser, times(1)).setPassword("oldPassword");
        // Make sure we restore anonymity
        verify(partialMockCurrentUser, times(1))
                .putAuthData(ParseAnonymousUtils.AUTH_TYPE, oldAnonymousAuthData);
        // Make sure task is failed
        assertTrue(signUpTask.isFaulted());
        assertSame(saveException, signUpTask.getError());
    }

    @Test
    public void testSignUpAsyncWithNoCurrentUserAndSignUpSuccess() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register a mock userController to make logIn work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .put("newKey", "newValue")
                        .sessionToken("newSessionToken")
                        .build();
        when(userController.signUpAsync(
                        any(ParseUser.State.class),
                        any(ParseOperationSet.class),
                        nullable(String.class)))
                .thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);

        ParseUser user = new ParseUser();
        user.setUsername("userName");
        user.setPassword("password");

        ParseTaskUtils.wait(user.signUpAsync(Task.forResult(null)));

        // Make sure we sign up the user
        verify(userController, times(1))
                .signUpAsync(
                        any(ParseUser.State.class),
                        any(ParseOperationSet.class),
                        nullable(String.class));
        // Make sure user's data is correct
        assertEquals("newSessionToken", user.getSessionToken());
        assertEquals("newValue", user.getString("newKey"));
        assertFalse(user.isLazy());
        // Make sure we set the current user
        verify(currentUserController, times(1)).setAsync(user);
    }

    // endregion

    // region testLogInWithAsync

    @Test
    public void testSignUpAsyncWithNoCurrentUserAndSignUpFailure() {
        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register a mock userController to make logIn work
        ParseUserController userController = mock(ParseUserController.class);
        ParseException signUpException = new ParseException(ParseException.OTHER_CAUSE, "test");
        when(userController.signUpAsync(
                        any(ParseUser.State.class),
                        any(ParseOperationSet.class),
                        nullable(String.class)))
                .thenReturn(Task.forError(signUpException));
        ParseCorePlugins.getInstance().registerUserController(userController);

        ParseUser user = new ParseUser();
        user.put("key", "value");
        user.setUsername("userName");
        user.setPassword("password");

        Task<Void> signUpTask = user.signUpAsync(Task.forResult(null));

        // Make sure we sign up the user
        verify(userController, times(1))
                .signUpAsync(
                        any(ParseUser.State.class),
                        any(ParseOperationSet.class),
                        nullable(String.class));
        // Make sure user's data is correct
        assertEquals("value", user.getString("key"));
        // Make sure we never set the current user
        verify(currentUserController, never()).setAsync(user);
        // Make sure task is failed
        assertTrue(signUpTask.isFaulted());
        assertSame(signUpException, signUpTask.getError());
    }

    @Test
    public void testLoginWithAsyncWithoutExistingLazyUser() throws ParseException {
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(false)).thenReturn(Task.forResult(null));
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));

        ParseUser.State userState = mock(ParseUser.State.class);
        when(userState.className()).thenReturn("_User");
        when(userState.objectId()).thenReturn("1234");
        when(userState.isComplete()).thenReturn(true);

        ParseUserController userController = mock(ParseUserController.class);
        when(userController.logInAsync(anyString(), anyMap()))
                .thenReturn(Task.forResult(userState));

        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        ParseCorePlugins.getInstance().registerUserController(userController);

        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("token", "123");
        ParseUser user = ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, authData));

        verify(currentUserController).getAsync(false);
        verify(userController).logInAsync(authType, authData);
        verify(currentUserController).setAsync(user);
        assertSame(userState, user.getState());

        verifyNoMoreInteractions(currentUserController);
        verifyNoMoreInteractions(userController);
    }

    @Test
    public void testLoginWithAsyncWithLinkedLazyUser() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>());
        setLazy(currentUser);
        ParseUser partialMockCurrentUser = spy(currentUser);
        when(partialMockCurrentUser.getSessionToken()).thenReturn("oldSessionToken");
        doReturn(Task.<ParseUser>forResult(null))
                .when(partialMockCurrentUser)
                .resolveLazinessAsync(any());
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(false))
                .thenReturn(Task.forResult(partialMockCurrentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("token", "123");
        ParseUser userAfterLogin =
                ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, authData));

        // Make sure we stripAnonymity
        assertNull(userAfterLogin.getAuthData().get(ParseAnonymousUtils.AUTH_TYPE));
        // Make sure we update authData
        assertEquals(authData, userAfterLogin.getAuthData().get("facebook"));
        // Make sure we resolveLaziness
        verify(partialMockCurrentUser, times(1)).resolveLazinessAsync(any());
    }

    @Test
    public void testLoginWithAsyncWithLinkedLazyUseAndResolveLazinessFailure() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        Map<String, String> oldAnonymousAuthData = new HashMap<>();
        oldAnonymousAuthData.put("oldKey", "oldToken");
        currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, oldAnonymousAuthData);
        ParseUser partialMockCurrentUser = spy(currentUser);
        when(partialMockCurrentUser.getSessionToken()).thenReturn("oldSessionToken");
        doReturn(Task.<ParseUser>forError(new Exception()))
                .when(partialMockCurrentUser)
                .resolveLazinessAsync(any());
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(false))
                .thenReturn(Task.forResult(partialMockCurrentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("token", "123");

        Task<ParseUser> loginTask = ParseUser.logInWithInBackground(authType, authData);
        loginTask.waitForCompletion();

        // Make sure we try to resolveLaziness
        verify(partialMockCurrentUser, times(1)).resolveLazinessAsync(any());
        // Make sure we do not save new authData
        assertNull(partialMockCurrentUser.getAuthData().get("facebook"));
        // Make sure we restore anonymity after resolve laziness failure
        assertEquals(
                oldAnonymousAuthData,
                partialMockCurrentUser.getAuthData().get(ParseAnonymousUtils.AUTH_TYPE));
        // Make sure task fails
        assertTrue(loginTask.isFaulted());
    }

    @Test
    public void testLoginWithAsyncWithLinkedNotLazyUser() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser.State state =
                new ParseUser.State.Builder()
                        .objectId("objectId") // Make it not lazy
                        .putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>())
                        .build();
        ParseUser currentUser = ParseUser.from(state);
        ParseUser partialMockCurrentUser = spy(currentUser); // ParseUser.mutex
        doReturn(Task.<Void>forResult(null))
                .when(partialMockCurrentUser)
                .linkWithInBackground(anyString(), anyMap());
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync()).thenReturn(Task.forResult(partialMockCurrentUser));
        when(currentUserController.getAsync(anyBoolean()))
                .thenReturn(Task.forResult(partialMockCurrentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("token", "123");

        ParseUser userAfterLogin =
                ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, authData));

        // Make sure we link authData
        verify(partialMockCurrentUser, times(1)).linkWithInBackground(authType, authData);
        assertSame(partialMockCurrentUser, userAfterLogin);
    }

    @Test
    public void testLoginWithAsyncWithLinkedNotLazyUserLinkFailure() throws Exception {
        // Register a mock userController to make logIn work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .put("newKey", "newValue")
                        .sessionToken("newSessionToken")
                        .build();
        when(userController.logInAsync(anyString(), anyMap()))
                .thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        currentUser.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>());
        currentUser.setObjectId("objectId"); // Make it not lazy.
        ParseUser partialMockCurrentUser = spy(currentUser);
        when(partialMockCurrentUser.getSessionToken()).thenReturn("sessionToken");
        ParseException linkException =
                new ParseException(ParseException.ACCOUNT_ALREADY_LINKED, "Account already linked");
        doReturn(Task.<Void>forError(linkException))
                .when(partialMockCurrentUser)
                .linkWithInBackground(anyString(), anyMap());
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(false))
                .thenReturn(Task.forResult(partialMockCurrentUser));
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("token", "123");
        ParseUser userAfterLogin =
                ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, authData));

        // Make sure we link authData
        verify(partialMockCurrentUser, times(1)).linkWithInBackground(authType, authData);
        // Make sure we login authData
        verify(userController, times(1)).logInAsync("facebook", authData);
        // Make sure we save the new created user as currentUser
        verify(currentUserController, times(1)).setAsync(any(ParseUser.class));
        // Make sure the new created user has correct data
        assertEquals("newValue", userAfterLogin.get("newKey"));
        assertEquals("newSessionToken", userAfterLogin.getSessionToken());
    }

    // endregion

    // region testlinkWithInBackground

    @Test
    public void testLoginWithAsyncWithNoCurrentUser() throws Exception {
        // Register a mock userController to make logIn work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .put("newKey", "newValue")
                        .sessionToken("newSessionToken")
                        .build();
        when(userController.logInAsync(anyString(), anyMap()))
                .thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);
        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(false)).thenReturn(Task.forResult(null));
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("token", "123");

        ParseUser userAfterLogin =
                ParseTaskUtils.wait(ParseUser.logInWithInBackground(authType, authData));

        // Make sure we login authData
        verify(userController, times(1)).logInAsync("facebook", authData);
        // Make sure we save the new created user as currentUser
        verify(currentUserController, times(1)).setAsync(any(ParseUser.class));
        // Make sure the new created user has correct data
        assertEquals("newValue", userAfterLogin.get("newKey"));
        assertEquals("newSessionToken", userAfterLogin.getSessionToken());
    }

    @Test
    public void testlinkWithInBackgroundWithSaveAsyncSuccess() throws Exception {
        // Register a mock currentUserController to make setCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getCurrentSessionTokenAsync()).thenReturn(Task.forResult(null));
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register mock callbacks
        AuthenticationCallback callbacks = mock(AuthenticationCallback.class);
        when(callbacks.onRestore(anyMap())).thenReturn(true);
        ParseUser.registerAuthenticationCallback("facebook", callbacks);

        ParseUser user = new ParseUser();
        // To make synchronizeAuthData work
        user.setIsCurrentUser(true);
        // To verify stripAnonymity
        user.setObjectId("objectId");
        user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, new HashMap<>());
        ParseUser partialMockUser = spy(user);
        doReturn(Task.<Void>forResult(null))
                .when(partialMockUser)
                .saveAsync(anyString(), eq(false), any());
        doReturn("sessionTokenAgain").when(partialMockUser).getSessionToken();
        Map<String, String> authData = new HashMap<>();
        authData.put("token", "test");

        ParseTaskUtils.wait(partialMockUser.linkWithInBackground("facebook", authData));

        // Make sure we stripAnonymity
        assertNull(partialMockUser.getAuthData().get(ParseAnonymousUtils.AUTH_TYPE));
        // Make sure new authData is added
        assertSame(authData, partialMockUser.getAuthData().get("facebook"));
        // Make sure we save the user
        verify(partialMockUser, times(1)).saveAsync(eq("sessionTokenAgain"), eq(false), any());
        // Make sure synchronizeAuthData() is called
        verify(callbacks, times(1)).onRestore(authData);
    }

    // endregion

    // region testResolveLazinessAsync

    @Test
    public void testlinkWithInBackgroundWithSaveAsyncFailure() throws Exception {
        // Register a mock currentUserController to make setCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getCurrentSessionTokenAsync())
                .thenReturn(Task.forResult("sessionToken"));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();
        Map<String, String> anonymousAuthData = new HashMap<>();
        anonymousAuthData.put("anonymousToken", "anonymousTest");
        // To verify stripAnonymity
        user.setObjectId("objectId");
        user.putAuthData(ParseAnonymousUtils.AUTH_TYPE, anonymousAuthData);
        ParseUser partialMockUser = spy(user);
        Exception saveException = new Exception();
        doReturn(Task.<Void>forError(saveException))
                .when(partialMockUser)
                .saveAsync(anyString(), eq(false), any());
        doReturn("sessionTokenAgain").when(partialMockUser).getSessionToken();
        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("facebookToken", "facebookTest");

        Task<Void> linkTask = partialMockUser.linkWithInBackground(authType, authData);
        linkTask.waitForCompletion();

        // Make sure we save the user
        verify(partialMockUser, times(1)).saveAsync(eq("sessionTokenAgain"), eq(false), any());
        // Make sure old authData is restored
        assertSame(
                anonymousAuthData,
                partialMockUser.getAuthData().get(ParseAnonymousUtils.AUTH_TYPE));
        // Make sure failed new authData is cleared
        assertNull(partialMockUser.getAuthData().get("facebook"));
        // Verify exception
        assertSame(saveException, linkTask.getError());
    }

    @Test
    public void testResolveLazinessAsyncWithAuthDataAndNotNewUser() throws Exception {
        ParseUser user = new ParseUser();
        setLazy(user);
        user.putAuthData("facebook", new HashMap<>());
        // Register a mock userController to make logIn work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .put("newKey", "newValue")
                        .sessionToken("newSessionToken")
                        .isNew(false)
                        .build();
        when(userController.logInAsync(any(ParseUser.State.class), any(ParseOperationSet.class)))
                .thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);
        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseTaskUtils.wait(user.resolveLazinessAsync(Task.forResult(null)));
        ArgumentCaptor<ParseUser> userAfterResolveLazinessCaptor =
                ArgumentCaptor.forClass(ParseUser.class);

        // Make sure we logIn the lazy user
        verify(userController, times(1))
                .logInAsync(any(ParseUser.State.class), any(ParseOperationSet.class));
        // Make sure we save currentUser
        verify(currentUserController, times(1)).setAsync(userAfterResolveLazinessCaptor.capture());
        ParseUser userAfterResolveLaziness = userAfterResolveLazinessCaptor.getValue();
        // Make sure user's data is correct
        assertEquals("newSessionToken", userAfterResolveLaziness.getSessionToken());
        assertEquals("newValue", userAfterResolveLaziness.get("newKey"));
        // Make sure userAfterResolveLaziness is not lazy
        assertFalse(userAfterResolveLaziness.isLazy());
        // Make sure we create new user
        assertNotSame(user, userAfterResolveLaziness);
    }

    @Test
    public void testResolveLazinessAsyncWithAuthDataAndNewUser() throws Exception {
        ParseUser user = new ParseUser();
        setLazy(user);
        user.putAuthData("facebook", new HashMap<>());
        // Register a mock userController to make logIn work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .objectId("objectId")
                        .put("newKey", "newValue")
                        .sessionToken("newSessionToken")
                        .isNew(true)
                        .build();
        when(userController.logInAsync(any(ParseUser.State.class), any(ParseOperationSet.class)))
                .thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);
        // Register a mock currentUserController to verify setAsync
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseTaskUtils.wait(user.resolveLazinessAsync(Task.forResult(null)));

        // Make sure we logIn the lazy user
        verify(userController, times(1))
                .logInAsync(any(ParseUser.State.class), any(ParseOperationSet.class));
        // Make sure we do not save currentUser
        verify(currentUserController, never()).setAsync(any(ParseUser.class));
        // Make sure userAfterResolveLaziness's data is correct
        assertEquals("newSessionToken", user.getSessionToken());
        assertEquals("newValue", user.get("newKey"));
        // Make sure userAfterResolveLaziness is not lazy
        assertFalse(user.isLazy());
    }

    // endregion

    // region testValidateSave

    @Test
    public void testResolveLazinessAsyncWithAuthDataAndNotNewUserAndLDSEnabled() throws Exception {
        ParseUser user = new ParseUser();
        setLazy(user);
        user.putAuthData("facebook", new HashMap<>());
        // To verify handleSaveResultAsync is not called
        user.setPassword("password");
        // Register a mock userController to make logIn work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .put("newKey", "newValue")
                        .sessionToken("newSessionToken")
                        .isNew(false)
                        .build();
        when(userController.logInAsync(any(ParseUser.State.class), any(ParseOperationSet.class)))
                .thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);
        // Register a mock currentUserController to make getCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Enable LDS
        Parse.enableLocalDatastore(null);

        ParseTaskUtils.wait(user.resolveLazinessAsync(Task.forResult(null)));
        ArgumentCaptor<ParseUser> userAfterResolveLazinessCaptor =
                ArgumentCaptor.forClass(ParseUser.class);

        // Make sure we logIn the lazy user
        verify(userController, times(1))
                .logInAsync(any(ParseUser.State.class), any(ParseOperationSet.class));
        // Make sure handleSaveResultAsync() is not called, if handleSaveResultAsync is called,
        // password
        // field should be cleaned
        assertEquals("password", user.getPassword());
        // Make sure we do not save currentUser
        verify(currentUserController, times(1)).setAsync(userAfterResolveLazinessCaptor.capture());
        ParseUser userAfterResolveLaziness = userAfterResolveLazinessCaptor.getValue();
        // Make sure userAfterResolveLaziness's data is correct
        assertEquals("newSessionToken", userAfterResolveLaziness.getSessionToken());
        assertEquals("newValue", userAfterResolveLaziness.get("newKey"));
        // Make sure userAfterResolveLaziness is not lazy
        assertFalse(userAfterResolveLaziness.isLazy());
        // Make sure we create new user
        assertNotSame(user, userAfterResolveLaziness);
    }

    // TODO(mengyan): Add testValidateSaveWithIsAuthenticatedWithNotDirty

    // TODO(mengyan): Add testValidateSaveWithIsAuthenticatedWithIsCurrentUser

    @Test
    public void testValidateSaveWithNoObjectId() {
        ParseUser user = new ParseUser();

        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage(
                "Cannot save a ParseUser until it has been signed up. Call signUp first.");

        user.validateSave();
    }

    @Test
    public void testValidateSaveWithLDSNotEnabled() {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        currentUser.setObjectId("test");
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();
        user.setObjectId("test");
        // Make isDirty return true
        user.put("key", "value");
        // Make isCurrent return false
        user.setIsCurrentUser(false);

        user.validateSave();
    }

    // endregion

    // region testSaveAsync

    @Test
    public void testValidateSaveWithLDSNotEnabledAndCurrentUserNotMatch() {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        currentUser.setObjectId("testAgain");
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();
        user.setObjectId("test");
        // Make isDirty return true
        user.put("key", "value");
        // Make isCurrent return false
        user.setIsCurrentUser(false);

        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Cannot save a ParseUser that is not authenticated.");

        user.validateSave();
    }

    @Test
    public void testSaveAsyncWithLazyAndCurrentUser() throws Exception {
        // Register a mock currentUserController to make setCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        // Set facebook authData to null to verify cleanAuthData()
        ParseUser.State userState =
                new ParseUser.State.Builder().putAuthData("facebook", null).build();
        ParseUser user = ParseObject.from(userState);
        setLazy(user);
        user.setIsCurrentUser(true);
        ParseUser partialMockUser = spy(user);
        doReturn(Task.<Void>forResult(null)).when(partialMockUser).resolveLazinessAsync(any());

        ParseTaskUtils.wait(partialMockUser.saveAsync("sessionToken", Task.forResult(null)));

        // Make sure we clean authData
        assertFalse(partialMockUser.getState().authData().containsKey("facebook"));
        // Make sure we save new currentUser
        verify(currentUserController, times(1)).setAsync(partialMockUser);
    }

    // TODO(mengyan): Add testSaveAsyncWithNotLazyAndNotCurrentUser, right now we can not mock
    // super.save()

    // endregion

    // region testLogOutAsync

    @Test
    public void testSaveAsyncWithLazyAndNotCurrentUser() throws Exception {
        // Register a mock currentUserController to make setCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        // Set facebook authData to null to verify cleanAuthData()
        ParseUser.State userState =
                new ParseUser.State.Builder().putAuthData("facebook", null).build();
        ParseUser user = ParseObject.from(userState);
        setLazy(user);
        user.setIsCurrentUser(false);
        ParseUser partialMockUser = spy(user);
        doReturn(Task.<Void>forResult(null)).when(partialMockUser).resolveLazinessAsync(any());

        ParseTaskUtils.wait(partialMockUser.saveAsync("sessionToken", Task.forResult(null)));

        // Make sure we do not clean authData
        assertTrue(partialMockUser.getAuthData().containsKey("facebook"));
        // Make sure we do not save new currentUser
        verify(currentUserController, never()).setAsync(partialMockUser);
    }

    // endregion

    // region testEnable/UpgradeSessionToken

    @Test
    public void testLogOutAsync() throws Exception {
        // Register a mock sessionController to verify revokeAsync()
        NetworkSessionController sessionController = mock(NetworkSessionController.class);
        when(sessionController.revokeAsync(anyString())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerSessionController(sessionController);
        ParseAuthenticationManager manager = mock(ParseAuthenticationManager.class);
        when(manager.deauthenticateAsync(anyString())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerAuthenticationManager(manager);

        // Set user initial state
        String facebookAuthType = "facebook";
        Map<String, String> facebookAuthData = new HashMap<>();
        facebookAuthData.put("facebookToken", "facebookTest");
        ParseUser.State userState =
                new ParseUser.State.Builder()
                        .objectId("test")
                        .putAuthData(facebookAuthType, facebookAuthData)
                        .sessionToken("r:oldSessionToken")
                        .build();
        ParseUser user = ParseObject.from(userState);

        ParseTaskUtils.wait(user.logOutAsync());

        verify(manager).deauthenticateAsync("facebook");
        // Verify we revoke session
        verify(sessionController, times(1)).revokeAsync("r:oldSessionToken");
    }

    @Test
    public void testEnableRevocableSessionInBackgroundWithCurrentUser() throws Exception {
        // Register a mock ParsePlugins to make restClient() work
        ParsePlugins mockPlugins = mock(ParsePlugins.class);
        when(mockPlugins.restClient()).thenReturn(null);
        ParsePlugins.set(mockPlugins);
        // Register a mock currentUserController to verify setAsync
        ParseUser mockUser = mock(ParseUser.class);
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(mockUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseTaskUtils.wait(ParseUser.enableRevocableSessionInBackground());

        verify(currentUserController, times(1)).getAsync(false);
        verify(mockUser, times(1)).upgradeToRevocableSessionAsync();
    }

    @Test
    public void testEnableRevocableSessionInBackgroundWithNoCurrentUser() throws Exception {
        // Register a mock ParsePlugins to make restClient() work
        ParsePlugins mockPlugins = mock(ParsePlugins.class);
        when(mockPlugins.restClient()).thenReturn(null);
        ParsePlugins.set(mockPlugins);
        // Register a mock currentUserController to verify setAsync
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseTaskUtils.wait(ParseUser.enableRevocableSessionInBackground());

        verify(currentUserController, times(1)).getAsync(false);
    }

    @Test
    public void testUpgradeToRevocableSessionAsync() throws Exception {
        // Register a mock currentUserController to verify setAsync
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register a mock sessionController to verify revokeAsync()
        NetworkSessionController sessionController = mock(NetworkSessionController.class);
        ParseSession.State state =
                new ParseSession.State.Builder("_Session")
                        .put("sessionToken", "r:newSessionToken")
                        .build();
        when(sessionController.upgradeToRevocable(anyString())).thenReturn(Task.forResult(state));
        ParseCorePlugins.getInstance().registerSessionController(sessionController);

        // Set user initial state
        ParseUser.State userState =
                new ParseUser.State.Builder()
                        .objectId("test")
                        .sessionToken("oldSessionToken")
                        .build();
        ParseUser user = ParseObject.from(userState);

        ParseTaskUtils.wait(user.upgradeToRevocableSessionAsync());

        // Make sure we update to new sessionToken
        assertEquals("r:newSessionToken", user.getSessionToken());
        // Make sure we update currentUser
        verify(currentUserController, times(1)).setAsync(user);
    }

    // endregion

    // region testUnlinkFromAsync

    @Test
    public void testDontOverwriteSessionTokenForCurrentUser() {
        ParseUser.State sessionTokenState =
                new ParseUser.State.Builder()
                        .sessionToken("sessionToken")
                        .put("key0", "value0")
                        .put("key1", "value1")
                        .isComplete(true)
                        .build();
        ParseUser.State newState =
                new ParseUser.State.Builder()
                        .put("key0", "newValue0")
                        .put("key2", "value2")
                        .isComplete(true)
                        .build();
        ParseUser.State emptyState = new ParseUser.State.Builder().isComplete(true).build();

        ParseUser user = ParseObject.from(sessionTokenState);
        user.setIsCurrentUser(true);
        assertEquals(user.getSessionToken(), "sessionToken");
        assertEquals(user.getString("key0"), "value0");
        assertEquals(user.getString("key1"), "value1");

        user.setState(newState);
        assertEquals(user.getSessionToken(), "sessionToken");
        assertEquals(user.getString("key0"), "newValue0");
        assertNull(user.getString("key1"));
        assertEquals(user.getString("key2"), "value2");

        user.setIsCurrentUser(false);
        user.setState(emptyState);
        assertNull(user.getSessionToken());
        assertNull(user.getString("key0"));
        assertNull(user.getString("key1"));
        assertNull(user.getString("key2"));
    }

    // endregion

    // region testLogin

    @Test
    public void testUnlinkFromAsyncWithAuthType() throws Exception {
        // Register a mock currentUserController to make getAsync work
        ParseUser mockUser = mock(ParseUser.class);
        when(mockUser.getSessionToken()).thenReturn("sessionToken");
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync()).thenReturn(Task.forResult(mockUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        // Set user initial state
        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("facebookToken", "facebookTest");
        ParseUser.State userState =
                new ParseUser.State.Builder()
                        .objectId("test")
                        .putAuthData(authType, authData)
                        .build();
        ParseUser user = ParseObject.from(userState);
        ParseUser partialMockUser = spy(user);
        doReturn(Task.<Void>forResult(null)).when(partialMockUser).saveAsync(anyString(), any());

        ParseTaskUtils.wait(partialMockUser.unlinkFromInBackground(authType));

        // Verify we delete authData
        assertNull(user.getAuthData().get("facebook"));
        // Verify we save the user
        verify(partialMockUser, times(1)).saveAsync(eq("sessionToken"), any());
    }

    @Test
    public void testLogInInWithNoUserName() throws Exception {
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Must specify a username for the user to log in with");

        ParseTaskUtils.wait(ParseUser.logInInBackground(null, "password"));
    }

    @Test
    public void testLogInWithNoPassword() throws Exception {
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Must specify a password for the user to log in with");

        ParseTaskUtils.wait(ParseUser.logInInBackground("userName", null));
    }

    @Test
    public void testLogIn() throws Exception {
        // Register a mock currentUserController to make setCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register a mock userController to make logIn work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .put("newKey", "newValue")
                        .sessionToken("newSessionToken")
                        .build();
        when(userController.logInAsync(anyString(), anyString()))
                .thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);

        ParseUser user = ParseUser.logIn("userName", "password");

        // Make sure user is login
        verify(userController, times(1)).logInAsync("userName", "password");
        // Make sure we set currentUser
        verify(currentUserController, times(1)).setAsync(user);
        // Make sure user's data is correct
        assertEquals("newSessionToken", user.getSessionToken());
        assertEquals("newValue", user.get("newKey"));
    }

    // endregion

    // region testBecome

    @Test
    public void testLogInWithCallback() throws Exception {
        // Register a mock currentUserController to make setCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register a mock userController to make logIn work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .put("newKey", "newValue")
                        .sessionToken("newSessionToken")
                        .build();
        when(userController.logInAsync(anyString(), anyString()))
                .thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);

        final Semaphore done = new Semaphore(0);
        ParseUser.logInInBackground(
                "userName",
                "password",
                (user, e) -> {
                    done.release();
                    assertNull(e);
                    // Make sure user's data is correct
                    assertEquals("newSessionToken", user.getSessionToken());
                    assertEquals("newValue", user.get("newKey"));
                });

        shadowMainLooper().idle();

        assertTrue(done.tryAcquire(5, TimeUnit.SECONDS));
        // Make sure user is login
        verify(userController, times(1)).logInAsync("userName", "password");
        // Make sure we set currentUser
        verify(currentUserController, times(1)).setAsync(any(ParseUser.class));
    }

    @Test
    public void testBecomeWithNoSessionToken() throws Exception {
        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Must specify a sessionToken for the user to log in with");

        ParseUser.become(null);
    }

    @Test
    public void testBecome() throws Exception {
        // Register a mock currentUserController to make setCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register a mock userController to make getUsreAsync work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .put("key", "value")
                        .sessionToken("sessionToken")
                        .build();
        when(userController.getUserAsync(anyString())).thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);

        ParseUser user = ParseUser.become("sessionToken");

        // Make sure we call getUserAsync
        verify(userController, times(1)).getUserAsync("sessionToken");
        // Make sure we set currentUser
        verify(currentUserController, times(1)).setAsync(user);
        // Make sure user's data is correct
        assertEquals("sessionToken", user.getSessionToken());
        assertEquals("value", user.get("key"));
    }

    // endregion

    // region testToRest

    @Test
    public void testBecomeWithCallback() {
        // Register a mock currentUserController to make setCurrentUser work
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.setAsync(any(ParseUser.class))).thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register a mock userController to make getUsreAsync work
        ParseUserController userController = mock(ParseUserController.class);
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .put("key", "value")
                        .sessionToken("sessionToken")
                        .build();
        when(userController.getUserAsync(anyString())).thenReturn(Task.forResult(newUserState));
        ParseCorePlugins.getInstance().registerUserController(userController);

        final Semaphore done = new Semaphore(0);
        ParseUser.becomeInBackground(
                "sessionToken",
                (user, e) -> {
                    done.release();
                    assertNull(e);
                    // Make sure user's data is correct
                    assertEquals("sessionToken", user.getSessionToken());
                    assertEquals("value", user.get("key"));
                });

        // Make sure we call getUserAsync
        verify(userController, times(1)).getUserAsync("sessionToken");
        // Make sure we set currentUser
        verify(currentUserController, times(1)).setAsync(any(ParseUser.class));
    }

    // endregion

    // region testValidateDelete

    @Test
    public void testToRest() throws Exception {
        ParseUser user = new ParseUser();
        user.setUsername("userName");
        user.setPassword("password");

        JSONObject json =
                user.toRest(user.getState(), user.operationSetQueue, PointerEncoder.get());

        // Make sure we delete password operations
        assertFalse(json.getJSONArray("__operations").getJSONObject(0).has("password"));
        // Make sure we have username operations
        assertEquals(
                "userName",
                json.getJSONArray("__operations").getJSONObject(0).getString("username"));
    }

    // endregion

    // region testValidateDelete

    @Test
    public void testValidDelete() {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        currentUser.setObjectId("test");
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();
        user.setObjectId("test");
        // Make isDirty return true
        user.put("key", "value");

        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Cannot delete a ParseUser that is not authenticated.");

        user.validateDelete();
    }

    // endregion

    // region testSynchronizeAuthData

    @Test
    public void testValidateSaveEventually() throws Exception {
        ParseUser user = new ParseUser();
        user.setPassword("password");

        thrown.expect(ParseException.class);
        thrown.expectMessage("Unable to saveEventually on a ParseUser with dirty password");

        user.validateSaveEventually();
    }

    @Test
    public void testSynchronizeAuthData() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register mock callbacks
        AuthenticationCallback callbacks = mock(AuthenticationCallback.class);
        when(callbacks.onRestore(any())).thenReturn(true);
        ParseUser.registerAuthenticationCallback("facebook", callbacks);

        // Set user initial state
        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("facebookToken", "facebookTest");
        ParseUser.State userState =
                new ParseUser.State.Builder().putAuthData(authType, authData).build();
        ParseUser user = ParseObject.from(userState);
        user.setIsCurrentUser(true);

        ParseTaskUtils.wait(user.synchronizeAuthDataAsync(authType));

        // Make sure we restore authentication
        verify(callbacks, times(1)).onRestore(authData);
    }

    // endregion

    // region testAutomaticUser

    @Test
    public void testSynchronizeAllAuthData() throws Exception {
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
        when(currentUserController.getAsync(anyBoolean())).thenReturn(Task.forResult(currentUser));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
        // Register mock callbacks
        AuthenticationCallback callbacks = mock(AuthenticationCallback.class);
        when(callbacks.onRestore(any())).thenReturn(true);
        ParseUser.registerAuthenticationCallback("facebook", callbacks);

        // Set user initial state
        String facebookAuthType = "facebook";
        Map<String, String> facebookAuthData = new HashMap<>();
        facebookAuthData.put("facebookToken", "facebookTest");
        ParseUser.State userState =
                new ParseUser.State.Builder()
                        .putAuthData(facebookAuthType, facebookAuthData)
                        .build();
        ParseUser user = ParseObject.from(userState);
        user.setIsCurrentUser(true);

        ParseTaskUtils.wait(user.synchronizeAllAuthDataAsync());

        // Make sure we restore authentication
        verify(callbacks, times(1)).onRestore(facebookAuthData);
    }

    // endregion

    // region testAutomaticUser

    @Test
    public void testAutomaticUser() {
        new ParseUser();

        ParseUser.disableAutomaticUser();
        assertFalse(ParseUser.isAutomaticUserEnabled());

        ParseUser.enableAutomaticUser();
        assertTrue(ParseUser.isAutomaticUserEnabled());
    }

    // endregion

    // region testPinCurrentUserIfNeededAsync

    @Test
    public void testPinCurrentUserIfNeededAsyncWithNoLDSEnabled() {
        thrown.expect(IllegalStateException.class);
        thrown.expectMessage("Method requires Local Datastore.");

        ParseUser.pinCurrentUserIfNeededAsync(new ParseUser());
    }

    // endregion

    // region testRemove

    @Test
    public void testPinCurrentUserIfNeededAsync() {
        // Enable LDS
        Parse.enableLocalDatastore(null);
        // Register a mock currentUserController to make getCurrentUser work
        ParseUser currentUser = new ParseUser();
        currentUser.setObjectId("test");
        CachedCurrentUserController currentUserController = mock(CachedCurrentUserController.class);
        when(currentUserController.setIfNeededAsync(any(ParseUser.class)))
                .thenReturn(Task.forResult(null));
        ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);

        ParseUser user = new ParseUser();
        ParseUser.pinCurrentUserIfNeededAsync(user);

        // Make sure we pin the user
        verify(currentUserController, times(1)).setIfNeededAsync(user);
    }

    // endregion

    // region testSetState

    @Test
    public void testRemoveWithUserName() {
        ParseUser user = new ParseUser();

        thrown.expect(IllegalArgumentException.class);
        thrown.expectMessage("Can't remove the username key.");

        user.remove("username");
    }

    @Test
    public void testSetCurrentUserStateWithoutAuthData() {
        // Set user initial state
        String authType = "facebook";
        Map<String, String> authData = new HashMap<>();
        authData.put("facebookToken", "facebookTest");
        ParseUser.State userState =
                new ParseUser.State.Builder()
                        .objectId("test")
                        .put("oldKey", "oldValue")
                        .put("key", "value")
                        .putAuthData(authType, authData)
                        .build();
        ParseUser user = ParseObject.from(userState);
        user.setIsCurrentUser(true);
        // Build new state
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .objectId("testAgain")
                        .put("key", "valueAgain")
                        .build();

        user.setState(newUserState);

        // Make sure we keep the authData
        assertEquals(1, user.getAuthData().size());
        assertEquals(authData, user.getAuthData().get(authType));
        // Make sure old state is replaced
        assertFalse(user.has("oldKey"));
        // Make sure new state is set
        assertEquals("testAgain", user.getObjectId());
        assertEquals("valueAgain", user.get("key"));
    }

    // endregion

    @Test
    public void testSetStateDoesNotAddNonExistentAuthData() {
        // Set user initial state
        ParseUser.State userState =
                new ParseUser.State.Builder()
                        .objectId("test")
                        .put("oldKey", "oldValue")
                        .put("key", "value")
                        .build();
        ParseUser user = ParseObject.from(userState);
        user.setIsCurrentUser(true);
        // Build new state
        ParseUser.State newUserState =
                new ParseUser.State.Builder()
                        .objectId("testAgain")
                        .put("key", "valueAgain")
                        .build();

        user.setState(newUserState);

        // Make sure we do not add authData when it did not exist before
        assertFalse(user.keySet().contains("authData"));
        assertEquals(1, user.keySet().size());
        assertEquals(0, user.getAuthData().size());
        // Make sure old state is replaced
        assertFalse(user.has("oldKey"));
        // Make sure new state is set
        assertEquals("testAgain", user.getObjectId());
        assertEquals("valueAgain", user.get("key"));
    }

    // region testSaveEventuallyWhenServerError

    @Test
    public void testSaveEventuallyWhenServerError() throws Exception {
        Shadows.shadowOf(RuntimeEnvironment.application)
                .grantPermissions(Manifest.permission.ACCESS_NETWORK_STATE);
        Parse.Configuration configuration =
                new Parse.Configuration.Builder(RuntimeEnvironment.application)
                        .applicationId(BuildConfig.LIBRARY_PACKAGE_NAME)
                        .server("https://api.parse.com/1")
                        .enableLocalDataStore()
                        .build();
        ParsePlugins plugins = ParseTestUtils.mockParsePlugins(configuration);
        JSONObject mockResponse = new JSONObject();
        mockResponse.put("objectId", "objectId");
        mockResponse.put("email", "email@parse.com");
        mockResponse.put("username", "username");
        mockResponse.put("sessionToken", "r:sessionToken");
        mockResponse.put("createdAt", ParseDateFormat.getInstance().format(new Date(1000)));
        mockResponse.put("updatedAt", ParseDateFormat.getInstance().format(new Date(2000)));
        ParseHttpClient restClient =
                ParseTestUtils.mockParseHttpClientWithResponse(mockResponse, 200, "OK");
        when(plugins.restClient()).thenReturn(restClient);
        Parse.initialize(configuration, plugins);

        ParseUser user = ParseUser.logIn("username", "password");
        assertFalse(user.isDirty());

        user.put("field", "data");
        assertTrue(user.isDirty());

        mockResponse = new JSONObject();
        mockResponse.put("updatedAt", ParseDateFormat.getInstance().format(new Date(3000)));
        ParseTestUtils.updateMockParseHttpClientWithResponse(restClient, mockResponse, 200, "OK");

        final CountDownLatch saveCountDown1 = new CountDownLatch(1);
        final Capture<Exception> exceptionCapture = new Capture<>();
        user.saveInBackground()
                .continueWith(
                        (Continuation<Void, Void>)
                                task -> {
                                    exceptionCapture.set(task.getError());
                                    saveCountDown1.countDown();
                                    return null;
                                });
        assertTrue(saveCountDown1.await(5, TimeUnit.SECONDS));
        assertNull(exceptionCapture.get());
        assertFalse(user.isDirty());

        user.put("field", "other data");
        assertTrue(user.isDirty());

        mockResponse = new JSONObject();
        mockResponse.put("error", "Save is not allowed");
        mockResponse.put("code", 141);
        ParseTestUtils.updateMockParseHttpClientWithResponse(
                restClient, mockResponse, 400, "Bad Request");

        final CountDownLatch saveEventuallyCountDown = new CountDownLatch(1);
        user.saveEventually()
                .continueWith(
                        (Continuation<Void, Void>)
                                task -> {
                                    exceptionCapture.set(task.getError());
                                    saveEventuallyCountDown.countDown();
                                    return null;
                                });
        assertTrue(saveEventuallyCountDown.await(5, TimeUnit.SECONDS));
        assertTrue(exceptionCapture.get() instanceof ParseException);
        assertEquals(
                ParseException.SCRIPT_ERROR, ((ParseException) exceptionCapture.get()).getCode());
        assertEquals("Save is not allowed", exceptionCapture.get().getMessage());
        assertTrue(user.isDirty());

        // Simulate reboot
        Parse.destroy();
        Parse.initialize(configuration, plugins);

        user = ParseUser.getCurrentUser();
        assertTrue(user.isDirty());

        assertEquals("other data", user.get("field"));
        user.put("field", "another data");

        mockResponse = new JSONObject();
        mockResponse.put("updatedAt", ParseDateFormat.getInstance().format(new Date(4000)));
        ParseTestUtils.updateMockParseHttpClientWithResponse(restClient, mockResponse, 200, "OK");

        final CountDownLatch saveCountDown2 = new CountDownLatch(1);
        user.saveInBackground()
                .continueWith(
                        (Continuation<Void, Void>)
                                task -> {
                                    exceptionCapture.set(task.getError());
                                    saveCountDown2.countDown();
                                    return null;
                                });

        assertTrue(saveCountDown2.await(5, TimeUnit.SECONDS));
        assertNull(exceptionCapture.get());
        assertFalse(user.isDirty());
    }

    // endregion
}
