/*
 * 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.core.persistence;

import static org.junit.Assert.assertEquals;

import com.google.common.collect.ImmutableMap;
import com.google.firebase.FirebaseApp;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.EventRecord;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.Query;
import com.google.firebase.database.TestHelpers;
import com.google.firebase.database.future.ReadFuture;
import com.google.firebase.database.future.WriteFuture;
import com.google.firebase.testing.IntegrationTestUtils;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class KeepSyncedTestIT {

  private static long globalKeepSyncedTestCounter = 0;
  private static FirebaseApp masterApp;

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

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

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

  @Test
  public void testKeepSynced() throws Exception {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    ref.keepSynced(true);
    assertIsKeptSynced(ref);

    ref.keepSynced(false);
    assertNotKeptSynced(ref);
  }

  // NOTE: This is not ideal behavior and should be fixed in a future release
  @Test
  public void testKeepSyncedAffectOnQueries() throws Exception {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    ref.keepSynced(true);
    Query query = ref.limitToFirst(5);
    query.keepSynced(true);
    assertIsKeptSynced(ref);

    ref.keepSynced(false);
    assertNotKeptSynced(ref);
    // currently, setting false on the default query affects all queries at that location
    assertNotKeptSynced(query);
  }

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

    try {
      ref.keepSynced(true);
      ref.keepSynced(true);
      ref.keepSynced(true);
      assertIsKeptSynced(ref);

      // If it were balanced, this would not be enough
      ref.keepSynced(false);
      ref.keepSynced(false);
      assertNotKeptSynced(ref);

      // If it were balanced, this would not be enough
      ref.keepSynced(true);
      assertIsKeptSynced(ref);
    } finally {
      // cleanup
      ref.keepSynced(false);
    }
  }

  // NOTE: No RemoveAllListenersDoesNotAffectKeepSynced test, since JVM client doesn't have
  // removeAllListeners...

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

    ref.keepSynced(true);
    try {
      assertIsKeptSynced(ref);

      // This will add and remove a listener.
      new ReadFuture(
          ref,
          new ReadFuture.CompletionCondition() {
            @Override
            public boolean isComplete(List<EventRecord> events) {
              return true;
            }
          })
          .timedGet();

      assertIsKeptSynced(ref);
    } finally {
      // cleanup
      ref.keepSynced(false);
    }
  }

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

    ReadFuture readFuture;
    ref.keepSynced(true);
    try {
      assertIsKeptSynced(ref);

      readFuture =
          new ReadFuture(ref, new ReadFuture.CompletionCondition() {
              @Override
              public boolean isComplete(List<EventRecord> events) {
                return events.get(events.size() - 1).getSnapshot().getValue().equals("done");
              }
            });
    } finally {
      // cleanup
      ref.keepSynced(false);
    }

    // Should trigger our listener.
    ref.setValueAsync("done");
    readFuture.timedGet();
  }

  @Test
  public void testDifferentIndependentQueries() throws Exception {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    Query query1 = ref.limitToFirst(1);
    Query query2 = ref.limitToFirst(2);

    query1.keepSynced(true);
    assertIsKeptSynced(query1);
    assertNotKeptSynced(query2);

    query2.keepSynced(true);
    assertIsKeptSynced(query1);
    assertIsKeptSynced(query2);

    query1.keepSynced(false);
    assertIsKeptSynced(query2);
    assertNotKeptSynced(query1);

    query2.keepSynced(false);
    assertNotKeptSynced(query1);
    assertNotKeptSynced(query2);
  }

  @Test
  public void testKeptSyncedChild() throws Exception {
    DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
    DatabaseReference child = ref.child("random-child");

    ref.keepSynced(true);
    try {
      assertIsKeptSynced(child);
    } finally {
      // cleanup
      ref.keepSynced(false);
    }
  }

  @Test
  public void testKeptSyncedRoot() throws Exception {
    DatabaseReference ref = FirebaseDatabase.getInstance().getReference();

    ref.keepSynced(true);
    try {
      assertIsKeptSynced(ref);
    } finally {
      // cleanup
      ref.keepSynced(false);
    }
  }

  private void assertIsKeptSynced(Query query) throws Exception {
    DatabaseReference ref = query.getRef();

    // First set a unique value to the value of a child.
    long counter = globalKeepSyncedTestCounter++;
    final Map<String, Object> value = ImmutableMap.<String, Object>of("child", counter);
    new WriteFuture(ref, value).timedGet();

    // Next go offline, if it's kept synced we should have kept the value.
    // After going offline no way to get the value except from cache.
    ref.getDatabase().goOffline();

    try {
      new ReadFuture(
          query,
          new ReadFuture.CompletionCondition() {
            @Override
            public boolean isComplete(List<EventRecord> events) {
              assertEquals(1, events.size());
              assertEquals(value, events.get(0).getSnapshot().getValue());
              return true;
            }
          })
          .timedGet();
    } finally {
      // All good, go back online
      ref.getDatabase().goOnline();
    }
  }

  private void assertNotKeptSynced(Query query) throws Exception {
    DatabaseReference ref = query.getRef();

    // First set a unique value to the value of a child.
    long current = globalKeepSyncedTestCounter++;
    final Map<String, Object> oldValue = ImmutableMap.<String, Object>of("child", current);

    long next = globalKeepSyncedTestCounter++;
    final Map<String, Object> nextValue = ImmutableMap.<String, Object>of("child", next);

    new WriteFuture(ref, oldValue).timedGet();

    // Next go offline, if it's kept synced we should have kept the value and we'll get an even
    // with the *old* value.
    ref.getDatabase().goOffline();

    try {
      ReadFuture readFuture =
          new ReadFuture(query, new ReadFuture.CompletionCondition() {
              @Override
              public boolean isComplete(List<EventRecord> events) {
                // We expect this to get called with the next value, not the old value.
                assertEquals(1, events.size());
                assertEquals(nextValue, events.get(0).getSnapshot().getValue());
                return true;
              }
            });

      // By now, if we had it synced we should have gotten an event with the wrong value
      // Write a new value so the value event listener will be triggered
      ref.setValueAsync(nextValue);
      readFuture.timedGet();
    } finally {
      // All good, go back online
      ref.getDatabase().goOnline();
    }
  }

  // TODO[offline]: Cancel listens for keep synced....
}