/*
 * 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.database.integration;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
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.cedarsoftware.util.DeepEquals;
import com.google.common.collect.ImmutableList;
import com.google.firebase.FirebaseApp;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseException;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.EventRecord;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.GenericTypeIndicator;
import com.google.firebase.database.MapBuilder;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.ServerValue;
import com.google.firebase.database.TestFailure;
import com.google.firebase.database.TestHelpers;
import com.google.firebase.database.Transaction;
import com.google.firebase.database.ValueEventListener;
import com.google.firebase.database.core.DatabaseConfig;
import com.google.firebase.database.core.Path;
import com.google.firebase.database.core.RepoManager;
import com.google.firebase.database.core.view.Event;
import com.google.firebase.database.future.ReadFuture;
import com.google.firebase.database.future.WriteFuture;
import com.google.firebase.testing.IntegrationTestUtils;
import com.google.firebase.testing.TestUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;

public class DataTestIT {

  private static FirebaseApp masterApp;

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

  @Before
  public void prepareApp() {
    TestHelpers.wrapForErrorHandling(masterApp);
  }

  @After
  public void checkAndCleanupApp() {
    TestHelpers.assertAndUnwrapErrorHandlers(masterApp);
  }

  @Test
  public void testBasicInstantiation() {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    assertTrue(ref != null);
  }

  @Test
  public void testWriteData() {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    // just make sure it doesn't throw
    ref.setValueAsync(42);
    assertTrue(true);
  }

  @Test
  public void testReadAndWrite() throws InterruptedException, TimeoutException, TestFailure {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    ReadFuture future = ReadFuture.untilNonNull(ref);

    ref.setValueAsync(42);
    List<EventRecord> events = future.timedGet();
    assertEquals(42L, events.get(events.size() - 1).getSnapshot().getValue());
  }

  @Test
  public void testValueReturnsJsonForNodesWithChildren()
      throws TimeoutException, InterruptedException, TestFailure {
    Map<String, Object> expected = new HashMap<>();
    Map<String, Object> innerExpected = new HashMap<>();
    innerExpected.put("bar", 5L);
    expected.put("foo", innerExpected);

    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    ReadFuture future = ReadFuture.untilNonNull(ref);

    ref.setValueAsync(expected);
    List<EventRecord> events = future.timedGet();
    EventRecord eventRecord = events.get(events.size() - 1);
    Object result = eventRecord.getSnapshot().getValue();
    TestHelpers.assertDeepEquals(expected, result);
  }

  @Test
  public void testWriteAndWaitForServerConfirmation()
      throws TimeoutException, InterruptedException, TestFailure {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    ref.setValueAsync(42);

    ReadFuture future = new ReadFuture(ref);

    EventRecord eventRecord = future.timedGet().get(0);
    assertEquals(42L, eventRecord.getSnapshot().getValue());
  }

  @Test
  public void testWriteValueReconnectRead()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference reader = refs.get(0);
    DatabaseReference writer = refs.get(1);

    WriteFuture writeFuture = new WriteFuture(writer, 42);
    writeFuture.timedGet();

    ReadFuture future = new ReadFuture(reader);

    EventRecord eventRecord = future.timedGet().get(0);
    long result = (Long) eventRecord.getSnapshot().getValue();
    assertEquals(42L, result);
    assertEquals(42, (int) eventRecord.getSnapshot().getValue(Integer.class));
  }

  @Test
  public void testMultipleWriteValuesReconnectRead()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    final DatabaseReference writer = refs.get(0);
    final DatabaseReference reader = refs.get(1);

    writer.child("a").child("b").child("c").setValueAsync(1);
    writer.child("a").child("d").child("e").setValueAsync(2);
    writer.child("a").child("d").child("f").setValueAsync(3);
    WriteFuture writeFuture = new WriteFuture(writer.child("g"), 4);
    writeFuture.timedGet();

    Map<String, Object> expected = new MapBuilder()
        .put("a",
            new MapBuilder().put("b", new MapBuilder().put("c", 1L).build())
                .put("d", new MapBuilder().put("e", 2L).put("f", 3L).build()).build())
        .put("g", 4L).build();
    ReadFuture readFuture = new ReadFuture(reader);

    GenericTypeIndicator<Map<String, Object>> t = new GenericTypeIndicator<Map<String, Object>>() {
    };
    Map<String, Object> result = readFuture.timedGet().get(0).getSnapshot().getValue(t);
    TestHelpers.assertDeepEquals(expected, result);
  }

  @Test
  public void testWriteLeafNodeOverwriteParentNodeWaitForEvents() throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    EventHelper helper = new EventHelper().addValueExpectation(ref.child("a/aa"))
        .addChildExpectation(ref.child("a"), Event.EventType.CHILD_ADDED, "aa")
        .addValueExpectation(ref.child("a")).addValueExpectation(ref.child("a/aa"))
        .addChildExpectation(ref.child("a"), Event.EventType.CHILD_CHANGED, "aa")
        .addValueExpectation(ref.child("a")).startListening(true);

    ref.child("a/aa").setValueAsync(1);
    ref.child("a").setValueAsync(new MapBuilder().put("aa", 2).build());

    assertTrue(helper.waitForEvents());
    helper.cleanup();
  }

  @Test
  public void testWriteLeafNodeOverwriteAtParentMultipleTimesWaitForExpectedEvents()
      throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    final AtomicInteger bbCount = new AtomicInteger(0);
    final ValueEventListener listener = ref.child("a/bb")
        .addValueEventListener(new ValueEventListener() {
          @Override
          public void onDataChange(DataSnapshot snapshot) {
            assertNull(snapshot.getValue());
            assertEquals(1, bbCount.incrementAndGet());
          }

          @Override
          public void onCancelled(DatabaseError error) {
          }
        });

    final EventHelper helper = new EventHelper().addValueExpectation(ref.child("a/aa"))
        .addChildExpectation(ref.child("a"), Event.EventType.CHILD_ADDED, "aa")
        .addValueExpectation(ref.child("a")).addValueExpectation(ref.child("a/aa"))
        .addChildExpectation(ref.child("a"), Event.EventType.CHILD_CHANGED, "aa")
        .addValueExpectation(ref.child("a")).addValueExpectation(ref.child("a/aa"))
        .addChildExpectation(ref.child("a"), Event.EventType.CHILD_CHANGED, "aa")
        .addValueExpectation(ref.child("a")).startListening(true);

    ref.child("a/aa").setValueAsync(1);
    ref.child("a").setValueAsync(MapBuilder.of("aa", 2));
    ref.child("a").setValueAsync(MapBuilder.of("aa", 3));
    ref.child("a").setValueAsync(MapBuilder.of("aa", 3));

    assertTrue(helper.waitForEvents());
    helper.cleanup();
    assertEquals(1, bbCount.get());
    ref.child("a/bb").removeEventListener(listener);
  }

  @Test
  public void testWriteParentNodeOverwriteAtLeafNodeWaitForEvents() throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    EventHelper helper = new EventHelper().addValueExpectation(ref.child("a/aa"))
        .addChildExpectation(ref.child("a"), Event.EventType.CHILD_ADDED, "aa")
        .addValueExpectation(ref.child("a")).addValueExpectation(ref.child("a/aa"))
        .addChildExpectation(ref.child("a"), Event.EventType.CHILD_CHANGED, "aa")
        .addValueExpectation(ref.child("a")).startListening(true);

    ref.child("a").setValueAsync(new MapBuilder().put("aa", 2).build());
    ref.child("a/aa").setValueAsync(1);

    assertTrue(helper.waitForEvents());
    helper.cleanup();
  }

  @Test
  @Ignore
  public void testWriteLeafNodeRemoveParentWaitForEvents()
      throws InterruptedException, TimeoutException, TestFailure, ExecutionException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference reader = refs.get(0);
    DatabaseReference writer = refs.get(1);

    EventHelper writeHelper = new EventHelper().addValueExpectation(writer.child("a/aa"))
        .addChildExpectation(writer.child("a"), Event.EventType.CHILD_ADDED, "aa")
        .addValueExpectation(writer.child("a"))
        .addChildExpectation(writer, Event.EventType.CHILD_ADDED, "a").addValueExpectation(writer)
        .startListening(true);

    WriteFuture w = new WriteFuture(writer.child("a/aa"), 42);
    assertTrue(writeHelper.waitForEvents());

    w.timedGet();
    EventHelper readHelper = new EventHelper().addValueExpectation(reader.child("a/aa"))
        .addChildExpectation(reader.child("a"), Event.EventType.CHILD_ADDED, "aa")
        .addValueExpectation(reader.child("a"))
        .addChildExpectation(reader, Event.EventType.CHILD_ADDED, "a").addValueExpectation(reader)
        .startListening();

    assertTrue(readHelper.waitForEvents());

    readHelper.addValueExpectation(reader.child("a/aa"))
        .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa")
        .addValueExpectation(reader.child("a"))
        .addChildExpectation(reader, Event.EventType.CHILD_REMOVED, "a").addValueExpectation(reader)
        .startListening();

    writeHelper.addValueExpectation(reader.child("a/aa"))
        .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa")
        .addValueExpectation(reader.child("a"))
        .addChildExpectation(reader, Event.EventType.CHILD_REMOVED, "a").addValueExpectation(reader)
        .startListening();

    writer.child("a").removeValueAsync();
    assertTrue(writeHelper.waitForEvents());
    assertTrue(readHelper.waitForEvents());
    writeHelper.cleanup();
    readHelper.cleanup();

    // Make sure we actually have null there now
    assertNull(TestHelpers.getSnap(reader).getValue());
    assertNull(TestHelpers.getSnap(writer).getValue());

    ReadFuture readFuture = ReadFuture.untilNonNull(reader);

    ReadFuture writeFuture = ReadFuture.untilNonNull(writer);

    writer.child("a/aa").setValueAsync(3.1415);

    List<EventRecord> readerEvents = readFuture.timedGet();
    List<EventRecord> writerEvents = writeFuture.timedGet();

    DataSnapshot readerSnap = readerEvents.get(readerEvents.size() - 1).getSnapshot();
    readerSnap = readerSnap.child("a/aa");
    assertEquals(3.1415, readerSnap.getValue());

    DataSnapshot writerSnap = writerEvents.get(writerEvents.size() - 1).getSnapshot();
    writerSnap = writerSnap.child("a/aa");
    assertEquals(3.1415, writerSnap.getValue());
  }

  @Test
  @Ignore
  public void testWriteLeafNodeRemoveLeafNodeWaitForEvents()
      throws InterruptedException, TimeoutException, TestFailure, ExecutionException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference reader = refs.get(0);
    DatabaseReference writer = refs.get(1);

    EventHelper writeHelper = new EventHelper().addValueExpectation(writer.child("a/aa"))
        .addChildExpectation(writer.child("a"), Event.EventType.CHILD_ADDED, "aa")
        .addValueExpectation(writer.child("a"))
        .addChildExpectation(writer, Event.EventType.CHILD_ADDED, "a").addValueExpectation(writer)
        .startListening(true);

    WriteFuture w = new WriteFuture(writer.child("a/aa"), 42);
    assertTrue(writeHelper.waitForEvents());

    w.timedGet();
    EventHelper readHelper = new EventHelper().addValueExpectation(reader.child("a/aa"))
        .addChildExpectation(reader.child("a"), Event.EventType.CHILD_ADDED, "aa")
        .addValueExpectation(reader.child("a"))
        .addChildExpectation(reader, Event.EventType.CHILD_ADDED, "a").addValueExpectation(reader)
        .startListening();

    assertTrue(readHelper.waitForEvents());

    readHelper.addValueExpectation(reader.child("a/aa"))
        .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa")
        .addValueExpectation(reader.child("a"))
        .addChildExpectation(reader, Event.EventType.CHILD_REMOVED, "a").addValueExpectation(reader)
        .startListening();

    writeHelper.addValueExpectation(reader.child("a/aa"))
        .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa")
        .addValueExpectation(reader.child("a"))
        .addChildExpectation(reader, Event.EventType.CHILD_REMOVED, "a").addValueExpectation(reader)
        .startListening();

    writer.child("a/aa").removeValueAsync();
    assertTrue(writeHelper.waitForEvents());
    assertTrue(readHelper.waitForEvents());
    writeHelper.cleanup();
    readHelper.cleanup();

    DataSnapshot readerSnap = TestHelpers.getSnap(reader);
    assertNull(readerSnap.getValue());

    DataSnapshot writerSnap = TestHelpers.getSnap(writer);
    assertNull(writerSnap.getValue());

    readerSnap = TestHelpers.getSnap(reader.child("a/aa"));
    assertNull(readerSnap.getValue());

    writerSnap = TestHelpers.getSnap(writer.child("a/aa"));
    assertNull(writerSnap.getValue());

    ReadFuture readFuture = ReadFuture.untilNonNull(reader);
    ReadFuture writeFuture = ReadFuture.untilNonNull(writer);

    writer.child("a/aa").setValueAsync(3.1415);

    final List<EventRecord> readerEvents = readFuture.timedGet();
    final List<EventRecord> writerEvents = writeFuture.timedGet();

    readerSnap = readerEvents.get(readerEvents.size() - 1).getSnapshot();
    readerSnap = readerSnap.child("a/aa");
    assertEquals(3.1415, readerSnap.getValue());

    writerSnap = writerEvents.get(writerEvents.size() - 1).getSnapshot();
    writerSnap = writerSnap.child("a/aa");
    assertEquals(3.1415, writerSnap.getValue());
  }

  @Test
  public void testWriteMultipleLeafNodesRemoveOneLeafNodeWaitForEvents()
      throws InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference reader = refs.get(0);
    DatabaseReference writer = refs.get(1);

    final EventHelper writeHelper = new EventHelper().addValueExpectation(writer.child("a/aa"))
        .addChildExpectation(writer.child("a"), Event.EventType.CHILD_ADDED, "aa")
        .addValueExpectation(writer.child("a"))
        .addChildExpectation(writer, Event.EventType.CHILD_ADDED, "a").addValueExpectation(writer)
        .addChildExpectation(writer.child("a"), Event.EventType.CHILD_ADDED, "bb")
        .addValueExpectation(writer.child("a"))
        .addChildExpectation(writer, Event.EventType.CHILD_CHANGED, "a").addValueExpectation(writer)
        .startListening(true);

    final EventHelper readHelper = new EventHelper().addValueExpectation(reader.child("a/aa"))
        .addChildExpectation(reader.child("a"), Event.EventType.CHILD_ADDED, "aa")
        .addValueExpectation(reader.child("a"))
        .addChildExpectation(reader, Event.EventType.CHILD_ADDED, "a").addValueExpectation(reader)
        .addChildExpectation(reader.child("a"), Event.EventType.CHILD_ADDED, "bb")
        .addValueExpectation(reader.child("a"))
        .addChildExpectation(reader, Event.EventType.CHILD_CHANGED, "a").addValueExpectation(reader)
        .startListening(true);

    writer.child("a/aa").setValueAsync(42);
    writer.child("a/bb").setValueAsync(24);

    assertTrue(writeHelper.waitForEvents());
    assertTrue(readHelper.waitForEvents());

    readHelper.addValueExpectation(reader.child("a/aa"))
        .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa")
        .addValueExpectation(reader.child("a"))
        .addChildExpectation(reader, Event.EventType.CHILD_CHANGED, "a").addValueExpectation(reader)
        .startListening();

    writeHelper.addValueExpectation(writer.child("a/aa"))
        .addChildExpectation(writer.child("a"), Event.EventType.CHILD_REMOVED, "aa")
        .addValueExpectation(writer.child("a"))
        .addChildExpectation(writer, Event.EventType.CHILD_CHANGED, "a").addValueExpectation(writer)
        .startListening();

    writer.child("a/aa").removeValueAsync();
    assertTrue(writeHelper.waitForEvents());
    assertTrue(readHelper.waitForEvents());

    DataSnapshot readerSnap = TestHelpers.getSnap(reader);
    DataSnapshot writerSnap = TestHelpers.getSnap(writer);
    Map<String, Object> expected = new MapBuilder()
        .put("a", new MapBuilder().put("bb", 24L).build()).build();
    TestHelpers.assertDeepEquals(expected, readerSnap.getValue());
    TestHelpers.assertDeepEquals(expected, writerSnap.getValue());

    readHelper.cleanup();
    writeHelper.cleanup();
  }

  @Test
  public void testVerifyNodesStartingWithPeriod() {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    try {
      ref.child(".foo");
      fail("Should fail");
    } catch (DatabaseException e) {
      // No-op
    }

    try {
      ref.child("foo/.foo");
      fail("Should fail");
    } catch (DatabaseException e) {
      // No-op
    }
  }

  // NOTE: skipping test re: writing .keys and .length. Those features will
  // require a new client
  // anyways

  @Test
  public void testNumericKeysGetTurnedIntoArrays() throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    ref.child("0").setValueAsync("alpha");
    ref.child("1").setValueAsync("bravo");
    ref.child("2").setValueAsync("charlie");
    ref.child("3").setValueAsync("delta");
    ref.child("4").setValueAsync("echo");

    DataSnapshot snap = TestHelpers.getSnap(ref);
    List<Object> expected = ImmutableList.of((Object) "alpha", "bravo", "charlie",
        "delta", "echo");
    TestHelpers.assertDeepEquals(expected, snap.getValue());
  }

  @Test
  public void testWriteFullJsonObjectsWithSetAndGetThemBack()
      throws TimeoutException, InterruptedException, TestFailure {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    Map<String, Object> expected = new MapBuilder()
        .put("a", new MapBuilder().put("aa", 5L).put("ab", 3L).build())
        .put("b",
            new MapBuilder().put("ba", "hey there!")
                .put("bb", MapBuilder.of("bba", false)).build())
        .put("c", ImmutableList.of(0L, new MapBuilder().put("c_1", 4L).build(), "hey", true, false,
            "dude"))
        .build();

    ReadFuture readFuture = ReadFuture.untilNonNull(ref);

    ref.setValueAsync(expected);
    List<EventRecord> events = readFuture.timedGet();
    Object result = events.get(events.size() - 1).getSnapshot().getValue();
    TestHelpers.assertDeepEquals(expected, result);
  }

  // NOTE: skipping test for value in callback. DataSnapshot is same instance in
  // callback as after
  // future is complete

  // NOTE: skipping test for passing a value to push. Not applicable.

  @Test
  public void testRemoveCallbackIsHit()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    WriteFuture writeFuture = new WriteFuture(ref, 42);
    writeFuture.timedGet();

    ReadFuture readFuture = new ReadFuture(ref, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        return events.get(events.size() - 1).getSnapshot().getValue() == null;
      }
    });

    final Semaphore callbackHit = new Semaphore(0);
    ref.removeValue(new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference callbackRef) {
        assertEquals(ref, callbackRef);
        callbackHit.release(1);
      }
    });

    readFuture.timedGet();
    assertTrue(callbackHit.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, MILLISECONDS));
    // We test the value in the completion condition
  }

  @Test
  public void testRemoveCallbackIsHitForAlreadyRemovedNodes() throws InterruptedException {
    final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    final Semaphore callbackHit = new Semaphore(0);
    ref.removeValue(new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference callbackRef) {
        assertEquals(ref, callbackRef);
        callbackHit.release(1);
      }
    });
    ref.removeValue(new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference callbackRef) {
        assertEquals(ref, callbackRef);
        callbackHit.release(1);
      }
    });

    assertTrue(
        callbackHit.tryAcquire(2, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
  }

  @Test
  public void testUsingNumbersAsKeys()
      throws TimeoutException, InterruptedException, TestFailure {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    ref.child("3024").setValueAsync(5);
    ReadFuture future = new ReadFuture(ref);

    List<EventRecord> events = future.timedGet();
    assertFalse(events.get(0).getSnapshot().getValue() instanceof List);
  }

  @Test
  public void testOnceWithCallbackHitsServerToGetData()
      throws InterruptedException, ExecutionException, TimeoutException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 3);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);
    DatabaseReference reader2 = refs.get(2);

    final Semaphore semaphore = new Semaphore(0);
    reader.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        assertEquals(null, snapshot.getValue());
        semaphore.release(1);
      }

      @Override
      public void onCancelled(DatabaseError error) {
        fail("Shouldn't happen");
      }
    });

    TestHelpers.waitFor(semaphore);
    new WriteFuture(writer, 42).timedGet();

    reader2.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        assertEquals(42L, snapshot.getValue());
        semaphore.release(1);
      }

      @Override
      public void onCancelled(DatabaseError error) {
        fail("Shouldn't happen");
      }
    });

    TestHelpers.waitFor(semaphore);
  }

  // NOTE: skipping forEach abort test. Not relevant, we return an iterable that
  // can be stopped
  // any
  // time.

  @Test
  public void testSetAndThenListenForValueEvents()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    new WriteFuture(ref, "cabbage").timedGet();
    EventRecord event = new ReadFuture(ref).timedGet().get(0);

    assertEquals("cabbage", event.getSnapshot().getValue());
  }

  @Test
  public void testHasChildren()
      throws TimeoutException, InterruptedException, TestFailure {
    final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    ref.setValueAsync(new MapBuilder()
        .put("one", 42)
        .put("two", new MapBuilder().put("a", 5).build())
        .put("three", new MapBuilder().put("a", 5).put("b", 6).build()).build());
    final AtomicBoolean removedTwo = new AtomicBoolean(false);
    ReadFuture readFuture = new ReadFuture(ref, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        if (removedTwo.compareAndSet(false, true)) {
          // removedTwo did equal false, now equals true
          try {
            ref.child("two").removeValueAsync();
          } catch (DatabaseException e) {
            fail("Should not fail");
          }
        }
        return events.size() == 2;
      }
    });

    List<EventRecord> events = readFuture.timedGet();
    DataSnapshot firstSnap = events.get(0).getSnapshot();
    assertEquals(3, firstSnap.getChildrenCount());
    assertEquals(0, firstSnap.child("one").getChildrenCount());
    assertEquals(1, firstSnap.child("two").getChildrenCount());
    assertEquals(2, firstSnap.child("three").getChildrenCount());
    assertEquals(0, firstSnap.child("four").getChildrenCount());

    DataSnapshot secondSnap = events.get(1).getSnapshot();
    assertEquals(2, secondSnap.getChildrenCount());
    assertEquals(0, secondSnap.child("two").getChildrenCount());
  }

  @Test
  public void testSetNodeWithChildrenToPrimitiveValueAndBack()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference reader = refs.get(0);
    final DatabaseReference writer = refs.get(1);

    final Map<String, Object> json = new MapBuilder().put("a", 5L).put("b", 6L).build();
    final long primitive = 76L;

    new WriteFuture(writer, json).timedGet();

    final AtomicBoolean sawJson = new AtomicBoolean(false);
    final AtomicBoolean sawPrimitive = new AtomicBoolean(false);

    ReadFuture readFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        if (sawJson.compareAndSet(false, true)) {
          try {
            writer.setValueAsync(primitive);
          } catch (DatabaseException e) {
            fail("Shouldn't happen: " + e.toString());
          }
        } else {
          // Saw the json already
          if (sawPrimitive.compareAndSet(false, true)) {
            try {
              writer.setValueAsync(json);
            } catch (DatabaseException e) {
              fail("Shouldn't happen: " + e.toString());
            }
          }
        }
        return events.size() == 3;
      }
    });

    List<EventRecord> events = readFuture.timedGet();
    DataSnapshot readSnap = events.get(0).getSnapshot();
    assertTrue(readSnap.hasChildren());
    TestHelpers.assertDeepEquals(json, readSnap.getValue());

    readSnap = events.get(1).getSnapshot();
    assertFalse(readSnap.hasChildren());
    assertEquals(primitive, readSnap.getValue());

    readSnap = events.get(2).getSnapshot();
    assertTrue(readSnap.hasChildren());
    TestHelpers.assertDeepEquals(json, readSnap.getValue());
  }

  @Test
  public void testWriteLeafNodeRemoveItTryToAddChildToRemovedNode()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    final DatabaseReference reader = refs.get(0);
    final DatabaseReference writer = refs.get(1);

    writer.setValueAsync(5);
    writer.removeValueAsync();
    new WriteFuture(writer.child("abc"), 5).timedGet();

    DataSnapshot snap = new ReadFuture(reader).timedGet().get(0).getSnapshot();

    assertEquals(5L, ((Map) snap.getValue()).get("abc"));
  }

  @Test
  public void testListenForValueThenWriteOnNodeWithExistingData()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference reader = refs.get(0);
    DatabaseReference writer = refs.get(1);

    new WriteFuture(writer, new MapBuilder().put("a", 5).put("b", 2).build()).timedGet();

    ReadFuture readFuture = new ReadFuture(reader);

    // Slight race condition. We're banking on this local set being processed
    // before the network catches up with the writer's broadcast.
    reader.child("a").setValueAsync(10);

    EventRecord event = readFuture.timedGet().get(0);
    assertEquals(10L, event.getSnapshot().child("a").getValue());
  }

  @Test
  public void testSetPriorityOnNonexistentNode() throws InterruptedException {
    final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    final Semaphore semaphore = new Semaphore(0);
    ref.setPriority(1, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference callbackRef) {
        assertEquals(ref, callbackRef);
        assertNotNull(error);
        semaphore.release(1);
      }
    });

    assertTrue(semaphore.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
  }

  @Test
  public void testSetPriority() throws InterruptedException {
    final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    ref.setValueAsync("hello!");
    final Semaphore semaphore = new Semaphore(0);
    ref.setPriority(10, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference callbackRef) {
        assertEquals(ref, callbackRef);
        assertNull(error);
        semaphore.release(1);
      }
    });

    assertTrue(semaphore.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
  }

  @Test
  public void testSetWithPriority()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    final DatabaseReference ref1 = refs.get(0);
    final DatabaseReference ref2 = refs.get(1);

    ReadFuture readFuture = ReadFuture.untilNonNull(ref1);

    new WriteFuture(ref1, "hello", 5).timedGet();
    List<EventRecord> result = readFuture.timedGet();
    DataSnapshot snap = result.get(result.size() - 1).getSnapshot();
    assertEquals(5.0, snap.getPriority());
    assertEquals("hello", snap.getValue());

    result = ReadFuture.untilNonNull(ref2).timedGet();
    snap = result.get(result.size() - 1).getSnapshot();
    assertEquals(5.0, snap.getPriority());
    assertEquals("hello", snap.getValue());
  }

  // NOTE: skipped test of getPriority on snapshot. Tested above

  // NOTE: skipped test of immediately visible priority changes. Tested above

  @Test
  public void testSetOverwritesPriorityOfTopLevelNodesAndChildren()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    final DatabaseReference ref1 = refs.get(0);
    final DatabaseReference ref2 = refs.get(1);

    ref1.setValueAsync(new MapBuilder().put("a", 5).build());
    ref1.setPriorityAsync(10);
    ref1.child("a").setPriorityAsync(18);
    new WriteFuture(ref1, new MapBuilder().put("a", 7).build()).timedGet();

    DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot();

    assertNull(snap.getPriority());
    assertNull(snap.child("a").getPriority());
  }

  @Test
  public void testSetWithPriorityOfLeafNodes()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference ref1 = refs.get(0);
    DatabaseReference ref2 = refs.get(1);

    new WriteFuture(ref1, "testleaf", "992").timedGet();

    DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot();

    assertEquals("992", snap.getPriority());
  }

  @Test
  public void testSetPriorityOfAnObject()
      throws ExecutionException, TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference ref1 = refs.get(0);
    DatabaseReference ref2 = refs.get(1);

    new WriteFuture(ref1, new MapBuilder().put("a", 5).build(), "991").timedGet();

    DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot();

    assertEquals("991", snap.getPriority());
  }

  // NOTE: skipping a test about following setPriority with set, it's tested
  // above

  @Test
  public void testGetPriority()
      throws TimeoutException, InterruptedException, TestFailure {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref, 7);

    ref.setValueAsync("a");
    ref.setValueAsync("b", 5);
    ref.setValueAsync("c", "6");
    ref.setValueAsync("d", 7);
    ref.setValueAsync(new MapBuilder().put(".value", "e").put(".priority", 8).build());
    ref.setValueAsync(new MapBuilder().put(".value", "f").put(".priority", "8").build());
    ref.setValueAsync(new MapBuilder().put(".value", "g").put(".priority", null).build());

    List<EventRecord> events = readFuture.timedGet();
    assertNull(events.get(0).getSnapshot().getPriority());
    assertEquals(5.0, events.get(1).getSnapshot().getPriority());
    assertEquals("6", events.get(2).getSnapshot().getPriority());
    assertEquals(7.0, events.get(3).getSnapshot().getPriority());
    assertEquals(8.0, events.get(4).getSnapshot().getPriority());
    assertEquals("8", events.get(5).getSnapshot().getPriority());
    assertNull(events.get(6).getSnapshot().getPriority());
  }

  @Test
  public void testNormalizeDifferentIntegerAndDoubleValues()
      throws DatabaseException, InterruptedException, TimeoutException, TestFailure {
    final long intMaxPlusOne = 2147483648L;

    DatabaseReference node = IntegrationTestUtils.getRandomNode(masterApp);
    Object[] writtenValues = {
        intMaxPlusOne,
        (double) intMaxPlusOne,
        -intMaxPlusOne,
        (double) -intMaxPlusOne,
        Integer.MAX_VALUE,
        0L,
        0.0,
        -0.0f,
        0
    };

    Object[] readValues = {intMaxPlusOne, -intMaxPlusOne, (long) Integer.MAX_VALUE, 0L};
    ReadFuture readFuture = ReadFuture.untilCountAfterNull(node, readValues.length);
    for (Object value : writtenValues) {
      node.setValueAsync(value);
    }

    List<EventRecord> events = readFuture.timedGet();
    for (int i = 0; i < readValues.length; ++i) {
      assertEquals(readValues[i], events.get(i).getSnapshot().getValue());
    }
  }

  @Test
  public void testExportFormatIncludesPriorities()
      throws TimeoutException, InterruptedException, TestFailure {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    Map<String, Object> expected = new MapBuilder().put("foo",
        new MapBuilder()
            .put("bar", new MapBuilder().put(".priority", 7.0).put(".value", 5L).build())
            .put(".priority", "hi").build())
        .build();
    ReadFuture readFuture = ReadFuture.untilNonNull(ref);
    ref.setValueAsync(expected);
    DataSnapshot snap = readFuture.timedGet().get(0).getSnapshot();
    Object result = snap.getValue(true);
    TestHelpers.assertDeepEquals(expected, result);
  }

  @Test
  public void testPriorityIsOverwrittenByServerPriority()
      throws TimeoutException, InterruptedException, TestFailure {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference ref1 = refs.get(0);
    final DatabaseReference ref2 = refs.get(1);

    ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref1, 2);

    ref1.setValueAsync("hi", 100);

    new ReadFuture(ref2, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        DataSnapshot snap = events.get(events.size() - 1).getSnapshot();
        Object priority = snap.getPriority();
        if (priority != null && priority.equals(100.0)) {
          try {
            ref2.setValueAsync("whatever");
          } catch (DatabaseException e) {
            fail("Shouldn't happen: " + e.toString());
          }
          return true;
        }
        return false;
      }
    }).timedGet();

    List<EventRecord> events = readFuture.timedGet();
    assertEquals(100.0, events.get(0).getSnapshot().getPriority());
    assertNull(events.get(1).getSnapshot().getPriority());
  }

  @Test
  public void testLargeNumericPriorities()
      throws TestFailure, TimeoutException, InterruptedException, ExecutionException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference ref1 = refs.get(0);
    DatabaseReference ref2 = refs.get(1);

    double priority = 1356721306842.0;

    new WriteFuture(ref1, 5, priority).timedGet();
    DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot();
    assertEquals(priority, snap.getPriority());
  }

  @Test
  public void testUrlEncodingAndDecoding()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {

    DatabaseReference ref = FirebaseDatabase.getInstance(masterApp)
        .getReference("/a%b&[email protected]/space: /non-ascii:ΓΈ");
    String result = ref.toString();
    String expected = IntegrationTestUtils.getDatabaseUrl()
        + "/a%25b%26c%40d/space%3A%20/non-ascii%3A%C3%B8";
    assertEquals(expected, result);

    String child = "" + new Random().nextInt(100000000);
    new WriteFuture(ref.child(child), "testdata").timedGet();
    DataSnapshot snap = TestHelpers.getSnap(ref.child(child));
    assertEquals("testdata", snap.getValue());
  }

  @Test
  public void testNameForRootAndNonRootLocations() throws DatabaseException {
    DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference();
    assertNull(ref.getKey());
    assertEquals("a", ref.child("a").getKey());
    assertEquals("c", ref.child("b/c").getKey());
  }

  @Test
  public void testNameAndRefForSnapshots()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {

    DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference();
    // Clear any data there
    new WriteFuture(ref, MapBuilder.of("foo", 10)).timedGet();

    DataSnapshot snap = TestHelpers.getSnap(ref);
    assertNull(snap.getKey());
    assertEquals(ref.toString(), snap.getRef().toString());
    DataSnapshot childSnap = snap.child("a");
    assertEquals("a", childSnap.getKey());
    assertEquals(ref.child("a").toString(), childSnap.getRef().toString());
    childSnap = childSnap.child("b/c");
    assertEquals("c", childSnap.getKey());
    assertEquals(ref.child("a/b/c").toString(), childSnap.getRef().toString());
  }

  @Test
  public void testParentForRootAndNonRootLocations() throws DatabaseException {
    DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference();
    assertNull(ref.getParent());
    DatabaseReference child = ref.child("a");
    assertEquals(ref, child.getParent());
    child = ref.child("a/b/c");
    assertEquals(ref, child.getParent().getParent().getParent());
  }

  @Test
  public void testRootForRootAndNonRootLocations() throws DatabaseException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    ref = ref.getRoot();
    assertEquals(IntegrationTestUtils.getDatabaseUrl(), ref.toString());
    ref = ref.getRoot(); // Should be a no-op
    assertEquals(IntegrationTestUtils.getDatabaseUrl(), ref.toString());
  }

  // NOTE: skip test about child accepting numbers. Not applicable in a
  // type-safe language

  // NOTE: skip test on numeric keys. We throw an exception if they aren't
  // strings

  @Test
  public void testSetChildAndListenAtTheRoot()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference ref1 = refs.get(0);
    DatabaseReference ref2 = refs.get(1);

    new WriteFuture(ref1.child("foo"), "hi").timedGet();
    DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot();
    Map<String, Object> expected = MapBuilder.of("foo", "hi");
    TestHelpers.assertDeepEquals(expected, snap.getValue());
  }

  @Test
  public void testInvalidPaths() throws InterruptedException {
    List<String> badPaths = ImmutableList.of(".test", "test.", "fo$o", "[what", "ever]", "ha#sh");
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    DatabaseReference root = FirebaseDatabase.getInstance(masterApp).getReference();
    DataSnapshot snap = TestHelpers.getSnap(ref);

    for (String path : badPaths) {
      try {
        ref.child(path);
        fail("Should not be a valid path: " + path);
      } catch (DatabaseException e) {
        // No-op, expected
      }

      try {
        root.child(path);
        fail("Should not be a valid path: " + path);
      } catch (DatabaseException e) {
        // No-op, expected
      }

      try {
        root.child(IntegrationTestUtils.getDatabaseUrl() + "/tests/" + path);
        fail("Should not be a valid path: " + path);
      } catch (DatabaseException e) {
        // No-op, expected
      }

      try {
        snap.child(path);
        fail("Should not be a valid path: " + path);
      } catch (DatabaseException e) {
        // No-op, expected
      }

      try {
        snap.hasChild(path);
        fail("Should not be a valid path: " + path);
      } catch (DatabaseException e) {
        // No-op, expected
      }
    }
  }

  @Test
  public void testInvalidKeys() throws DatabaseException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    List<String> badKeys = ImmutableList.of(".test", "test.", "fo$o", "[what", "ever]", "ha#sh",
        "/thing", "thi/ing", "thing/", "");

    List<Object> badObjects = new ArrayList<>();
    for (String key : badKeys) {
      badObjects.add(MapBuilder.of(key, "test"));
      badObjects.add(MapBuilder.of("deeper", MapBuilder.of(key, "test")));
    }

    // Skipping 'push' portion, that api doesn't exist in Java client

    for (Object badObject : badObjects) {
      try {
        ref.setValueAsync(badObject);
        fail("Should not be a valid object: " + badObject);
      } catch (DatabaseException e) {
        // No-op, expected
      }

      try {
        ref.onDisconnect().setValueAsync(badObject);
        fail("Should not be a valid object: " + badObject);
      } catch (DatabaseException e) {
        // No-op, expected
      }

      // TODO: Skipping transaction portion for now
    }
  }

  @Test
  public void testInvalidUpdates() throws DatabaseException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    List<Map<String, Object>> badUpdates = ImmutableList.of(
        new MapBuilder().put("/", "t").put("a", "t").build(),
        new MapBuilder().put("a", "t").put("a/b", "t").build(),
        new MapBuilder().put("/a", "t").put("a/b", "t").build(),
        new MapBuilder().put("/a/b", "t").put("a", "t").build(),
        new MapBuilder().put("/a/b", "t").put("/a/b/.priority", 1.0).build(),
        new MapBuilder().put("/a/b/.sv", "timestamp").build(),
        new MapBuilder().put("/a/b/.value", "t").build(),
        new MapBuilder().put("/a/b/.priority", MapBuilder.of("x", "y")).build());
    for (Map<String, Object> badUpdate : badUpdates) {
      try {
        ref.updateChildrenAsync(badUpdate);
        fail("Should not be a valid update: " + badUpdate);
      } catch (DatabaseException e) {
        // No-op, expected
      }

      try {
        ref.onDisconnect().updateChildrenAsync(badUpdate);
        fail("Should not be a valid object: " + badUpdate);
      } catch (DatabaseException e) {
        // No-op, expected
      }
    }
  }

  @Test
  public void testAsciiControlCharacters() throws DatabaseException {
    DatabaseReference node = IntegrationTestUtils.getRandomNode(masterApp);
    // Test all controls characters PLUS 0x7F (127).
    for (int i = 0; i <= 32; i++) {
      String ch = new String(Character.toChars(i < 32 ? i : 127));
      Map<String, Object> obj = TestHelpers.buildObjFromPath(new Path(ch), "test_value");
      try {
        node.setValueAsync(obj);
        fail("Ascii control character should not be allowed in path.");
      } catch (DatabaseException e) {
        // expected
      }
    }
  }

  @Test
  public void invalidDoubleValues()
      throws DatabaseException, TestFailure, TimeoutException, InterruptedException {
    DatabaseReference node = IntegrationTestUtils.getRandomNode(masterApp);
    Object[] invalidValues =
        new Object[] {
            Double.NEGATIVE_INFINITY,
            Double.POSITIVE_INFINITY,
            Double.NaN,
            Float.NEGATIVE_INFINITY,
            Float.POSITIVE_INFINITY,
            Float.NaN
        };
    for (Object invalidValue : invalidValues) {
      try {
        node.setValueAsync(invalidValue);
        fail("NaN or Inf are not allowed as values.");
      } catch (DatabaseException expected) {
        assertEquals("Invalid value: Value cannot be NaN, Inf or -Inf.", expected.getMessage());
      }
    }
  }

  @Test
  @Ignore
  // TODO: Stop ignoring this test once JSON parsing has been fixed.
  public void testPathKeyLengthLimits() throws TestFailure, TimeoutException, InterruptedException {
    final int maxPathLengthBytes = 768;
    final int maxPathDepth = 32;
    final String fire = new String(Character.toChars(128293));
    final String base = new String(Character.toChars(26594));

    List<String> goodKeys = ImmutableList.of(
        TestHelpers.repeatedString("k", maxPathLengthBytes - 1),
        TestHelpers.repeatedString(fire, maxPathLengthBytes / 4 - 1),
        TestHelpers.repeatedString(base, maxPathLengthBytes / 3 - 1),
        TestHelpers.repeatedString("key/", maxPathDepth - 1) + "key");

    class BadGroup {

      private String expectedError;
      private List<String> keys;

      private BadGroup(String expectedError, List<String> keys) {
        this.expectedError = expectedError;
        this.keys = keys;
      }
    }

    List<BadGroup> badGroups = ImmutableList.of(
        new BadGroup("key path longer than 768 bytes",
            ImmutableList.of(TestHelpers.repeatedString("k", maxPathLengthBytes),
                TestHelpers.repeatedString(fire, maxPathLengthBytes / 4),
                TestHelpers.repeatedString(base, maxPathLengthBytes / 3),
                TestHelpers.repeatedString("j", maxPathLengthBytes / 2) + '/'
                    + TestHelpers.repeatedString("k", maxPathLengthBytes / 2))),
        new BadGroup("Path specified exceeds the maximum depth",
            Collections.singletonList(TestHelpers.repeatedString("key/", maxPathDepth) + "key")));

    DatabaseReference node = FirebaseDatabase.getInstance(masterApp).getReference();

    // Ensure "good keys" work from the root.
    for (String key : goodKeys) {
      Path path = new Path(key);
      Map<String, Object> obj = TestHelpers.buildObjFromPath(path, "test_value");
      node.setValueAsync(obj);
      ReadFuture future = ReadFuture.untilNonNull(node);
      assertEquals("test_value", TestHelpers.applyPath(future.waitForLastValue(), path));

      node.child(key).setValueAsync("another_value");
      future = ReadFuture.untilNonNull(node.child(key));
      assertEquals("another_value", future.waitForLastValue());

      node.updateChildrenAsync(obj);
      future = ReadFuture.untilNonNull(node);
      assertEquals("test_value", TestHelpers.applyPath(future.waitForLastValue(), path));
    }

    // Ensure "good keys" fail when created from child node (relative paths too
    // long).
    DatabaseReference nodeChild = IntegrationTestUtils.getRandomNode(masterApp);
    for (String key : goodKeys) {
      Map<String, Object> obj = TestHelpers.buildObjFromPath(new Path(key), "test_value");
      try {
        nodeChild.setValueAsync(obj);
        fail("Too-long path for setValue should throw exception.");
      } catch (DatabaseException e) {
        // expected
      }
      try {
        nodeChild.child(key).setValueAsync("another_value");
        fail("Too-long path before setValue should throw exception.");
      } catch (DatabaseException e) {
        // expected
      }
      try {
        nodeChild.updateChildrenAsync(obj);
        fail("Too-long path for updateChildren should throw exception.");
      } catch (DatabaseException e) {
        // expected
      }
      try {
        Map<String, Object> deepUpdate = MapBuilder.of(key, "test_value");
        nodeChild.updateChildrenAsync(deepUpdate);
        fail("Too-long path in deep update for updateChildren should throw exception.");
      } catch (DatabaseException e) {
        // expected
      }
    }

    for (BadGroup badGroup : badGroups) {
      for (String key : badGroup.keys) {
        Map<String, Object> obj = TestHelpers.buildObjFromPath(new Path(key), "test_value");
        try {
          node.setValueAsync(obj);
          fail("Expected setValueAsync(bad key) to throw exception: " + key);
        } catch (DatabaseException e) {
          TestHelpers.assertContains(e.getMessage(), badGroup.expectedError);
        }
        try {
          node.child(key).setValueAsync("another_value");
          fail("Expected child(\"" + key + "\").setValueAsync() to throw exception: " + key);
        } catch (DatabaseException e) {
          TestHelpers.assertContains(e.getMessage(), badGroup.expectedError);
        }
        try {
          node.updateChildrenAsync(obj);
          fail("Expected updateChildrenAsync(bad key) to throw exception: " + key);
        } catch (DatabaseException e) {
          TestHelpers.assertContains(e.getMessage(), badGroup.expectedError);
        }
        try {
          Map<String, Object> deepUpdate = MapBuilder.of(key, "test_value");
          node.updateChildrenAsync(deepUpdate);
          fail("Expected updateChildrean(bad deep update key) to throw exception: " + key);
        } catch (DatabaseException e) {
          TestHelpers.assertContains(e.getMessage(), badGroup.expectedError);
        }
        try {
          node.onDisconnect().setValueAsync(obj);
          fail("Expected onDisconnect.setValueAsync(bad key) to throw exception: " + key);
        } catch (DatabaseException e) {
          TestHelpers.assertContains(e.getMessage(), badGroup.expectedError);
        }
        try {
          node.onDisconnect().updateChildrenAsync(obj);
          fail("Expected onDisconnect.updateChildrenAsync(bad key) to throw exception: " + key);
        } catch (DatabaseException e) {
          TestHelpers.assertContains(e.getMessage(), badGroup.expectedError);
        }
        try {
          Map<String, Object> deepUpdate = MapBuilder.of(key, "test_value");
          node.onDisconnect().updateChildrenAsync(deepUpdate);
          fail("Expected onDisconnect.updateChildrenAsync(bad deep update key) to throw exception: "
              + key);
        } catch (DatabaseException e) {
          TestHelpers.assertContains(e.getMessage(), badGroup.expectedError);
        }
      }
    }
  }

  @Test
  public void testNamespaceCaseInsensitive()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {

    String child = "" + new Random().nextInt(100000000);
    DatabaseReference ref1 = FirebaseDatabase.getInstance(masterApp).getReference(child);

    String url = "https://" + IntegrationTestUtils.getProjectId().toUpperCase() + ".firebaseio.com/"
        + child;
    DatabaseReference ref2 = FirebaseDatabase.getInstance(masterApp).getReferenceFromUrl(url);

    new WriteFuture(ref1, "testdata").timedGet();
    DataSnapshot snap = TestHelpers.getSnap(ref2);
    assertEquals("testdata", snap.getValue());
  }

  @Test
  public void testNamespacesToStringCaseInsensitiveIn() throws DatabaseException {
    DatabaseReference ref1 = FirebaseDatabase.getInstance(masterApp).getReference();
    String url = "https://" + IntegrationTestUtils.getProjectId().toUpperCase() + ".firebaseio.com";
    DatabaseReference ref2 = FirebaseDatabase.getInstance(masterApp).getReferenceFromUrl(url);

    assertEquals(ref1.toString(), ref2.toString());
  }

  // NOTE: skipping test on re-entrant remove call. Not an issue w/ work queue
  // architecture

  @Test
  public void testSetNodeWithQuotedKey()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    Map<String, Object> expected = MapBuilder.of("\"herp\"", 1234L);
    new WriteFuture(ref, expected).timedGet();
    DataSnapshot snap = TestHelpers.getSnap(ref);
    TestHelpers.assertDeepEquals(expected, snap.getValue());
  }

  @Test
  public void testSetChildWithQuote()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    ReadFuture readFuture = new ReadFuture(ref);
    new WriteFuture(ref.child("\""), 1).timedGet();
    DataSnapshot snap = readFuture.timedGet().get(0).getSnapshot();
    assertEquals(1L, snap.child("\"").getValue());
  }

  @Test
  public void testEmptyChildrenGetValueEventBeforeParent() throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    EventHelper helper = new EventHelper().addValueExpectation(ref.child("a/aa/aaa"))
        .addValueExpectation(ref.child("a/aa")).addValueExpectation(ref.child("a"))
        .startListening();
    ref.setValueAsync(MapBuilder.of("b", 5));

    assertTrue(helper.waitForEvents());
    helper.cleanup();
  }

  // NOTE: skipping test for recursive sets. Java does not have reentrant
  // firebase api calls

  @Test
  public void testOnAfterSetWaitsForLatestData()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference ref1 = refs.get(0);
    DatabaseReference ref2 = refs.get(1);

    new WriteFuture(ref1, 5).timedGet();
    new WriteFuture(ref2, 42).timedGet();

    DataSnapshot snap = new ReadFuture(ref1).timedGet().get(0).getSnapshot();
    assertEquals(42L, snap.getValue());
  }

  @Test
  public void testOnceWaitsForLatestDataEachTime()
      throws InterruptedException, TestFailure, ExecutionException, TimeoutException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference ref1 = refs.get(0);
    DatabaseReference ref2 = refs.get(1);

    final Semaphore semaphore = new Semaphore(0);
    ref1.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        assertNull(snapshot.getValue());
        semaphore.release(1);
      }

      @Override
      public void onCancelled(DatabaseError error) {
        fail("Should not be cancelled");
      }
    });

    TestHelpers.waitFor(semaphore);

    new WriteFuture(ref2, 5).timedGet();
    ref1.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        assertEquals(5L, snapshot.getValue());
        semaphore.release(1);
      }

      @Override
      public void onCancelled(DatabaseError error) {
        fail("Should not be cancelled");
      }
    });

    TestHelpers.waitFor(semaphore);

    new WriteFuture(ref2, 42).timedGet();

    ref1.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        assertEquals(42L, snapshot.getValue());
        semaphore.release(1);
      }

      @Override
      public void onCancelled(DatabaseError error) {
        fail("Should not be cancelled");
      }
    });

    TestHelpers.waitFor(semaphore);
  }

  @Test
  public void testMemoryFreeingOnUnlistenDoesNotCorruptData()
      throws TestFailure, TimeoutException, InterruptedException, ExecutionException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference ref1 = refs.get(0);
    DatabaseReference ref2 = refs.get(1);

    final Semaphore semaphore = new Semaphore(0);
    final AtomicBoolean hasRun = new AtomicBoolean(false);
    ValueEventListener listener = ref1.addValueEventListener(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        if (hasRun.compareAndSet(false, true)) {
          assertNull(snapshot.getValue());
          semaphore.release(1);
        }
      }

      @Override
      public void onCancelled(DatabaseError error) {
        fail("Should not fail");
      }
    });

    TestHelpers.waitFor(semaphore);

    new WriteFuture(ref1, "test").timedGet();
    ref1.removeEventListener(listener);

    DatabaseReference other = IntegrationTestUtils.getRandomNode(masterApp);
    new WriteFuture(other, "hello").timedGet();
    ref1.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        assertEquals("test", snapshot.getValue());
        semaphore.release(1);
      }

      @Override
      public void onCancelled(DatabaseError error) {
        fail("Should not fail");
      }
    });

    TestHelpers.waitFor(semaphore);

    ref2.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        assertEquals("test", snapshot.getValue());
        semaphore.release(1);
      }

      @Override
      public void onCancelled(DatabaseError error) {
        fail("Should not fail");
      }
    });

    TestHelpers.waitFor(semaphore);
  }

  @Test
  public void testUpdateRaisesCorrectLocalEvents()
      throws InterruptedException, TestFailure, TimeoutException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    final ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref, 2);

    ref.setValueAsync(new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4).build());

    EventHelper helper = new EventHelper().addValueExpectation(ref.child("a"))
        .addValueExpectation(ref.child("d"))
        .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "a")
        .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "d").addValueExpectation(ref)
        .startListening(true);

    ref.updateChildrenAsync(new MapBuilder().put("a", 4).put("d", 1).build());
    helper.waitForEvents();
    List<EventRecord> events = readFuture.timedGet();
    helper.cleanup();

    Map<String, Object> expected = new MapBuilder().put("a", 4L).put("b", 2L).put("c", 3L)
        .put("d", 1L).build();
    Object result = events.get(events.size() - 1).getSnapshot().getValue();
    TestHelpers.assertDeepEquals(expected, result);
  }

  @Test
  public void testUpdateRaisesCorrectRemoteEvents()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);

    new WriteFuture(writer,
        new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4).build()).timedGet();

    EventHelper helper = new EventHelper().addValueExpectation(reader.child("a"))
        .addValueExpectation(reader.child("d"))
        .addChildExpectation(reader, Event.EventType.CHILD_CHANGED, "a")
        .addChildExpectation(reader, Event.EventType.CHILD_CHANGED, "d").addValueExpectation(reader)
        .startListening(true);

    writer.updateChildrenAsync(new MapBuilder().put("a", 4).put("d", 1).build());
    helper.waitForEvents();
    helper.cleanup();

    DataSnapshot snap = TestHelpers.getSnap(reader);
    Map<String, Object> expected = new MapBuilder().put("a", 4L).put("b", 2L).put("c", 3L)
        .put("d", 1L).build();
    Object result = snap.getValue();
    TestHelpers.assertDeepEquals(expected, result);
  }

  @Test
  public void testUpdateRaisesChildEventsOnNewListener() throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    EventHelper helper = new EventHelper().addValueExpectation(ref.child("a"))
        .addValueExpectation(ref.child("d"))
        .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "a")
        .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "d")
        .addValueExpectation(ref.child("c")).addValueExpectation(ref.child("d"))
        .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "c")
        .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "d").startListening();

    ref.updateChildrenAsync(new MapBuilder().put("a", 11).put("d", 44).build());
    ref.updateChildrenAsync(new MapBuilder().put("c", 33).put("d", 45).build());
    helper.waitForEvents();
    helper.cleanup();
  }

  @Test
  public void testUpdateAfterSetLeafNodeWorks() throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    final Semaphore semaphore = new Semaphore(0);
    final Map<String, Object> expected = new MapBuilder().put("a", 1L).put("b", 2L).build();

    ref.addValueEventListener(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        if (DeepEquals.deepEquals(snapshot.getValue(), expected)) {
          semaphore.release();
        }
      }

      @Override
      public void onCancelled(DatabaseError error) {
      }
    });
    ref.setValueAsync(42);
    ref.updateChildrenAsync(expected);

    TestHelpers.waitFor(semaphore);
  }

  @Test
  public void testUpdateChangesAreStoredCorrectlyInServer()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    final DatabaseReference writer = refs.get(0);
    final DatabaseReference reader = refs.get(1);

    new WriteFuture(writer,
        new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4).build()).timedGet();

    final Semaphore semaphore = new Semaphore(0);
    writer.updateChildren(MapBuilder.of("a", 42),
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            assertNull(error);
            semaphore.release(1);
          }
        });

    TestHelpers.waitFor(semaphore);

    DataSnapshot snap = TestHelpers.getSnap(reader);
    Map<String, Object> expected = new MapBuilder().put("a", 42L).put("b", 2L).put("c", 3L)
        .put("d", 4L).build();
    Object result = snap.getValue();
    TestHelpers.assertDeepEquals(expected, result);
  }

  @Test
  public void testUpdateAffectPriorityLocally()
      throws TestFailure, TimeoutException, InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref, 2);

    ref.setValueAsync(new MapBuilder().put("a", 1).put("b", 2).put("c", 3).build(), "testpri");
    ref.updateChildrenAsync(MapBuilder.of("a", 4));

    List<EventRecord> events = readFuture.timedGet();
    DataSnapshot snap = events.get(0).getSnapshot();
    assertEquals("testpri", snap.getPriority());

    snap = events.get(1).getSnapshot();
    assertEquals(4L, snap.child("a").getValue());
    assertEquals("testpri", snap.getPriority());
  }

  @Test
  public void testUpdateAffectPriorityRemotely()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference reader = refs.get(0);
    DatabaseReference writer = refs.get(1);

    new WriteFuture(writer, new MapBuilder().put("a", 1).put("b", 2).put("c", 3).build(), "testpri")
        .timedGet();

    DataSnapshot snap = TestHelpers.getSnap(reader);
    assertEquals("testpri", snap.getPriority());

    final Semaphore semaphore = new Semaphore(0);
    writer.updateChildren(MapBuilder.of("a", 4),
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            assertNull(error);
            semaphore.release(1);
          }
        });

    TestHelpers.waitFor(semaphore);
    snap = TestHelpers.getSnap(reader);
    assertEquals("testpri", snap.getPriority());
  }

  @Test
  public void testUpdateReplacesChildren() throws InterruptedException, TestFailure,
      TimeoutException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    final DatabaseReference writer = refs.get(0);
    final DatabaseReference reader = refs.get(1);

    final ReadFuture readFuture = ReadFuture.untilCountAfterNull(writer, 2);

    writer.setValueAsync(
        new MapBuilder().put("a", new MapBuilder().put("aa", 1).put("ab", 2).build()).build());
    final Semaphore semaphore = new Semaphore(0);
    Map<String, Object> expected = MapBuilder.of("a", MapBuilder.of("aa", 1L));
    writer.updateChildren(expected, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        assertNull(error);
        semaphore.release(1);
      }
    });

    TestHelpers.waitFor(semaphore);

    DataSnapshot snap = TestHelpers.getSnap(reader);
    TestHelpers.assertDeepEquals(expected, snap.getValue());

    snap = readFuture.timedGet().get(1).getSnapshot();
    TestHelpers.assertDeepEquals(expected, snap.getValue());
  }

  @Test
  public void testDeepUpdate() throws InterruptedException, TestFailure, TimeoutException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    final DatabaseReference writer = refs.get(0);
    final DatabaseReference reader = refs.get(1);

    final ReadFuture readFuture = ReadFuture.untilCount(writer, 2);

    writer.setValueAsync(
        new MapBuilder().put("a", new MapBuilder().put("aa", 1).put("ab", 2).build()).build());
    Map<String, Object> expected = new MapBuilder()
        .put("a", new MapBuilder().put("aa", 10L).put("ab", 20L).build()).build();
    Map<String, Object> update = new MapBuilder().put("a/aa", 10).put(".priority", 3.0)
        .put("a/ab", new MapBuilder().put(".priority", 2.0).put(".value", 20).build()).build();

    final Semaphore semaphore = new Semaphore(0);
    writer.updateChildren(update, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        assertNull(error);
        semaphore.release(1);
      }
    });

    TestHelpers.waitFor(semaphore);

    DataSnapshot snap = TestHelpers.getSnap(reader);
    TestHelpers.assertDeepEquals(expected, snap.getValue());
    assertEquals(3.0, snap.getPriority());

    snap = readFuture.timedGet().get(1).getSnapshot();
    TestHelpers.assertDeepEquals(expected, snap.getValue());
    assertEquals(3.0, snap.getPriority());

    snap = TestHelpers.getSnap(reader.child("a/ab"));
    assertEquals(2.0, snap.getPriority());
  }

  // NOTE: skipping test of update w/ scalar value. Disallowed by the type
  // system

  @Test
  public void testUpdateWithNoChanges() throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    final Semaphore semaphore = new Semaphore(0);
    ref.updateChildren(new HashMap<String, Object>(), new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        assertNull(error);
        semaphore.release(1);
      }
    });

    TestHelpers.waitFor(semaphore);
  }

  // TODO: consider implementing update stress test
  // NOTE: skipping update stress test for now

  @Test
  public void testUpdateFiresCorrectEventWhenDeletingChild()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);

    final ReadFuture writerFuture = ReadFuture.untilCountAfterNull(writer, 2);
    new WriteFuture(writer, new MapBuilder().put("a", 12).put("b", 6).build()).timedGet();
    final Semaphore semaphore = new Semaphore(0);
    final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        if (events.size() == 1) {
          semaphore.release();
        }
        return events.size() == 2;
      }
    });

    TestHelpers.waitFor(semaphore);

    writer.updateChildrenAsync(MapBuilder.of("a", null));
    DataSnapshot snap = writerFuture.timedGet().get(1).getSnapshot();

    Map<String, Object> expected = MapBuilder.of("b", 6L);
    TestHelpers.assertDeepEquals(expected, snap.getValue());

    snap = readerFuture.timedGet().get(1).getSnapshot();

    TestHelpers.assertDeepEquals(expected, snap.getValue());
  }

  @Test
  public void testUpdateFiresCorrectEventOnNewChildren()
      throws TestFailure, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);

    final Semaphore readerInitializedSemaphore = new Semaphore(0);
    final Semaphore writerInitializedSemaphore = new Semaphore(0);
    final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        if (events.size() == 1) {
          writerInitializedSemaphore.release();
        }
        return events.size() == 2;
      }
    });
    final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        if (events.size() == 1) {
          readerInitializedSemaphore.release();
        }
        return events.size() == 2;
      }
    });

    TestHelpers.waitFor(readerInitializedSemaphore);
    TestHelpers.waitFor(writerInitializedSemaphore);

    writer.updateChildrenAsync(MapBuilder.of("a", 42));
    DataSnapshot snap = writerFuture.timedGet().get(1).getSnapshot();

    Map<String, Object> expected = MapBuilder.of("a", 42L);
    TestHelpers.assertDeepEquals(expected, snap.getValue());

    snap = readerFuture.timedGet().get(1).getSnapshot();

    TestHelpers.assertDeepEquals(expected, snap.getValue());
  }

  @Test
  public void testUpdateFiresCorrectEventWhenAllChildrenAreDeleted()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);

    final ReadFuture writerFuture = ReadFuture.untilCountAfterNull(writer, 2);
    new WriteFuture(writer, MapBuilder.of("a", 12)).timedGet();
    final Semaphore semaphore = new Semaphore(0);
    final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        if (events.size() == 1) {
          semaphore.release();
        }
        return events.size() == 2;
      }
    });

    TestHelpers.waitFor(semaphore);

    writer.updateChildrenAsync(MapBuilder.of("a", null));
    DataSnapshot snap = writerFuture.timedGet().get(1).getSnapshot();

    assertNull(snap.getValue());

    snap = readerFuture.timedGet().get(1).getSnapshot();

    assertNull(snap.getValue());
  }

  @Test
  public void testUpdateFiresCorrectEventOnChangedChildren()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);

    final ReadFuture writerFuture = ReadFuture.untilCountAfterNull(writer, 2);
    new WriteFuture(writer, MapBuilder.of("a", 12)).timedGet();
    final Semaphore semaphore = new Semaphore(0);
    final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        if (events.size() == 1) {
          semaphore.release();
        }
        return events.size() == 2;
      }
    });

    TestHelpers.waitFor(semaphore);

    writer.updateChildrenAsync(MapBuilder.of("a", 11));
    DataSnapshot snap = writerFuture.timedGet().get(1).getSnapshot();

    Map<String, Object> expected = MapBuilder.of("a", 11L);
    TestHelpers.assertDeepEquals(expected, snap.getValue());

    snap = readerFuture.timedGet().get(1).getSnapshot();

    TestHelpers.assertDeepEquals(expected, snap.getValue());
  }

  @Test
  public void testPriorityUpdate() throws InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    final DatabaseReference writer = refs.get(0);
    final DatabaseReference reader = refs.get(1);

    Map<String, Object> writeValue = new MapBuilder().put("a", 5).put(".priority", "pri1").build();

    Map<String, Object> updateValue = new MapBuilder().put("a", 6).put(".priority", "pri2")
        .put("b", new MapBuilder().put(".priority", "pri3").put("c", 10).build()).build();

    final Semaphore semaphore = new Semaphore(0);
    writer.setValueAsync(writeValue);
    writer.updateChildren(updateValue, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        assertNull(error);
        semaphore.release(1);
      }
    });
    TestHelpers.waitFor(semaphore);

    DataSnapshot snap = TestHelpers.getSnap(reader);
    assertEquals(6L, snap.child("a").getValue());
    assertEquals("pri2", snap.getPriority());
    assertEquals("pri3", snap.child("b").getPriority());
    assertEquals(10L, snap.child("b/c").getValue());
  }

  // NOTE: skipping test for circular data structures. StackOverflowError is
  // thrown, they'll
  // see it.

  // NOTE: skipping test for creating a child name 'hasOwnProperty'

  // NOTE: skipping nesting tests. The Java api is async and doesn't do
  // reentrant callbacks

  @Test
  public void testParentDeleteShadowsChildListeners() throws InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference deleter = refs.get(1);

    String childName = writer.push().getKey();

    final AtomicBoolean initialized = new AtomicBoolean(false);
    final AtomicBoolean called = new AtomicBoolean(false);
    final Semaphore semaphore = new Semaphore(0);
    final ValueEventListener listener = deleter.child(childName)
        .addValueEventListener(new ValueEventListener() {
          @Override
          public void onDataChange(DataSnapshot snapshot) {
            Object val = snapshot.getValue();
            boolean inited = initialized.get();
            if (val == null && inited) {
              assertTrue(called.compareAndSet(false, true));
            } else if (!inited && val != null) {
              assertTrue(initialized.compareAndSet(false, true));
              semaphore.release(1);
            }
          }

          @Override
          public void onCancelled(DatabaseError error) {
          }
        });
    writer.child(childName).setValueAsync("foo");
    // Make sure we get the data in the listener before we delete it
    TestHelpers.waitFor(semaphore);

    deleter.removeValue(new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        semaphore.release(1);
      }
    });

    TestHelpers.waitFor(semaphore);
    assertTrue(called.get());
    deleter.child(childName).removeEventListener(listener);
  }

  @Test
  public void testParentDeleteShadowsNonDefaultChildListeners() throws InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference deleter = refs.get(1);

    String childName = writer.push().getKey();

    final AtomicBoolean queryCalled = new AtomicBoolean(false);
    final AtomicBoolean deepChildCalled = new AtomicBoolean(false);

    final ValueEventListener queryListener = deleter.child(childName).startAt(null, "b")
        .addValueEventListener(new ValueEventListener() {
          @Override
          public void onDataChange(DataSnapshot snapshot) {
            assertNull(snapshot.getValue());
            queryCalled.compareAndSet(false, true);
          }

          @Override
          public void onCancelled(DatabaseError error) {
            fail("Should not be cancelled");
          }
        });

    final ValueEventListener listener = deleter.child(childName).child("a")
        .addValueEventListener(new ValueEventListener() {
          @Override
          public void onDataChange(DataSnapshot snapshot) {
            assertNull(snapshot.getValue());
            deepChildCalled.compareAndSet(false, true);
          }

          @Override
          public void onCancelled(DatabaseError error) {
            fail("Should not be cancelled");
          }
        });

    writer.child(childName).setValueAsync("foo");
    final Semaphore semaphore = new Semaphore(0);
    deleter.removeValue(new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        semaphore.release(1);
      }
    });

    TestHelpers.waitFor(semaphore);
    assertTrue(queryCalled.get());
    assertTrue(deepChildCalled.get());
    deleter.child(childName).startAt(null, "b").removeEventListener(queryListener);
    deleter.child(childName).child("a").removeEventListener(listener);
  }

  @Test
  public void testServerValuesSetWithPriorityRemoteEvents()
      throws TestFailure, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);

    final Semaphore opSemaphore = new Semaphore(0);
    final Semaphore valSemaphore = new Semaphore(0);
    final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        valSemaphore.release();
        Object snap = events.get(events.size() - 1).getSnapshot().getValue();
        return snap != null;
      }
    });

    // Wait for initial (null) value.
    TestHelpers.waitFor(valSemaphore, 1);

    Map<String, Object> initialValues = new MapBuilder()
        .put("a", ServerValue.TIMESTAMP)
        .put("b", new MapBuilder()
            .put(".value", ServerValue.TIMESTAMP)
            .put(".priority", ServerValue.TIMESTAMP).build())
        .build();

    writer.setValue(initialValues, ServerValue.TIMESTAMP,
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            opSemaphore.release();
          }
        });
    TestHelpers.waitFor(opSemaphore);

    EventRecord readerEventRecord = readerFuture.timedGet().get(1);
    DataSnapshot snap = readerEventRecord.getSnapshot();
    assertEquals(snap.child("a").getValue().getClass(), Long.class);
    assertEquals(snap.getPriority().getClass(), Double.class);
    assertEquals(snap.getPriority(), snap.child("b").getPriority());
    assertEquals(snap.child("a").getValue(), snap.child("b").getValue());
    TestHelpers.assertTimeDelta(Long.parseLong(snap.child("a").getValue().toString()));
  }

  @Test
  public void testServerValuesSetPriorityRemoteEvents() throws InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);

    final Semaphore opSemaphore = new Semaphore(0);
    final Semaphore valSemaphore = new Semaphore(0);
    final AtomicLong priority = new AtomicLong(0);
    reader.addChildEventListener(new ChildEventListener() {
      @Override
      public void onChildAdded(DataSnapshot snapshot, String previousChildName) {
      }

      @Override
      public void onChildChanged(DataSnapshot snapshot, String previousChildName) {
      }

      @Override
      public void onChildRemoved(DataSnapshot snapshot) {
      }

      @Override
      public void onCancelled(DatabaseError error) {
      }

      @Override
      public void onChildMoved(DataSnapshot snapshot, String previousChildName) {
        priority.set(((Double) snapshot.getPriority()).longValue());
        valSemaphore.release();
      }
    });

    Map<String, Object> initialValues = new MapBuilder()
        .put("a", 1)
        .put("b", new MapBuilder()
            .put(".value", 1)
            .put(".priority", 1).build())
        .build();

    writer.setValue(initialValues, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        opSemaphore.release();
      }
    });
    TestHelpers.waitFor(opSemaphore);

    writer.child("a").setPriority(ServerValue.TIMESTAMP,
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            opSemaphore.release();
          }
        });
    TestHelpers.waitFor(opSemaphore);

    TestHelpers.waitFor(valSemaphore);
    TestHelpers.assertTimeDelta(priority.get());
  }

  @Test
  @Ignore
  public void testServerValuesUpdateRemoteEvents()
      throws TestFailure, TimeoutException, InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);

    final Semaphore opSemaphore = new Semaphore(0);
    final Semaphore valSemaphore = new Semaphore(0);
    final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        valSemaphore.release();
        return events.size() == 3;
      }
    });

    // Wait for initial (null) value.
    TestHelpers.waitFor(valSemaphore, 1);

    writer.child("a/b/d").setValue(1, ServerValue.TIMESTAMP,
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            opSemaphore.release();
          }
        });
    TestHelpers.waitFor(opSemaphore);

    Map<String, Object> updatedValue = new MapBuilder()
        .put("b", new MapBuilder()
            .put("c", ServerValue.TIMESTAMP)
            .put("d", ServerValue.TIMESTAMP).build())
        .build();

    writer.child("a").updateChildren(updatedValue, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        assertNull(error);
        opSemaphore.release();
      }
    });
    TestHelpers.waitFor(opSemaphore);

    EventRecord readerEventRecord = readerFuture.timedGet().get(2);
    DataSnapshot snap = readerEventRecord.getSnapshot();
    assertEquals(snap.child("a/b/c").getValue().getClass(), Long.class);
    assertEquals(snap.child("a/b/d").getValue().getClass(), Long.class);
    TestHelpers.assertTimeDelta(Long.parseLong(snap.child("a/b/c").getValue().toString()));
  }

  @Test
  public void testServerValuesSetWithPriorityLocalEvents()
      throws TestFailure, TimeoutException, InterruptedException {
    DatabaseReference writer = IntegrationTestUtils.getRandomNode(masterApp);

    final Semaphore opSemaphore = new Semaphore(0);
    final Semaphore valSemaphore = new Semaphore(0);
    final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        valSemaphore.release();
        Object snap = events.get(events.size() - 1).getSnapshot().getValue();
        return snap != null;
      }
    });

    // Wait for initial (null) value.
    TestHelpers.waitFor(valSemaphore, 1);

    Map<String, Object> initialValues = new MapBuilder()
        .put("a", ServerValue.TIMESTAMP)
        .put("b", new MapBuilder()
            .put(".value", ServerValue.TIMESTAMP)
            .put(".priority", ServerValue.TIMESTAMP).build())
        .build();

    writer.setValue(initialValues, ServerValue.TIMESTAMP,
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            opSemaphore.release();
          }
        });
    TestHelpers.waitFor(opSemaphore);

    EventRecord readerEventRecord = writerFuture.timedGet().get(1);
    DataSnapshot snap = readerEventRecord.getSnapshot();
    assertEquals(snap.child("a").getValue().getClass(), Long.class);
    assertEquals(snap.getPriority().getClass(), Double.class);
    assertEquals(snap.getPriority(), snap.child("b").getPriority());
    assertEquals(snap.child("a").getValue(), snap.child("b").getValue());
    TestHelpers.assertTimeDelta(Long.parseLong(snap.child("a").getValue().toString()));
  }

  @Test
  public void testServerValuesSetPriorityLocalEvents() throws InterruptedException {
    DatabaseReference writer = IntegrationTestUtils.getRandomNode(masterApp);

    final Semaphore opSemaphore = new Semaphore(0);
    final Semaphore valSemaphore = new Semaphore(0);
    final AtomicLong priority = new AtomicLong(0);
    writer.addChildEventListener(new ChildEventListener() {
      @Override
      public void onChildAdded(DataSnapshot snapshot, String previousChildName) {
      }

      @Override
      public void onChildChanged(DataSnapshot snapshot, String previousChildName) {
      }

      @Override
      public void onChildRemoved(DataSnapshot snapshot) {
      }

      @Override
      public void onCancelled(DatabaseError error) {
      }

      @Override
      public void onChildMoved(DataSnapshot snapshot, String previousChildName) {
        priority.set(((Double) snapshot.getPriority()).longValue());
        valSemaphore.release();
      }
    });

    Map<String, Object> initialValues = new MapBuilder()
        .put("a", 1)
        .put("b", new MapBuilder()
            .put(".value", 1)
            .put(".priority", 1).build())
        .build();

    writer.setValue(initialValues, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        opSemaphore.release();
      }
    });
    TestHelpers.waitFor(opSemaphore);

    writer.child("a").setPriority(ServerValue.TIMESTAMP,
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            opSemaphore.release();
          }
        });
    TestHelpers.waitFor(opSemaphore);

    TestHelpers.waitFor(valSemaphore);
    TestHelpers.assertTimeDelta(priority.get());
  }

  @Test
  public void testServerValuesUpdateLocalEvents()
      throws TestFailure, TimeoutException, InterruptedException {
    DatabaseReference writer = IntegrationTestUtils.getRandomNode(masterApp);

    final Semaphore opSemaphore = new Semaphore(0);
    final Semaphore valSemaphore = new Semaphore(0);
    final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        valSemaphore.release();
        return events.size() == 4;
      }
    });

    // Wait for initial (null) value.
    TestHelpers.waitFor(valSemaphore, 1);

    writer.child("a/b/d").setValue(1, ServerValue.TIMESTAMP,
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            opSemaphore.release();
          }
        });
    TestHelpers.waitFor(opSemaphore);

    Map<String, Object> updatedValue = new MapBuilder()
        .put("b", new MapBuilder()
            .put("c", ServerValue.TIMESTAMP)
            .put("d", ServerValue.TIMESTAMP).build())
        .build();

    writer.child("a").updateChildren(updatedValue, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        opSemaphore.release();
      }
    });
    TestHelpers.waitFor(opSemaphore);

    EventRecord readerEventRecord = writerFuture.timedGet().get(3);
    DataSnapshot snap = readerEventRecord.getSnapshot();
    assertEquals(snap.child("a/b/c").getValue().getClass(), Long.class);
    assertEquals(snap.child("a/b/d").getValue().getClass(), Long.class);
    TestHelpers.assertTimeDelta(Long.parseLong(snap.child("a/b/c").getValue().toString()));
  }

  @Test
  public void testServerValuesTransactionLocalEvents()
      throws TestFailure, TimeoutException, InterruptedException {
    DatabaseReference writer = IntegrationTestUtils.getRandomNode(masterApp);
    final Semaphore valSemaphore1 = new Semaphore(0);
    final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        valSemaphore1.release();
        return events.size() == 2;
      }
    });

    final Semaphore valSemaphore2 = new Semaphore(0);
    final ReadFuture readerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() {
      @Override
      public boolean isComplete(List<EventRecord> events) {
        valSemaphore2.release();
        return events.size() == 2;
      }
    });

    // Wait for local (null) events.
    TestHelpers.waitFor(valSemaphore1);
    TestHelpers.waitFor(valSemaphore2);

    writer.runTransaction(new Transaction.Handler() {
      @Override
      public Transaction.Result doTransaction(MutableData currentData) {
        try {
          currentData.setValue(ServerValue.TIMESTAMP);
        } catch (DatabaseException e) {
          fail("Should not fail");
        }
        return Transaction.success(currentData);
      }

      @Override
      public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
        if (error != null || !committed) {
          fail("Transaction should succeed");
        }
      }
    });

    // Wait for local events.
    TestHelpers.waitFor(valSemaphore1);
    TestHelpers.waitFor(valSemaphore2);

    EventRecord writerEventRecord = writerFuture.timedGet().get(1);
    DataSnapshot snap1 = writerEventRecord.getSnapshot();
    assertEquals(snap1.getValue().getClass(), Long.class);
    TestHelpers.assertTimeDelta(Long.parseLong(snap1.getValue().toString()));

    EventRecord readerEventRecord = readerFuture.timedGet().get(1);
    DataSnapshot snap2 = readerEventRecord.getSnapshot();
    assertEquals(snap2.getValue().getClass(), Long.class);
    TestHelpers.assertTimeDelta(Long.parseLong(snap2.getValue().toString()));
  }

  @Test
  public void testUpdateAfterChildSet() throws InterruptedException {
    final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    final Semaphore doneSemaphore = new Semaphore(0);

    Map<String, Object> initial = MapBuilder.of("a", "a");
    ref.setValue(initial, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        ref.addValueEventListener(new ValueEventListener() {
          @Override
          public void onDataChange(DataSnapshot snapshot) {
            Map res = (Map) snapshot.getValue();
            if (res != null && res.size() == 3 && res.containsKey("a") && res.containsKey("b")
                && res.containsKey("c")) {
              doneSemaphore.release();
            }
          }

          @Override
          public void onCancelled(DatabaseError error) {
          }
        });

        ref.child("b").setValueAsync("b");

        Map<String, Object> update = MapBuilder.of("c", "c");
        ref.updateChildrenAsync(update);
      }
    });

    TestHelpers.waitFor(doneSemaphore);
  }

  @Test
  public void testMaxFrameSize()
      throws InterruptedException, TestFailure, ExecutionException, TimeoutException {
    StringBuilder msg = new StringBuilder("");
    // This will generate a string well over max frame size
    for (int i = 0; i < 16384; ++i) {
      msg.append(String.valueOf(i));
    }

    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    new WriteFuture(ref, msg.toString()).timedGet();

    DataSnapshot snap = TestHelpers.getSnap(ref);
    assertEquals(msg.toString(), snap.getValue());
  }

  @Test
  public void testDeltaSyncNoDataUpdatesAfterReconnect() throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    // Create a fresh connection so we can be sure we won't get any other data
    // updates for stuff.    
    final DatabaseReference ref2 = FirebaseDatabase.getInstance(masterApp)
        .getReference(ref.getKey());

    final Map<String, Object> data = new MapBuilder().put("a", 1L).put("b", 2L)
        .put("c", new MapBuilder().put(".value", 3L).put(".priority", 3.0).build()).put("d", 4L)
        .build();

    final Semaphore gotData = new Semaphore(0);
    final AtomicReference<Map> result = new AtomicReference<>();
    ref.setValue(data, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        ref2.addValueEventListener(new ValueEventListener() {
          @Override
          public void onDataChange(DataSnapshot snapshot) {
            assertFalse(gotData.tryAcquire());
            result.compareAndSet(null, (Map) snapshot.getValue(true));
            gotData.release();
          }

          @Override
          public void onCancelled(DatabaseError error) {
          }
        });
      }
    });

    TestHelpers.waitFor(gotData);
    TestHelpers.assertDeepEquals(data, result.get());

    final Semaphore done = new Semaphore(0);

    // Bounce connection.
    DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp);
    RepoManager.interrupt(ctx);
    RepoManager.resume(ctx);

    ref2.getRoot().child(".info/connected").addValueEventListener(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        if ((Boolean) snapshot.getValue()) { 
          // We're connected. Do one more round-trip to make
          // sure all state restoration is done
          ref2.getRoot().child("foobar/empty/blah").setValue(null,
              new DatabaseReference.CompletionListener() {
                @Override
                public void onComplete(DatabaseError error, DatabaseReference ref) {
                  done.release();
                }
              });
        }
      }

      @Override
      public void onCancelled(DatabaseError error) {
      }
    });

    TestHelpers.waitFor(done);
  }

  @Test
  public void testDeltaSyncWithQueryNoDataUpdatesAfterReconnect() throws InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    // Create a fresh connection so we can be sure we won't get any other data
    // updates for stuff.    
    final DatabaseReference ref2 = FirebaseDatabase.getInstance(masterApp)
        .getReference(ref.getKey());

    final Map<String, Object> data = new MapBuilder()
        .put("a", 1L)
        .put("b", 2L)
        .put("c", new MapBuilder().put(".value", 3L).put(".priority", 3.0).build())
        .put("d", 4L)
        .build();

    final Map<String, Object> expected = new MapBuilder()
        .put("c", new MapBuilder().put(".value", 3L).put(".priority", 3.0).build())
        .put("d", 4L)
        .build();

    final Semaphore gotData = new Semaphore(0);

    final AtomicInteger updateCount = new AtomicInteger(0);
    final AtomicReference<Map> result = new AtomicReference<>();
    ref.setValue(data, new DatabaseReference.CompletionListener() {
      @Override
      public void onComplete(DatabaseError error, DatabaseReference ref) {
        ref2.limitToLast(2).addValueEventListener(new ValueEventListener() {
          @Override
          public void onDataChange(DataSnapshot snapshot) {
            assertFalse(gotData.tryAcquire());
            updateCount.incrementAndGet();
            result.compareAndSet(null, (Map) snapshot.getValue(true));
            gotData.release();
          }

          @Override
          public void onCancelled(DatabaseError error) {
          }
        });
      }
    });

    TestHelpers.waitFor(gotData);
    assertEquals(1, updateCount.get());
    TestHelpers.assertDeepEquals(expected, result.get());

    final Semaphore done = new Semaphore(0);

    // Bounce connection.
    DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp);
    RepoManager.interrupt(ctx);
    RepoManager.resume(ctx);

    ref2.getRoot().child(".info/connected").addValueEventListener(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        if ((Boolean) snapshot.getValue()) { 
          // We're connected. Do one more round-trip to make
          // sure all state restoration is // done.
          ref2.getRoot().child("foobar/empty/blah").setValue(null,
              new DatabaseReference.CompletionListener() {
                @Override
                public void onComplete(DatabaseError error, DatabaseReference ref) {
                  done.release();
                }
              });
        }
      }

      @Override
      public void onCancelled(DatabaseError error) {
      }
    });

    TestHelpers.waitFor(done);
  }

  @Test
  @Ignore
  public void testDeltaSyncWithUnfilteredQuery()
      throws InterruptedException, ExecutionException, TestFailure, TimeoutException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writeRef = refs.get(0);
    DatabaseReference readRef = refs.get(1);

    // List must be large enough to trigger delta sync.
    Map<String, Object> longList = new HashMap<>();
    for (long i = 0; i < 50; i++) {
      String key = writeRef.push().getKey();
      longList.put(key,
          new MapBuilder().put("order", i).put("text", "This is an awesome message!").build());
    }

    new WriteFuture(writeRef, longList).timedGet();

    // start listening.
    final List<DataSnapshot> readSnapshots = new ArrayList<>();
    final Semaphore readSemaphore = new Semaphore(0);
    readRef.orderByChild("order").addValueEventListener(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        readSnapshots.add(snapshot);
        readSemaphore.release();
      }

      @Override
      public void onCancelled(DatabaseError error) {
      }
    });

    TestHelpers.waitFor(readSemaphore);
    TestHelpers.assertDeepEquals(longList, readSnapshots.get(0).getValue());

    // Add a new child while readRef is offline.
    readRef.getDatabase().goOffline();

    DatabaseReference newChildRef = writeRef.push();
    Map<String, Object> newChild = new MapBuilder().put("order", 50L)
        .put("text", "This is a new child!").build();
    new WriteFuture(newChildRef, newChild).timedGet();
    longList.put(newChildRef.getKey(), newChild);

    // Go back online and make sure we get the new item.
    readRef.getDatabase().goOnline();
    TestHelpers.waitFor(readSemaphore);
    TestHelpers.assertDeepEquals(longList, readSnapshots.get(1).getValue());
  }

  @Test
  public void testNegativeIntegerKeys()
      throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    new WriteFuture(ref,
        new MapBuilder().put("-1", "minus-one").put("0", "zero").put("1", "one").build())
            .timedGet();

    DataSnapshot snap = TestHelpers.getSnap(ref);
    Map<String, Object> expected = new MapBuilder().put("-1", "minus-one").put("0", "zero")
        .put("1", "one").build();
    Object result = snap.getValue();
    TestHelpers.assertDeepEquals(expected, result);
  }

  @Test
  @Ignore
  public void testLocalServerValuesEventuallyButNotImmediatelyMatchServer()
      throws InterruptedException {
    List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
    DatabaseReference writer = refs.get(0);
    DatabaseReference reader = refs.get(1);

    final Semaphore completionSemaphore = new Semaphore(0);
    final List<DataSnapshot> readSnaps = new ArrayList<>();
    final List<DataSnapshot> writeSnaps = new ArrayList<>();

    reader.addValueEventListener(new ValueEventListener() {
      @Override
      public void onCancelled(DatabaseError error) {
      }

      @Override
      public void onDataChange(DataSnapshot snapshot) {
        if (snapshot.getValue() != null) {
          readSnaps.add(snapshot);
          completionSemaphore.release();
        }
      }
    });

    writer.addValueEventListener(new ValueEventListener() {
      @Override
      public void onCancelled(DatabaseError error) {
      }

      @Override
      public void onDataChange(DataSnapshot snapshot) {
        if (snapshot.getValue() != null) {
          if (snapshot.getValue() != null) {
            writeSnaps.add(snapshot);
            completionSemaphore.release();
          }
        }
      }
    });

    writer.setValueAsync(ServerValue.TIMESTAMP, ServerValue.TIMESTAMP);

    TestHelpers.waitFor(completionSemaphore, 3);

    assertEquals(1, readSnaps.size());
    assertEquals(2, writeSnaps.size());
    assertTrue(Math.abs(System.currentTimeMillis() - (Long) writeSnaps.get(0).getValue()) < 3000);
    assertTrue(
        Math.abs(System.currentTimeMillis() - (Double) writeSnaps.get(0).getPriority()) < 3000);
    assertTrue(Math.abs(System.currentTimeMillis() - (Long) writeSnaps.get(1).getValue()) < 3000);
    assertTrue(
        Math.abs(System.currentTimeMillis() - (Double) writeSnaps.get(1).getPriority()) < 3000);
    assertFalse(writeSnaps.get(0).getValue().equals(writeSnaps.get(1).getValue()));
    assertFalse(writeSnaps.get(0).getPriority().equals(writeSnaps.get(1).getPriority()));
    assertEquals(writeSnaps.get(1).getValue(), readSnaps.get(0).getValue());
    assertEquals(writeSnaps.get(1).getPriority(), readSnaps.get(0).getPriority());
  }

  @Test
  public void testBasicObjectMappingRoundTrip()
      throws InterruptedException, ExecutionException, TimeoutException, TestFailure {

    DumbBean bean = new DumbBean();
    bean.name = "bean";

    DumbBean nestedBean = new DumbBean();
    nestedBean.name = "nested-bean";
    bean.nestedBean = nestedBean;

    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    new WriteFuture(ref, bean).timedGet();

    final Semaphore done = new Semaphore(0);
    ref.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        DumbBean bean = snapshot.getValue(DumbBean.class);
        assertEquals("bean", bean.name);
        assertEquals("nested-bean", bean.nestedBean.name);
        assertNull(bean.nestedBean.nestedBean);
        done.release();
      }

      @Override
      public void onCancelled(DatabaseError error) {
      }
    });

    TestHelpers.waitFor(done);
  }

  @Test
  public void testUpdateChildrenWithObjectMapping() throws InterruptedException {

    DumbBean bean1 = new DumbBean();
    bean1.name = "bean1";

    DumbBean bean2 = new DumbBean();
    bean2.name = "bean2";

    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    final Semaphore writeComplete = new Semaphore(0);
    ref.updateChildren(new MapBuilder().put("bean1", bean1).put("bean2", bean2).build(),
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            writeComplete.release();
          }
        });
    TestHelpers.waitFor(writeComplete);

    final Semaphore done = new Semaphore(0);
    ref.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        Map<String, DumbBean> beans = snapshot
            .getValue(new GenericTypeIndicator<Map<String, DumbBean>>() {
            });
        assertEquals("bean1", beans.get("bean1").name);
        assertEquals("bean2", beans.get("bean2").name);
        done.release();
      }

      @Override
      public void onCancelled(DatabaseError error) {
      }
    });

    TestHelpers.waitFor(done);
  }

  @Test
  public void testUpdateChildrenDeepUpdatesWithObjectMapping() throws InterruptedException {

    DumbBean bean1 = new DumbBean();
    bean1.name = "bean1";

    DumbBean bean2 = new DumbBean();
    bean2.name = "bean2";

    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);

    final Semaphore writeComplete = new Semaphore(0);
    ref.updateChildren(new MapBuilder().put("bean1", bean1).put("deep/bean2", bean2).build(),
        new DatabaseReference.CompletionListener() {
          @Override
          public void onComplete(DatabaseError error, DatabaseReference ref) {
            writeComplete.release();
          }
        });
    TestHelpers.waitFor(writeComplete);

    final Semaphore done = new Semaphore(0);
    ref.addListenerForSingleValueEvent(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot snapshot) {
        assertEquals("bean1", snapshot.child("bean1").getValue(DumbBean.class).name);
        assertEquals("bean2", snapshot.child("deep/bean2").getValue(DumbBean.class).name);
        done.release();
      }

      @Override
      public void onCancelled(DatabaseError error) {
      }
    });

    TestHelpers.waitFor(done);
  }

  private static class DumbBean {
    public String name;
    public DumbBean nestedBean;
  }
}