// Copyright 2018 Google LLC
//
// 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.firestore;

import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollection;
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollectionWithDocs;
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testDocument;
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testDocumentWithData;
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.toDataMap;
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitForException;
import static com.google.firebase.firestore.testutil.TestUtil.map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
import java.util.Map;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public final class SourceTest {

  @After
  public void tearDown() {
    IntegrationTestUtil.tearDown();
  }

  @Test
  public void getDocumentWhileOnlineWithDefaultGetOptions() {
    Map<String, Object> initialData = map("key", "value");
    DocumentReference docRef = testDocumentWithData(initialData);

    Task<DocumentSnapshot> docTask = docRef.get();
    waitFor(docTask);

    DocumentSnapshot doc = docTask.getResult();
    assertTrue(doc.exists());
    assertFalse(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
    assertEquals(initialData, doc.getData());
  }

  @Test
  public void getCollectionWhileOnlineWithDefaultGetOptions() {
    Map<String, Map<String, Object>> initialDocs =
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2"),
            "doc3", map("key3", "value3"));
    CollectionReference colRef = testCollectionWithDocs(initialDocs);

    Task<QuerySnapshot> qrySnapTask = colRef.get();
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertFalse(qrySnap.getMetadata().isFromCache());
    assertFalse(qrySnap.getMetadata().hasPendingWrites());
    assertEquals(3, qrySnap.getDocumentChanges().size());
    assertEquals(initialDocs, toDataMap(qrySnap));
  }

  @Test
  public void getDocumentWhileOfflineWithDefaultGetOptions() {
    Map<String, Object> initialData = map("key", "value");
    DocumentReference docRef = testDocumentWithData(initialData);

    waitFor(docRef.get());
    waitFor(docRef.getFirestore().disableNetwork());

    Task<DocumentSnapshot> docTask = docRef.get();
    waitFor(docTask);
    DocumentSnapshot doc = docTask.getResult();

    assertTrue(doc.exists());
    assertTrue(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
    assertEquals(initialData, doc.getData());
  }

  @Test
  public void getCollectionWhileOfflineWithDefaultGetOptions() {
    Map<String, Map<String, Object>> initialDocs =
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2"),
            "doc3", map("key3", "value3"));
    CollectionReference colRef = testCollectionWithDocs(initialDocs);

    waitFor(colRef.get());
    waitFor(colRef.getFirestore().disableNetwork());

    // Since we're offline, the returned promises won't complete
    colRef.document("doc2").set(map("key2b", "value2b"), SetOptions.merge());
    colRef.document("doc3").set(map("key3b", "value3b"));
    colRef.document("doc4").set(map("key4", "value4"));

    Task<QuerySnapshot> qrySnapTask = colRef.get();
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.getMetadata().isFromCache());
    assertTrue(qrySnap.getMetadata().hasPendingWrites());
    assertEquals(4, qrySnap.getDocumentChanges().size());
    assertEquals(
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2", "key2b", "value2b"),
            "doc3", map("key3b", "value3b"),
            "doc4", map("key4", "value4")),
        toDataMap(qrySnap));
  }

  @Test
  public void getDocumentWhileOnlineWithSourceEqualToCache() {
    Map<String, Object> initialData = map("key", "value");
    DocumentReference docRef = testDocumentWithData(initialData);

    waitFor(docRef.get());
    Task<DocumentSnapshot> docTask = docRef.get(Source.CACHE);
    waitFor(docTask);
    DocumentSnapshot doc = docTask.getResult();

    assertTrue(doc.exists());
    assertTrue(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
    assertEquals(initialData, doc.getData());
  }

  @Test
  public void getCollectionWhileOnlineWithSourceEqualToCache() {
    Map<String, Map<String, Object>> initialDocs =
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2"),
            "doc3", map("key3", "value3"));
    CollectionReference colRef = testCollectionWithDocs(initialDocs);

    waitFor(colRef.get());
    Task<QuerySnapshot> qrySnapTask = colRef.get(Source.CACHE);
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.getMetadata().isFromCache());
    assertFalse(qrySnap.getMetadata().hasPendingWrites());
    assertEquals(3, qrySnap.getDocumentChanges().size());
    assertEquals(initialDocs, toDataMap(qrySnap));
  }

  @Test
  public void getDocumentWhileOfflineWithSourceEqualToCache() {
    Map<String, Object> initialData = map("key", "value");
    DocumentReference docRef = testDocumentWithData(initialData);

    waitFor(docRef.get());
    waitFor(docRef.getFirestore().disableNetwork());
    Task<DocumentSnapshot> docTask = docRef.get(Source.CACHE);
    waitFor(docTask);
    DocumentSnapshot doc = docTask.getResult();

    assertTrue(doc.exists());
    assertTrue(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
    assertEquals(initialData, doc.getData());
  }

  @Test
  public void getCollectionWhileOfflineWithSourceEqualToCache() {
    Map<String, Map<String, Object>> initialDocs =
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2"),
            "doc3", map("key3", "value3"));
    CollectionReference colRef = testCollectionWithDocs(initialDocs);

    waitFor(colRef.get());
    waitFor(colRef.getFirestore().disableNetwork());

    // Since we're offline, the returned promises won't complete
    colRef.document("doc2").set(map("key2b", "value2b"), SetOptions.merge());
    colRef.document("doc3").set(map("key3b", "value3b"));
    colRef.document("doc4").set(map("key4", "value4"));

    Task<QuerySnapshot> qrySnapTask = colRef.get(Source.CACHE);
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.getMetadata().isFromCache());
    assertTrue(qrySnap.getMetadata().hasPendingWrites());
    assertEquals(4, qrySnap.getDocumentChanges().size());
    assertEquals(
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2", "key2b", "value2b"),
            "doc3", map("key3b", "value3b"),
            "doc4", map("key4", "value4")),
        toDataMap(qrySnap));
  }

  @Test
  public void getDocumentWhileOnlineWithSourceEqualToServer() {
    Map<String, Object> initialData = map("key", "value");
    DocumentReference docRef = testDocumentWithData(initialData);

    Task<DocumentSnapshot> docTask = docRef.get(Source.SERVER);
    waitFor(docTask);

    DocumentSnapshot doc = docTask.getResult();
    assertTrue(doc.exists());
    assertFalse(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
    assertEquals(initialData, doc.getData());
  }

  @Test
  public void getCollectionWhileOnlineWithSourceEqualToServer() {
    Map<String, Map<String, Object>> initialDocs =
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2"),
            "doc3", map("key3", "value3"));
    CollectionReference colRef = testCollectionWithDocs(initialDocs);

    Task<QuerySnapshot> qrySnapTask = colRef.get(Source.SERVER);
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertFalse(qrySnap.getMetadata().isFromCache());
    assertFalse(qrySnap.getMetadata().hasPendingWrites());
    assertEquals(3, qrySnap.getDocumentChanges().size());
    assertEquals(initialDocs, toDataMap(qrySnap));
  }

  @Test
  public void getDocumentWhileOfflineWithSourceEqualToServer() {
    Map<String, Object> initialData = map("key", "value");
    DocumentReference docRef = testDocumentWithData(initialData);

    waitFor(docRef.get());
    waitFor(docRef.getFirestore().disableNetwork());

    Task<DocumentSnapshot> docTask = docRef.get(Source.SERVER);
    waitForException(docTask);
  }

  @Test
  public void getCollectionWhileOfflineWithSourceEqualToServer() {
    Map<String, Map<String, Object>> initialDocs =
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2"),
            "doc3", map("key3", "value3"));
    CollectionReference colRef = testCollectionWithDocs(initialDocs);

    waitFor(colRef.get());
    waitFor(colRef.getFirestore().disableNetwork());

    Task<QuerySnapshot> qrySnapTask = colRef.get(Source.SERVER);
    waitForException(qrySnapTask);
  }

  @Test
  public void getDocumentWhileOfflineWithDifferentGetOptions() {
    Map<String, Object> initialData = map("key", "value");
    DocumentReference docRef = testDocumentWithData(initialData);

    waitFor(docRef.get());
    waitFor(docRef.getFirestore().disableNetwork());

    // Create an initial listener for this query (to attempt to disrupt the gets below) and wait for
    // the listener to deliver its initial snapshot before continuing.
    TaskCompletionSource<Void> source = new TaskCompletionSource<>();
    docRef.addSnapshotListener(
        (docSnap, error) -> {
          if (error != null) {
            source.setException(error);
          } else {
            source.setResult(null);
          }
        });
    waitFor(source.getTask());

    Task<DocumentSnapshot> docTask = docRef.get(Source.CACHE);
    waitFor(docTask);
    DocumentSnapshot doc = docTask.getResult();
    assertTrue(doc.exists());
    assertTrue(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
    assertEquals(initialData, doc.getData());

    docTask = docRef.get();
    waitFor(docTask);
    doc = docTask.getResult();
    assertTrue(doc.exists());
    assertTrue(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
    assertEquals(initialData, doc.getData());

    docTask = docRef.get(Source.SERVER);
    waitForException(docTask);
  }

  @Test
  public void getCollectionWhileOfflineWithDifferentGetOptions() {
    Map<String, Map<String, Object>> initialDocs =
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2"),
            "doc3", map("key3", "value3"));
    CollectionReference colRef = testCollectionWithDocs(initialDocs);

    waitFor(colRef.get());
    waitFor(colRef.getFirestore().disableNetwork());

    // since we're offline, the returned promises won't complete
    colRef.document("doc2").set(map("key2b", "value2b"), SetOptions.merge());
    colRef.document("doc3").set(map("key3b", "value3b"));
    colRef.document("doc4").set(map("key4", "value4"));

    // Create an initial listener for this query (to attempt to disrupt the gets below) and wait for
    // the listener to deliver its initial snapshot before continuing.
    TaskCompletionSource<Void> source = new TaskCompletionSource<>();
    colRef.addSnapshotListener(
        (qrySnap, error) -> {
          if (error != null) {
            source.setException(error);
          } else {
            source.setResult(null);
          }
        });
    waitFor(source.getTask());

    Task<QuerySnapshot> qrySnapTask = colRef.get(Source.CACHE);
    waitFor(qrySnapTask);
    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.getMetadata().isFromCache());
    assertTrue(qrySnap.getMetadata().hasPendingWrites());
    assertEquals(4, qrySnap.getDocumentChanges().size());
    assertEquals(
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2", "key2b", "value2b"),
            "doc3", map("key3b", "value3b"),
            "doc4", map("key4", "value4")),
        toDataMap(qrySnap));

    qrySnapTask = colRef.get();
    waitFor(qrySnapTask);
    qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.getMetadata().isFromCache());
    assertTrue(qrySnap.getMetadata().hasPendingWrites());
    assertEquals(4, qrySnap.getDocumentChanges().size());
    assertEquals(
        map(
            "doc1", map("key1", "value1"),
            "doc2", map("key2", "value2", "key2b", "value2b"),
            "doc3", map("key3b", "value3b"),
            "doc4", map("key4", "value4")),
        toDataMap(qrySnap));

    qrySnapTask = colRef.get(Source.SERVER);
    waitForException(qrySnapTask);
  }

  @Test
  public void getNonExistingDocWhileOnlineWithDefaultGetOptions() {
    DocumentReference docRef = testDocument();

    Task<DocumentSnapshot> docTask = docRef.get();
    waitFor(docTask);

    DocumentSnapshot doc = docTask.getResult();
    assertFalse(doc.exists());
    assertFalse(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
  }

  @Test
  public void getNonExistingCollectionWhileOnlineWithDefaultGetOptions() {
    CollectionReference colRef = testCollection();

    Task<QuerySnapshot> qrySnapTask = colRef.get();
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.isEmpty());
    assertEquals(0, qrySnap.getDocumentChanges().size());
    assertFalse(qrySnap.getMetadata().isFromCache());
    assertFalse(qrySnap.getMetadata().hasPendingWrites());
  }

  @Test
  public void getNonExistingDocWhileOfflineWithDefaultGetOptions() {
    DocumentReference docRef = testDocument();

    waitFor(docRef.getFirestore().disableNetwork());
    Task<DocumentSnapshot> docTask = docRef.get();
    waitForException(docTask);
  }

  // TODO(b/112267729): We should raise a fromCache=true event with a
  // nonexistent snapshot, but because the default source goes through a normal
  // listener, we do not.
  @Test
  @Ignore
  public void getDeletedDocWhileOfflineWithDefaultGetOptions() {
    DocumentReference docRef = testDocument();
    waitFor(docRef.delete());

    waitFor(docRef.getFirestore().disableNetwork());
    Task<DocumentSnapshot> docTask = docRef.get();
    waitFor(docTask);

    DocumentSnapshot doc = docTask.getResult();
    assertFalse(doc.exists());
    assertNull(doc.getData());
    assertTrue(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
  }

  @Test
  public void getNonExistingCollectionWhileOfflineWithDefaultGetOptions() {
    CollectionReference colRef = testCollection();

    waitFor(colRef.getFirestore().disableNetwork());
    Task<QuerySnapshot> qrySnapTask = colRef.get();
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.isEmpty());
    assertEquals(0, qrySnap.getDocumentChanges().size());
    assertTrue(qrySnap.getMetadata().isFromCache());
    assertFalse(qrySnap.getMetadata().hasPendingWrites());
  }

  @Test
  public void getNonExistingDocWhileOnlineWithSourceEqualToCache() {
    DocumentReference docRef = testDocument();

    // Attempt to get doc. This will fail since there's nothing in cache.
    Task<DocumentSnapshot> docTask = docRef.get(Source.CACHE);
    waitForException(docTask);
  }

  @Test
  public void getNonExistingCollectionWhileOnlineWithSourceEqualToCache() {
    CollectionReference colRef = testCollection();

    Task<QuerySnapshot> qrySnapTask = colRef.get(Source.CACHE);
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.isEmpty());
    assertEquals(0, qrySnap.getDocumentChanges().size());
    assertTrue(qrySnap.getMetadata().isFromCache());
    assertFalse(qrySnap.getMetadata().hasPendingWrites());
  }

  @Test
  public void getNonExistingDocWhileOfflineWithSourceEqualToCache() {
    DocumentReference docRef = testDocument();

    waitFor(docRef.getFirestore().disableNetwork());
    // Attempt to get doc. This will fail since there's nothing in cache.
    Task<DocumentSnapshot> docTask = docRef.get(Source.CACHE);
    waitForException(docTask);
  }

  @Test
  public void getDeletedDocWhileOfflineWithSourceEqualToCache() {
    DocumentReference docRef = testDocument();
    waitFor(docRef.delete());

    waitFor(docRef.getFirestore().disableNetwork());
    Task<DocumentSnapshot> docTask = docRef.get(Source.CACHE);
    waitFor(docTask);

    DocumentSnapshot doc = docTask.getResult();
    assertFalse(doc.exists());
    assertNull(doc.getData());
    assertTrue(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
  }

  @Test
  public void getNonExistingCollectionWhileOfflineWithSourceEqualToCache() {
    CollectionReference colRef = testCollection();

    waitFor(colRef.getFirestore().disableNetwork());
    Task<QuerySnapshot> qrySnapTask = colRef.get(Source.CACHE);
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.isEmpty());
    assertEquals(0, qrySnap.getDocumentChanges().size());
    assertTrue(qrySnap.getMetadata().isFromCache());
    assertFalse(qrySnap.getMetadata().hasPendingWrites());
  }

  @Test
  public void getNonExistingDocWhileOnlineWithSourceEqualToServer() {
    DocumentReference docRef = testDocument();

    Task<DocumentSnapshot> docTask = docRef.get(Source.SERVER);
    waitFor(docTask);

    DocumentSnapshot doc = docTask.getResult();
    assertFalse(doc.exists());
    assertFalse(doc.getMetadata().isFromCache());
    assertFalse(doc.getMetadata().hasPendingWrites());
  }

  @Test
  public void getNonExistingCollectionWhileOnlineWithSourceEqualToServer() {
    CollectionReference colRef = testCollection();

    Task<QuerySnapshot> qrySnapTask = colRef.get(Source.SERVER);
    waitFor(qrySnapTask);

    QuerySnapshot qrySnap = qrySnapTask.getResult();
    assertTrue(qrySnap.isEmpty());
    assertEquals(0, qrySnap.getDocumentChanges().size());
    assertFalse(qrySnap.getMetadata().isFromCache());
    assertFalse(qrySnap.getMetadata().hasPendingWrites());
  }

  @Test
  public void getNonExistingDocWhileOfflineWithSourceEqualToServer() {
    DocumentReference docRef = testDocument();

    waitFor(docRef.getFirestore().disableNetwork());
    Task<DocumentSnapshot> docTask = docRef.get(Source.SERVER);
    waitForException(docTask);
  }

  @Test
  public void getNonExistingCollectionWhileOfflineWithSourceEqualToServer() {
    CollectionReference colRef = testCollection();

    waitFor(colRef.getFirestore().disableNetwork());
    Task<QuerySnapshot> qrySnapTask = colRef.get(Source.SERVER);
    waitForException(qrySnapTask);
  }
}