/*
 * Copyright 2017 Google Inc.
 *
 * 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 com.google.firebase.auth;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.api.client.googleapis.util.Utils;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.json.JsonHttpContent;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.BaseEncoding;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.ImplFirebaseTrampolines;
import com.google.firebase.auth.UserRecord.CreateRequest;
import com.google.firebase.auth.UserRecord.UpdateRequest;
import com.google.firebase.auth.hash.Scrypt;
import com.google.firebase.testing.IntegrationTestUtils;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.BeforeClass;
import org.junit.Test;

public class FirebaseAuthIT {

  private static final String VERIFY_CUSTOM_TOKEN_URL =
      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken";
  private static final String VERIFY_PASSWORD_URL =
      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword";
  private static final String RESET_PASSWORD_URL =
      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword";
  private static final String EMAIL_LINK_SIGN_IN_URL =
      "https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSignin";
  private static final JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
  private static final HttpTransport transport = Utils.getDefaultTransport();
  private static final String ACTION_LINK_CONTINUE_URL = "http://localhost/?a=1&b=2#c=3";

  private static FirebaseAuth auth;

  @BeforeClass
  public static void setUpClass() {
    FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp();
    auth = FirebaseAuth.getInstance(masterApp);
  }

  @Test
  public void testGetNonExistingUser() throws Exception {
    try {
      auth.getUserAsync("non.existing").get();
      fail("No error thrown for non existing uid");
    } catch (ExecutionException e) {
      assertTrue(e.getCause() instanceof FirebaseAuthException);
      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
          ((FirebaseAuthException) e.getCause()).getErrorCode());
    }
  }

  @Test
  public void testGetNonExistingUserByEmail() throws Exception {
    try {
      auth.getUserByEmailAsync("[email protected]").get();
      fail("No error thrown for non existing email");
    } catch (ExecutionException e) {
      assertTrue(e.getCause() instanceof FirebaseAuthException);
      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
          ((FirebaseAuthException) e.getCause()).getErrorCode());
    }
  }

  @Test
  public void testUpdateNonExistingUser() throws Exception {
    try {
      auth.updateUserAsync(new UpdateRequest("non.existing")).get();
      fail("No error thrown for non existing uid");
    } catch (ExecutionException e) {
      assertTrue(e.getCause() instanceof FirebaseAuthException);
      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
          ((FirebaseAuthException) e.getCause()).getErrorCode());
    }
  }

  @Test
  public void testDeleteNonExistingUser() throws Exception {
    try {
      auth.deleteUserAsync("non.existing").get();
      fail("No error thrown for non existing uid");
    } catch (ExecutionException e) {
      assertTrue(e.getCause() instanceof FirebaseAuthException);
      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
          ((FirebaseAuthException) e.getCause()).getErrorCode());
    }
  }

  @Test
  public void testDeleteUsers() throws Exception {
    UserRecord user1 = newUserWithParams();
    UserRecord user2 = newUserWithParams();
    UserRecord user3 = newUserWithParams();

    DeleteUsersResult deleteUsersResult =
        slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), user2.getUid(), user3.getUid()))
            .get();

    assertEquals(3, deleteUsersResult.getSuccessCount());
    assertEquals(0, deleteUsersResult.getFailureCount());
    assertTrue(deleteUsersResult.getErrors().isEmpty());

    GetUsersResult getUsersResult =
        auth.getUsersAsync(
                ImmutableList.<UserIdentifier>of(new UidIdentifier(user1.getUid()),
                    new UidIdentifier(user2.getUid()), new UidIdentifier(user3.getUid())))
            .get();

    assertTrue(getUsersResult.getUsers().isEmpty());
    assertEquals(3, getUsersResult.getNotFound().size());
  }

  @Test
  public void testDeleteExistingAndNonExistingUsers() throws Exception {
    UserRecord user1 = newUserWithParams();

    DeleteUsersResult deleteUsersResult =
        slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), "uid-that-doesnt-exist")).get();

    assertEquals(2, deleteUsersResult.getSuccessCount());
    assertEquals(0, deleteUsersResult.getFailureCount());
    assertTrue(deleteUsersResult.getErrors().isEmpty());

    GetUsersResult getUsersResult =
        auth.getUsersAsync(ImmutableList.<UserIdentifier>of(new UidIdentifier(user1.getUid()),
                               new UidIdentifier("uid-that-doesnt-exist")))
            .get();

    assertTrue(getUsersResult.getUsers().isEmpty());
    assertEquals(2, getUsersResult.getNotFound().size());
  }

  @Test
  public void testDeleteUsersIsIdempotent() throws Exception {
    UserRecord user1 = newUserWithParams();

    DeleteUsersResult result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get();

    assertEquals(1, result.getSuccessCount());
    assertEquals(0, result.getFailureCount());
    assertTrue(result.getErrors().isEmpty());

    // Delete the user again to ensure that everything still counts as a success.
    result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get();

    assertEquals(1, result.getSuccessCount());
    assertEquals(0, result.getFailureCount());
    assertTrue(result.getErrors().isEmpty());
  }

  /**
   * The {@code batchDelete} endpoint has a rate limit of 1 QPS. Use this test
   * helper to ensure you don't exceed the quota.
   */
  // TODO(rsgowman): When/if the rate limit is relaxed, eliminate this helper.
  private ApiFuture<DeleteUsersResult> slowDeleteUsersAsync(List<String> uids) throws Exception {
    TimeUnit.SECONDS.sleep(1);
    return auth.deleteUsersAsync(uids);
  }

  @Test
  public void testCreateUserWithParams() throws Exception {
    RandomUser randomUser = RandomUser.create();
    String phone = randomPhoneNumber();
    CreateRequest user = new CreateRequest()
        .setUid(randomUser.uid)
        .setEmail(randomUser.email)
        .setPhoneNumber(phone)
        .setDisplayName("Random User")
        .setPhotoUrl("https://example.com/photo.png")
        .setEmailVerified(true)
        .setPassword("password");

    UserRecord userRecord = auth.createUserAsync(user).get();
    try {
      assertEquals(randomUser.uid, userRecord.getUid());
      assertEquals("Random User", userRecord.getDisplayName());
      assertEquals(randomUser.email, userRecord.getEmail());
      assertEquals(phone, userRecord.getPhoneNumber());
      assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
      assertTrue(userRecord.isEmailVerified());
      assertFalse(userRecord.isDisabled());

      assertEquals(2, userRecord.getProviderData().length);
      List<String> providers = new ArrayList<>();
      for (UserInfo provider : userRecord.getProviderData()) {
        providers.add(provider.getProviderId());
      }
      assertTrue(providers.contains("password"));
      assertTrue(providers.contains("phone"));

      checkRecreate(randomUser.uid);
    } finally {
      auth.deleteUserAsync(userRecord.getUid()).get();
    }
  }

  @Test
  public void testUserLifecycle() throws Exception {
    // Create user
    UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get();
    String uid = userRecord.getUid();

    // Get user
    userRecord = auth.getUserAsync(userRecord.getUid()).get();
    assertEquals(uid, userRecord.getUid());
    assertNull(userRecord.getDisplayName());
    assertNull(userRecord.getEmail());
    assertNull(userRecord.getPhoneNumber());
    assertNull(userRecord.getPhotoUrl());
    assertFalse(userRecord.isEmailVerified());
    assertFalse(userRecord.isDisabled());
    assertTrue(userRecord.getUserMetadata().getCreationTimestamp() > 0);
    assertEquals(0, userRecord.getUserMetadata().getLastSignInTimestamp());
    assertEquals(0, userRecord.getProviderData().length);
    assertTrue(userRecord.getCustomClaims().isEmpty());

    // Update user
    RandomUser randomUser = RandomUser.create();
    String phone = randomPhoneNumber();
    UpdateRequest request = userRecord.updateRequest()
        .setDisplayName("Updated Name")
        .setEmail(randomUser.email)
        .setPhoneNumber(phone)
        .setPhotoUrl("https://example.com/photo.png")
        .setEmailVerified(true)
        .setPassword("secret");
    userRecord = auth.updateUserAsync(request).get();
    assertEquals(uid, userRecord.getUid());
    assertEquals("Updated Name", userRecord.getDisplayName());
    assertEquals(randomUser.email, userRecord.getEmail());
    assertEquals(phone, userRecord.getPhoneNumber());
    assertEquals("https://example.com/photo.png", userRecord.getPhotoUrl());
    assertTrue(userRecord.isEmailVerified());
    assertFalse(userRecord.isDisabled());
    assertEquals(2, userRecord.getProviderData().length);
    assertTrue(userRecord.getCustomClaims().isEmpty());

    // Get user by email
    userRecord = auth.getUserByEmailAsync(userRecord.getEmail()).get();
    assertEquals(uid, userRecord.getUid());

    // Disable user and remove properties
    request = userRecord.updateRequest()
        .setPhotoUrl(null)
        .setDisplayName(null)
        .setPhoneNumber(null)
        .setDisabled(true);
    userRecord = auth.updateUserAsync(request).get();
    assertEquals(uid, userRecord.getUid());
    assertNull(userRecord.getDisplayName());
    assertEquals(randomUser.email, userRecord.getEmail());
    assertNull(userRecord.getPhoneNumber());
    assertNull(userRecord.getPhotoUrl());
    assertTrue(userRecord.isEmailVerified());
    assertTrue(userRecord.isDisabled());
    assertEquals(1, userRecord.getProviderData().length);
    assertTrue(userRecord.getCustomClaims().isEmpty());

    // Delete user
    auth.deleteUserAsync(userRecord.getUid()).get();
    try {
      auth.getUserAsync(userRecord.getUid()).get();
      fail("No error thrown for deleted user");
    } catch (ExecutionException e) {
      assertTrue(e.getCause() instanceof FirebaseAuthException);
      assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR,
          ((FirebaseAuthException) e.getCause()).getErrorCode());
    }
  }

  @Test
  public void testLastRefreshTime() throws Exception {
    RandomUser user = RandomUser.create();
    UserRecord newUserRecord = auth.createUser(new CreateRequest()
                                                   .setUid(user.uid)
                                                   .setEmail(user.email)
                                                   .setEmailVerified(false)
                                                   .setPassword("password"));

    try {
      // New users should not have a lastRefreshTimestamp set.
      assertEquals(0, newUserRecord.getUserMetadata().getLastRefreshTimestamp());

      // Login to cause the lastRefreshTimestamp to be set.
      signInWithPassword(newUserRecord.getEmail(), "password");

      // Attempt to retrieve the user 3 times (with a small delay between each
      // attempt). Occassionally, this call retrieves the user data without the
      // lastLoginTime/lastRefreshTime set; possibly because it's hitting a
      // different server than the login request uses.
      UserRecord userRecord = null;
      for (int i = 0; i < 3; i++) {
        userRecord = auth.getUser(newUserRecord.getUid());

        if (userRecord.getUserMetadata().getLastRefreshTimestamp() != 0) {
          break;
        }

        TimeUnit.SECONDS.sleep((long)Math.pow(2, i));
      }

      // Ensure the lastRefreshTimestamp is approximately "now" (with a tollerance of 10 minutes).
      long now = System.currentTimeMillis();
      long tollerance = TimeUnit.MINUTES.toMillis(10);
      long lastRefreshTimestamp = userRecord.getUserMetadata().getLastRefreshTimestamp();
      assertTrue(now - tollerance <= lastRefreshTimestamp);
      assertTrue(lastRefreshTimestamp <= now + tollerance);
    } finally {
      auth.deleteUser(newUserRecord.getUid());
    }
  }

  @Test
  public void testListUsers() throws Exception {
    final List<String> uids = new ArrayList<>();

    try {
      uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid());
      uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid());
      uids.add(auth.createUserAsync(new CreateRequest().setPassword("password")).get().getUid());

      // Test list by batches
      final AtomicInteger collected = new AtomicInteger(0);
      ListUsersPage page = auth.listUsersAsync(null).get();
      while (page != null) {
        for (ExportedUserRecord user : page.getValues()) {
          if (uids.contains(user.getUid())) {
            collected.incrementAndGet();
            assertNotNull("Missing passwordHash field. A common cause would be "
                + "forgetting to add the \"Firebase Authentication Admin\" permission. See "
                + "instructions in CONTRIBUTING.md", user.getPasswordHash());
            assertNotNull(user.getPasswordSalt());
          }
        }
        page = page.getNextPage();
      }
      assertEquals(uids.size(), collected.get());

      // Test iterate all
      collected.set(0);
      page = auth.listUsersAsync(null).get();
      for (ExportedUserRecord user : page.iterateAll()) {
        if (uids.contains(user.getUid())) {
          collected.incrementAndGet();
          assertNotNull(user.getPasswordHash());
          assertNotNull(user.getPasswordSalt());
        }
      }
      assertEquals(uids.size(), collected.get());

      // Test iterate async
      collected.set(0);
      final Semaphore semaphore = new Semaphore(0);
      final AtomicReference<Throwable> error = new AtomicReference<>();
      ApiFuture<ListUsersPage> pageFuture = auth.listUsersAsync(null);
      ApiFutures.addCallback(pageFuture, new ApiFutureCallback<ListUsersPage>() {
        @Override
        public void onFailure(Throwable t) {
          error.set(t);
          semaphore.release();
        }

        @Override
        public void onSuccess(ListUsersPage result) {
          for (ExportedUserRecord user : result.iterateAll()) {
            if (uids.contains(user.getUid())) {
              collected.incrementAndGet();
              assertNotNull(user.getPasswordHash());
              assertNotNull(user.getPasswordSalt());
            }
          }
          semaphore.release();
        }
      }, MoreExecutors.directExecutor());
      semaphore.acquire();
      assertEquals(uids.size(), collected.get());
      assertNull(error.get());
    } finally {
      for (String uid : uids) {
        auth.deleteUserAsync(uid).get();
      }
    }
  }

  @Test
  public void testCustomClaims() throws Exception {
    UserRecord userRecord = auth.createUserAsync(new CreateRequest()).get();
    String uid = userRecord.getUid();

    try {
      // New user should not have any claims
      assertTrue(userRecord.getCustomClaims().isEmpty());

      Map<String, Object> expected = ImmutableMap.<String, Object>of(
          "admin", true, "package", "gold");
      auth.setCustomUserClaimsAsync(uid, expected).get();

      // Should have 2 claims
      UserRecord updatedUser = auth.getUserAsync(uid).get();
      assertEquals(2, updatedUser.getCustomClaims().size());
      for (Map.Entry<String, Object> entry : expected.entrySet()) {
        assertEquals(entry.getValue(), updatedUser.getCustomClaims().get(entry.getKey()));
      }

      // User's ID token should have the custom claims
      String customToken = auth.createCustomTokenAsync(uid).get();
      String idToken = signInWithCustomToken(customToken);
      FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get();
      Map<String, Object> result = decoded.getClaims();
      for (Map.Entry<String, Object> entry : expected.entrySet()) {
        assertEquals(entry.getValue(), result.get(entry.getKey()));
      }

      // Should be able to remove custom claims
      auth.setCustomUserClaimsAsync(uid, null).get();
      updatedUser = auth.getUserAsync(uid).get();
      assertTrue(updatedUser.getCustomClaims().isEmpty());
    } finally {
      auth.deleteUserAsync(uid).get();
    }
  }

  @Test
  public void testCustomToken() throws Exception {
    String customToken = auth.createCustomTokenAsync("user1").get();
    String idToken = signInWithCustomToken(customToken);
    FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get();
    assertEquals("user1", decoded.getUid());
  }

  @Test
  public void testCustomTokenWithIAM() throws Exception {
    FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp();
    GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(masterApp);
    AccessToken token = credentials.getAccessToken();
    if (token == null) {
      token = credentials.refreshAccessToken();
    }
    FirebaseOptions options = new FirebaseOptions.Builder()
        .setCredentials(GoogleCredentials.create(token))
        .setServiceAccountId(((ServiceAccountSigner) credentials).getAccount())
        .setProjectId(IntegrationTestUtils.getProjectId())
        .build();
    FirebaseApp customApp = FirebaseApp.initializeApp(options, "tempApp");
    try {
      FirebaseAuth auth = FirebaseAuth.getInstance(customApp);
      String customToken = auth.createCustomTokenAsync("user1").get();
      String idToken = signInWithCustomToken(customToken);
      FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get();
      assertEquals("user1", decoded.getUid());
    } finally {
      customApp.delete();
    }
  }

  @Test
  public void testVerifyIdToken() throws Exception {
    String customToken = auth.createCustomTokenAsync("user2").get();
    String idToken = signInWithCustomToken(customToken);
    FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get();
    assertEquals("user2", decoded.getUid());
    decoded = auth.verifyIdTokenAsync(idToken, true).get();
    assertEquals("user2", decoded.getUid());
    Thread.sleep(1000);
    auth.revokeRefreshTokensAsync("user2").get();
    decoded = auth.verifyIdTokenAsync(idToken, false).get();
    assertEquals("user2", decoded.getUid());
    try {
      auth.verifyIdTokenAsync(idToken, true).get();
      fail("expecting exception");
    } catch (ExecutionException e) {
      assertTrue(e.getCause() instanceof FirebaseAuthException);
      assertEquals(RevocationCheckDecorator.ID_TOKEN_REVOKED_ERROR,
                   ((FirebaseAuthException) e.getCause()).getErrorCode());
    }
    idToken = signInWithCustomToken(customToken);
    decoded = auth.verifyIdTokenAsync(idToken, true).get();
    assertEquals("user2", decoded.getUid());    
    auth.deleteUserAsync("user2");
  }

  @Test
  public void testVerifySessionCookie() throws Exception {
    String customToken = auth.createCustomTokenAsync("user3").get();
    String idToken = signInWithCustomToken(customToken);

    SessionCookieOptions options = SessionCookieOptions.builder()
        .setExpiresIn(TimeUnit.HOURS.toMillis(1))
        .build();
    String sessionCookie = auth.createSessionCookieAsync(idToken, options).get();
    assertFalse(Strings.isNullOrEmpty(sessionCookie));

    FirebaseToken decoded = auth.verifySessionCookieAsync(sessionCookie).get();
    assertEquals("user3", decoded.getUid());
    decoded = auth.verifySessionCookieAsync(sessionCookie, true).get();
    assertEquals("user3", decoded.getUid());
    Thread.sleep(1000);

    auth.revokeRefreshTokensAsync("user3").get();
    decoded = auth.verifySessionCookieAsync(sessionCookie, false).get();
    assertEquals("user3", decoded.getUid());
    try {
      auth.verifySessionCookieAsync(sessionCookie, true).get();
      fail("expecting exception");
    } catch (ExecutionException e) {
      assertTrue(e.getCause() instanceof FirebaseAuthException);
      assertEquals(RevocationCheckDecorator.SESSION_COOKIE_REVOKED_ERROR,
          ((FirebaseAuthException) e.getCause()).getErrorCode());
    }

    idToken = signInWithCustomToken(customToken);
    sessionCookie = auth.createSessionCookieAsync(idToken, options).get();
    decoded = auth.verifySessionCookieAsync(sessionCookie, true).get();
    assertEquals("user3", decoded.getUid());
    auth.deleteUserAsync("user3");
  }

  @Test
  public void testCustomTokenWithClaims() throws Exception {
    Map<String, Object> devClaims = ImmutableMap.<String, Object>of(
        "premium", true, "subscription", "silver");
    String customToken = auth.createCustomTokenAsync("user2", devClaims).get();
    String idToken = signInWithCustomToken(customToken);
    FirebaseToken decoded = auth.verifyIdTokenAsync(idToken).get();
    assertEquals("user2", decoded.getUid());
    assertTrue((Boolean) decoded.getClaims().get("premium"));
    assertEquals("silver", decoded.getClaims().get("subscription"));
  }

  @Test
  public void testImportUsers() throws Exception {
    RandomUser randomUser = RandomUser.create();
    ImportUserRecord user = ImportUserRecord.builder()
        .setUid(randomUser.uid)
        .setEmail(randomUser.email)
        .build();

    UserImportResult result = auth.importUsersAsync(ImmutableList.of(user)).get();
    assertEquals(1, result.getSuccessCount());
    assertEquals(0, result.getFailureCount());

    try {
      UserRecord savedUser = auth.getUserAsync(randomUser.uid).get();
      assertEquals(randomUser.email, savedUser.getEmail());
    } finally {
      auth.deleteUserAsync(randomUser.uid).get();
    }
  }

  @Test
  public void testImportUsersWithPassword() throws Exception {
    RandomUser randomUser = RandomUser.create();
    final byte[] passwordHash = BaseEncoding.base64().decode(
        "V358E8LdWJXAO7muq0CufVpEOXaj8aFiC7T/rcaGieN04q/ZPJ08WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw==");
    ImportUserRecord user = ImportUserRecord.builder()
        .setUid(randomUser.uid)
        .setEmail(randomUser.email)
        .setPasswordHash(passwordHash)
        .setPasswordSalt("NaCl".getBytes())
        .build();

    final byte[] scryptKey = BaseEncoding.base64().decode(
        "jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZJzxUECKbm0QsEmYUSDzZvpjeJ9WmXA==");
    final byte[] saltSeparator = BaseEncoding.base64().decode("Bw==");
    UserImportResult result = auth.importUsersAsync(
        ImmutableList.of(user),
        UserImportOptions.withHash(Scrypt.builder()
            .setKey(scryptKey)
            .setSaltSeparator(saltSeparator)
            .setRounds(8)
            .setMemoryCost(14)
            .build())).get();
    assertEquals(1, result.getSuccessCount());
    assertEquals(0, result.getFailureCount());

    try {
      UserRecord savedUser = auth.getUserAsync(randomUser.uid).get();
      assertEquals(randomUser.email, savedUser.getEmail());
      String idToken = signInWithPassword(randomUser.email, "password");
      assertFalse(Strings.isNullOrEmpty(idToken));
    } finally {
      auth.deleteUserAsync(randomUser.uid).get();
    }
  }

  @Test
  public void testGeneratePasswordResetLink() throws Exception {
    RandomUser user = RandomUser.create();
    auth.createUser(new CreateRequest()
        .setUid(user.uid)
        .setEmail(user.email)
        .setEmailVerified(false)
        .setPassword("password"));
    try {
      String link = auth.generatePasswordResetLink(user.email, ActionCodeSettings.builder()
          .setUrl(ACTION_LINK_CONTINUE_URL)
          .setHandleCodeInApp(false)
          .build());
      Map<String, String> linkParams = parseLinkParameters(link);
      assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
      String email = resetPassword(user.email, "password", "newpassword",
          linkParams.get("oobCode"));
      assertEquals(user.email, email);
      // Password reset also verifies the user's email
      assertTrue(auth.getUser(user.uid).isEmailVerified());
    } finally {
      auth.deleteUser(user.uid);
    }
  }

  @Test
  public void testGenerateEmailVerificationResetLink() throws Exception {
    RandomUser user = RandomUser.create();
    auth.createUser(new CreateRequest()
        .setUid(user.uid)
        .setEmail(user.email)
        .setEmailVerified(false)
        .setPassword("password"));
    try {
      String link = auth.generateEmailVerificationLink(user.email, ActionCodeSettings.builder()
          .setUrl(ACTION_LINK_CONTINUE_URL)
          .setHandleCodeInApp(false)
          .build());
      Map<String, String> linkParams = parseLinkParameters(link);
      assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
      // There doesn't seem to be a public API for verifying an email, so we cannot do a more
      // thorough test here.
      assertEquals("verifyEmail", linkParams.get("mode"));
    } finally {
      auth.deleteUser(user.uid);
    }
  }

  @Test
  public void testGenerateSignInWithEmailLink() throws Exception {
    RandomUser user = RandomUser.create();
    auth.createUser(new CreateRequest()
        .setUid(user.uid)
        .setEmail(user.email)
        .setEmailVerified(false)
        .setPassword("password"));
    try {
      String link = auth.generateSignInWithEmailLink(user.email, ActionCodeSettings.builder()
          .setUrl(ACTION_LINK_CONTINUE_URL)
          .setHandleCodeInApp(false)
          .build());
      Map<String, String> linkParams = parseLinkParameters(link);
      assertEquals(ACTION_LINK_CONTINUE_URL, linkParams.get("continueUrl"));
      String idToken = signInWithEmailLink(user.email, linkParams.get("oobCode"));
      assertFalse(Strings.isNullOrEmpty(idToken));
      assertTrue(auth.getUser(user.uid).isEmailVerified());
    } finally {
      auth.deleteUser(user.uid);
    }
  }

  private Map<String, String> parseLinkParameters(String link) throws Exception {
    Map<String, String> result = new HashMap<>();
    int queryBegin = link.indexOf('?');
    if (queryBegin != -1) {
      String[] segments = link.substring(queryBegin + 1).split("&");
      for (String segment : segments) {
        int equalSign = segment.indexOf('=');
        String key = segment.substring(0, equalSign);
        String value = segment.substring(equalSign + 1);
        result.put(key, URLDecoder.decode(value, "UTF-8"));
      }
    }
    return result;
  }

  static String randomPhoneNumber() {
    Random random = new Random();
    StringBuilder builder = new StringBuilder("+1");
    for (int i = 0; i < 10; i++) {
      builder.append(random.nextInt(10));
    }
    return builder.toString();
  }

  private String signInWithCustomToken(String customToken) throws IOException {
    GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key="
        + IntegrationTestUtils.getApiKey());
    Map<String, Object> content = ImmutableMap.<String, Object>of(
        "token", customToken, "returnSecureToken", true);
    HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
        new JsonHttpContent(jsonFactory, content));
    request.setParser(new JsonObjectParser(jsonFactory));
    HttpResponse response = request.execute();
    try {
      GenericJson json = response.parseAs(GenericJson.class);
      return json.get("idToken").toString();
    } finally {
      response.disconnect();
    }
  }

  private String signInWithPassword(String email, String password) throws IOException {
    GenericUrl url = new GenericUrl(VERIFY_PASSWORD_URL + "?key="
        + IntegrationTestUtils.getApiKey());
    Map<String, Object> content = ImmutableMap.<String, Object>of(
        "email", email, "password", password, "returnSecureToken", true);
    HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
        new JsonHttpContent(jsonFactory, content));
    request.setParser(new JsonObjectParser(jsonFactory));
    HttpResponse response = request.execute();
    try {
      GenericJson json = response.parseAs(GenericJson.class);
      return json.get("idToken").toString();
    } finally {
      response.disconnect();
    }
  }

  private String resetPassword(
      String email, String oldPassword, String newPassword, String oobCode) throws IOException {
    GenericUrl url = new GenericUrl(RESET_PASSWORD_URL + "?key="
        + IntegrationTestUtils.getApiKey());
    Map<String, Object> content = ImmutableMap.<String, Object>of(
        "email", email, "oldPassword", oldPassword, "newPassword", newPassword, "oobCode", oobCode);
    HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
        new JsonHttpContent(jsonFactory, content));
    request.setParser(new JsonObjectParser(jsonFactory));
    HttpResponse response = request.execute();
    try {
      GenericJson json = response.parseAs(GenericJson.class);
      return json.get("email").toString();
    } finally {
      response.disconnect();
    }
  }

  private String signInWithEmailLink(
      String email, String oobCode) throws IOException {
    GenericUrl url = new GenericUrl(EMAIL_LINK_SIGN_IN_URL + "?key="
        + IntegrationTestUtils.getApiKey());
    Map<String, Object> content = ImmutableMap.<String, Object>of(
        "email", email, "oobCode", oobCode);
    HttpRequest request = transport.createRequestFactory().buildPostRequest(url,
        new JsonHttpContent(jsonFactory, content));
    request.setParser(new JsonObjectParser(jsonFactory));
    HttpResponse response = request.execute();
    try {
      GenericJson json = response.parseAs(GenericJson.class);
      return json.get("idToken").toString();
    } finally {
      response.disconnect();
    }
  }

  private void checkRecreate(String uid) throws Exception {
    try {
      auth.createUserAsync(new CreateRequest().setUid(uid)).get();
      fail("No error thrown for creating user with existing ID");
    } catch (ExecutionException e) {
      assertTrue(e.getCause() instanceof FirebaseAuthException);
      assertEquals("uid-already-exists", ((FirebaseAuthException) e.getCause()).getErrorCode());
    }
  }

  static class RandomUser {
    final String uid;
    final String email;

    private RandomUser(String uid, String email) {
      this.uid = uid;
      this.email = email;
    }

    static RandomUser create() {
      final String uid = UUID.randomUUID().toString().replaceAll("-", "");
      final String email = ("test" + uid.substring(0, 12) + "@example."
          + uid.substring(12) + ".com").toLowerCase();
      return new RandomUser(uid, email);
    }
  }

  static UserRecord newUserWithParams() throws Exception {
    return newUserWithParams(auth);
  }

  static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception {
    // TODO(rsgowman): This function could be used throughout this file (similar to the other
    // ports).
    RandomUser randomUser = RandomUser.create();
    return auth.createUser(new CreateRequest()
                               .setUid(randomUser.uid)
                               .setEmail(randomUser.email)
                               .setPhoneNumber(randomPhoneNumber())
                               .setDisplayName("Random User")
                               .setPhotoUrl("https://example.com/photo.png")
                               .setPassword("password"));
  }
}