/**
 * @license
 * 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.
 */

import { expect } from 'chai';

import {
  deleteDoc,
  disableNetwork,
  doc,
  getDoc,
  getDocFromCache,
  getDocFromServer,
  getDocs,
  onSnapshot,
  setDoc,
  getDocsFromCache,
  getDocsFromServer
} from '../util/firebase_export';
import {
  toDataMap,
  apiDescribe,
  withTestCollection,
  withTestDocAndInitialData
} from '../util/helpers';

apiDescribe('GetOptions', (persistence: boolean) => {
  it('get document while online with default get options', () => {
    const initialData = { key: 'value' };
    return withTestDocAndInitialData(persistence, initialData, docRef => {
      return getDoc(docRef).then(doc => {
        expect(doc.exists()).to.be.true;
        expect(doc.metadata.fromCache).to.be.false;
        expect(doc.metadata.hasPendingWrites).to.be.false;
        expect(doc.data()).to.deep.equal(initialData);
      });
    });
  });

  it('get collection while online with default get options', () => {
    const initialDocs = {
      doc1: { key1: 'value1' },
      doc2: { key2: 'value2' },
      doc3: { key3: 'value3' }
    };
    return withTestCollection(persistence, initialDocs, colRef => {
      return getDocs(colRef).then(qrySnap => {
        expect(qrySnap.metadata.fromCache).to.be.false;
        expect(qrySnap.metadata.hasPendingWrites).to.be.false;
        expect(qrySnap.docChanges().length).to.equal(3);
        expect(toDataMap(qrySnap)).to.deep.equal(initialDocs);
      });
    });
  });

  it('get document while offline with default get options', () => {
    const initialData = { key: 'value' };
    return withTestDocAndInitialData(persistence, initialData, (docRef, db) => {
      // Register a snapshot to force the data to stay in the cache and not be
      // garbage collected.
      onSnapshot(docRef, () => {});
      return getDoc(docRef)
        .then(() => disableNetwork(db))
        .then(() => getDoc(docRef))
        .then(doc => {
          expect(doc.exists()).to.be.true;
          expect(doc.metadata.fromCache).to.be.true;
          expect(doc.metadata.hasPendingWrites).to.be.false;
          expect(doc.data()).to.deep.equal(initialData);
        });
    });
  });

  it('get collection while offline with default get options', () => {
    const initialDocs = {
      doc1: { key1: 'value1' },
      doc2: { key2: 'value2' },
      doc3: { key3: 'value3' }
    };
    return withTestCollection(persistence, initialDocs, (colRef, db) => {
      // Register a snapshot to force the data to stay in the cache and not be
      // garbage collected.
      onSnapshot(colRef, () => {});
      return getDocs(colRef)
        .then(() => disableNetwork(db))
        .then(() => {
          // NB: since we're offline, the returned promises won't complete
          /* eslint-disable @typescript-eslint/no-floating-promises */
          setDoc(doc(colRef, 'doc2'), { key2b: 'value2b' }, { merge: true });
          setDoc(doc(colRef, 'doc3'), { key3b: 'value3b' });
          setDoc(doc(colRef, 'doc4'), { key4: 'value4' });
          /* eslint-enable @typescript-eslint/no-floating-promises */
          return getDocs(colRef);
        })
        .then(qrySnap => {
          expect(qrySnap.metadata.fromCache).to.be.true;
          expect(qrySnap.metadata.hasPendingWrites).to.be.true;
          const docsData = toDataMap(qrySnap);
          expect(qrySnap.docChanges().length).to.equal(4);
          expect(docsData).to.deep.equal({
            doc1: { key1: 'value1' },
            doc2: { key2: 'value2', key2b: 'value2b' },
            doc3: { key3b: 'value3b' },
            doc4: { key4: 'value4' }
          });
        });
    });
  });

  it('get document while online with source=cache', () => {
    const initialData = { key: 'value' };
    return withTestDocAndInitialData(persistence, initialData, docRef => {
      // Register a snapshot to force the data to stay in the cache and not be
      // garbage collected.
      onSnapshot(docRef, () => {});
      return getDoc(docRef)
        .then(() => getDocFromCache(docRef))
        .then(doc => {
          expect(doc.exists()).to.be.true;
          expect(doc.metadata.fromCache).to.be.true;
          expect(doc.metadata.hasPendingWrites).to.be.false;
          expect(doc.data()).to.deep.equal(initialData);
        });
    });
  });

  it('get collection while online with source=cache', () => {
    const initialDocs = {
      doc1: { key1: 'value1' },
      doc2: { key2: 'value2' },
      doc3: { key3: 'value3' }
    };
    return withTestCollection(persistence, initialDocs, colRef => {
      // Register a snapshot to force the data to stay in the cache and not be
      // garbage collected.
      onSnapshot(colRef, () => {});
      return getDocs(colRef)
        .then(() => getDocsFromCache(colRef))
        .then(qrySnap => {
          expect(qrySnap.metadata.fromCache).to.be.true;
          expect(qrySnap.metadata.hasPendingWrites).to.be.false;
          expect(qrySnap.docChanges().length).to.equal(3);
          expect(toDataMap(qrySnap)).to.deep.equal(initialDocs);
        });
    });
  });

  it('get document while offline with source=cache', () => {
    const initialData = { key: 'value' };

    return withTestDocAndInitialData(persistence, initialData, (docRef, db) => {
      // Register a snapshot to force the data to stay in the cache and not be
      // garbage collected.
      onSnapshot(docRef, () => {});
      return getDoc(docRef)
        .then(() => disableNetwork(db))
        .then(() => getDocFromCache(docRef))
        .then(doc => {
          expect(doc.exists()).to.be.true;
          expect(doc.metadata.fromCache).to.be.true;
          expect(doc.metadata.hasPendingWrites).to.be.false;
          expect(doc.data()).to.deep.equal(initialData);
        });
    });
  });

  it('get collection while offline with source=cache', () => {
    const initialDocs = {
      doc1: { key1: 'value1' },
      doc2: { key2: 'value2' },
      doc3: { key3: 'value3' }
    };
    return withTestCollection(persistence, initialDocs, (colRef, db) => {
      // Register a snapshot to force the data to stay in the cache and not be
      // garbage collected.
      onSnapshot(colRef, () => {});
      return getDocs(colRef)
        .then(() => disableNetwork(db))
        .then(() => {
          // NB: since we're offline, the returned promises won't complete
          /* eslint-disable @typescript-eslint/no-floating-promises */
          setDoc(doc(colRef, 'doc2'), { key2b: 'value2b' }, { merge: true });
          setDoc(doc(colRef, 'doc3'), { key3b: 'value3b' });
          setDoc(doc(colRef, 'doc4'), { key4: 'value4' });
          /* eslint-enable @typescript-eslint/no-floating-promises */

          return getDocsFromCache(colRef);
        })
        .then(qrySnap => {
          expect(qrySnap.metadata.fromCache).to.be.true;
          expect(qrySnap.metadata.hasPendingWrites).to.be.true;
          const docsData = toDataMap(qrySnap);
          expect(qrySnap.docChanges().length).to.equal(4);
          expect(docsData).to.deep.equal({
            doc1: { key1: 'value1' },
            doc2: { key2: 'value2', key2b: 'value2b' },
            doc3: { key3b: 'value3b' },
            doc4: { key4: 'value4' }
          });
        });
    });
  });

  it('get document while online with source=server', () => {
    const initialData = { key: 'value' };
    return withTestDocAndInitialData(persistence, initialData, docRef => {
      return getDocFromServer(docRef).then(doc => {
        expect(doc.exists()).to.be.true;
        expect(doc.metadata.fromCache).to.be.false;
        expect(doc.metadata.hasPendingWrites).to.be.false;
        expect(doc.data()).to.deep.equal(initialData);
      });
    });
  });

  it('get collection while online with source=server', () => {
    const initialDocs = {
      doc1: { key1: 'value1' },
      doc2: { key2: 'value2' },
      doc3: { key3: 'value3' }
    };
    return withTestCollection(persistence, initialDocs, colRef => {
      return getDocsFromServer(colRef).then(qrySnap => {
        expect(qrySnap.metadata.fromCache).to.be.false;
        expect(qrySnap.metadata.hasPendingWrites).to.be.false;
        expect(qrySnap.docChanges().length).to.equal(3);
        expect(toDataMap(qrySnap)).to.deep.equal(initialDocs);
      });
    });
  });

  it('get document while offline with source=server', () => {
    const initialData = { key: 'value' };
    return withTestDocAndInitialData(persistence, initialData, (docRef, db) => {
      return getDocFromServer(docRef)
        .then(() => disableNetwork(db))
        .then(() => getDocFromServer(docRef))
        .then(
          () => {
            expect.fail();
          },
          () => {}
        );
    });
  });

  it('get collection while offline with source=server', () => {
    const initialDocs = {
      doc1: { key1: 'value1' },
      doc2: { key2: 'value2' },
      doc3: { key3: 'value3' }
    };
    return withTestCollection(persistence, initialDocs, (colRef, db) => {
      // force local cache of these
      return (
        getDocs(colRef)
          // now go offine. Note that if persistence is disabled, this will cause
          // the initialDocs to be garbage collected.
          .then(() => disableNetwork(db))
          .then(() => getDocsFromServer(colRef))
          .then(
            () => expect.fail(),
            () => {}
          )
      );
    });
  });

  it('get document while offline with different get options', () => {
    const initialData = { key: 'value' };

    return withTestDocAndInitialData(persistence, initialData, (docRef, db) => {
      // Register a snapshot to force the data to stay in the cache and not be
      // garbage collected.
      onSnapshot(docRef, () => {});
      return getDoc(docRef)
        .then(() => disableNetwork(db))
        .then(() => {
          // 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.
          return new Promise<void>((resolve, reject) => {
            onSnapshot(
              docRef,
              () => resolve(),
              () => reject()
            );
          });
        })
        .then(() => getDocFromCache(docRef))
        .then(doc => {
          expect(doc.exists()).to.be.true;
          expect(doc.metadata.fromCache).to.be.true;
          expect(doc.metadata.hasPendingWrites).to.be.false;
          expect(doc.data()).to.deep.equal(initialData);
        })
        .then(() => getDoc(docRef))
        .then(doc => {
          expect(doc.exists()).to.be.true;
          expect(doc.metadata.fromCache).to.be.true;
          expect(doc.metadata.hasPendingWrites).to.be.false;
          expect(doc.data()).to.deep.equal(initialData);
        })
        .then(() => getDocFromServer(docRef))
        .then(
          () => expect.fail(),
          () => {}
        );
    });
  });

  it('get collection while offline with different get options', () => {
    const initialDocs = {
      doc1: { key1: 'value1' },
      doc2: { key2: 'value2' },
      doc3: { key3: 'value3' }
    };
    return withTestCollection(persistence, initialDocs, (colRef, db) => {
      // Register a snapshot to force the data to stay in the cache and not be
      // garbage collected.
      onSnapshot(colRef, () => {});
      return (
        getDocs(colRef)
          // now go offine. Note that if persistence is disabled, this will cause
          // the initialDocs to be garbage collected.
          .then(() => disableNetwork(db))
          .then(() => {
            // NB: since we're offline, the returned promises won't complete
            /* eslint-disable @typescript-eslint/no-floating-promises */
            setDoc(doc(colRef, 'doc2'), { key2b: 'value2b' }, { merge: true });
            setDoc(doc(colRef, 'doc3'), { key3b: 'value3b' });
            setDoc(doc(colRef, 'doc4'), { key4: 'value4' });
            /* eslint-enable @typescript-eslint/no-floating-promises */

            // 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.
            return new Promise<void>((resolve, reject) => {
              onSnapshot(
                colRef,
                () => resolve(),
                () => reject()
              );
            });
          })
          .then(() => getDocsFromCache(colRef))
          .then(qrySnap => {
            expect(qrySnap.metadata.fromCache).to.be.true;
            expect(qrySnap.metadata.hasPendingWrites).to.be.true;
            const docsData = toDataMap(qrySnap);
            expect(qrySnap.docChanges().length).to.equal(4);
            expect(docsData).to.deep.equal({
              doc1: { key1: 'value1' },
              doc2: { key2: 'value2', key2b: 'value2b' },
              doc3: { key3b: 'value3b' },
              doc4: { key4: 'value4' }
            });
          })
          .then(() => getDocs(colRef))
          .then(qrySnap => {
            expect(qrySnap.metadata.fromCache).to.be.true;
            expect(qrySnap.metadata.hasPendingWrites).to.be.true;
            const docsData = toDataMap(qrySnap);
            expect(qrySnap.docChanges().length).to.equal(4);
            expect(docsData).to.deep.equal({
              doc1: { key1: 'value1' },
              doc2: { key2: 'value2', key2b: 'value2b' },
              doc3: { key3b: 'value3b' },
              doc4: { key4: 'value4' }
            });
          })
          .then(() => getDocsFromServer(colRef))
          .then(
            () => expect.fail(),
            () => {}
          )
      );
    });
  });

  it('get non existing doc while online with default get options', () => {
    return withTestDocAndInitialData(persistence, null, docRef => {
      return getDoc(docRef).then(doc => {
        expect(doc.exists()).to.be.false;
        expect(doc.metadata.fromCache).to.be.false;
        expect(doc.metadata.hasPendingWrites).to.be.false;
      });
    });
  });

  it('get non existing collection while online with default get options', () => {
    return withTestCollection(persistence, {}, colRef => {
      return getDocs(colRef).then(qrySnap => {
        //expect(qrySnap.count).to.equal(0);
        expect(qrySnap.empty).to.be.true;
        expect(qrySnap.docChanges().length).to.equal(0);
        expect(qrySnap.metadata.fromCache).to.be.false;
        expect(qrySnap.metadata.hasPendingWrites).to.be.false;
      });
    });
  });

  it('get non existing doc while offline with default get options', () => {
    return withTestDocAndInitialData(persistence, null, (docRef, db) => {
      return (
        disableNetwork(db)
          // Attempt to get doc. This will fail since there's nothing in cache.
          .then(() => getDoc(docRef))
          .then(
            () => expect.fail(),
            () => {}
          )
      );
    });
  });

  // 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.
  // eslint-disable-next-line no-restricted-properties
  it.skip('get deleted doc while offline with default get options', () => {
    return withTestDocAndInitialData(persistence, null, (docRef, db) => {
      return deleteDoc(docRef)
        .then(() => disableNetwork(db))
        .then(() => getDoc(docRef))
        .then(doc => {
          expect(doc.exists()).to.be.false;
          expect(doc.data()).to.be.undefined;
          expect(doc.metadata.fromCache).to.be.true;
          expect(doc.metadata.hasPendingWrites).to.be.false;
        });
    });
  });

  it('get non existing collection while offline with default get options', () => {
    return withTestCollection(persistence, {}, (colRef, db) => {
      return disableNetwork(db)
        .then(() => getDocs(colRef))
        .then(qrySnap => {
          expect(qrySnap.empty).to.be.true;
          expect(qrySnap.docChanges().length).to.equal(0);
          expect(qrySnap.metadata.fromCache).to.be.true;
          expect(qrySnap.metadata.hasPendingWrites).to.be.false;
        });
    });
  });

  it('get non existing doc while online with source=cache', () => {
    return withTestDocAndInitialData(persistence, null, docRef => {
      // Attempt to get doc.  This will fail since there's nothing in cache.
      return getDocFromCache(docRef).then(
        () => expect.fail(),
        () => {}
      );
    });
  });

  it('get non existing collection while online with source=cache', () => {
    return withTestCollection(persistence, {}, colRef => {
      return getDocsFromCache(colRef).then(qrySnap => {
        expect(qrySnap.empty).to.be.true;
        expect(qrySnap.docChanges().length).to.equal(0);
        expect(qrySnap.metadata.fromCache).to.be.true;
        expect(qrySnap.metadata.hasPendingWrites).to.be.false;
      });
    });
  });

  it('get non existing doc while offline with source=cache', () => {
    return withTestDocAndInitialData(persistence, null, (docRef, db) => {
      return (
        disableNetwork(db)
          // Attempt to get doc.  This will fail since there's nothing in cache.
          .then(() =>
            getDocFromCache(docRef).then(
              () => expect.fail(),
              () => {}
            )
          )
      );
    });
  });

  // We need the deleted doc to stay in cache, so only run this with persistence.
  // eslint-disable-next-line no-restricted-properties,
  (persistence ? it : it.skip)(
    'get deleted doc while offline with source=cache',
    () => {
      return withTestDocAndInitialData(persistence, null, (docRef, db) => {
        return (
          deleteDoc(docRef)
            .then(() => disableNetwork(db))
            // Should get a document with exists=false, fromCache=true
            .then(() => getDocFromCache(docRef))
            .then(doc => {
              expect(doc.exists()).to.be.false;
              expect(doc.data()).to.be.undefined;
              expect(doc.metadata.fromCache).to.be.true;
              expect(doc.metadata.hasPendingWrites).to.be.false;
            })
        );
      });
    }
  );

  it('get non existing collection while offline with source=cache', () => {
    return withTestCollection(persistence, {}, (colRef, db) => {
      return disableNetwork(db)
        .then(() => getDocsFromCache(colRef))
        .then(qrySnap => {
          expect(qrySnap.empty).to.be.true;
          expect(qrySnap.docChanges().length).to.equal(0);
          expect(qrySnap.metadata.fromCache).to.be.true;
          expect(qrySnap.metadata.hasPendingWrites).to.be.false;
        });
    });
  });

  it('get non existing doc while online with source=server', () => {
    return withTestDocAndInitialData(persistence, null, docRef => {
      return getDocFromServer(docRef).then(doc => {
        expect(doc.exists()).to.be.false;
        expect(doc.metadata.fromCache).to.be.false;
        expect(doc.metadata.hasPendingWrites).to.be.false;
      });
    });
  });

  it('get non existing collection while online with source=server', () => {
    return withTestCollection(persistence, {}, (colRef, db) => {
      return getDocsFromServer(colRef).then(qrySnap => {
        expect(qrySnap.empty).to.be.true;
        expect(qrySnap.docChanges().length).to.equal(0);
        expect(qrySnap.metadata.fromCache).to.be.false;
        expect(qrySnap.metadata.hasPendingWrites).to.be.false;
      });
    });
  });

  it('get non existing doc while offline with source=server', () => {
    return withTestDocAndInitialData(persistence, null, (docRef, db) => {
      return (
        disableNetwork(db)
          // Attempt to get doc.  This will fail since there's nothing in cache.
          .then(() => getDocFromServer(docRef))
          .then(
            () => expect.fail(),
            () => {}
          )
      );
    });
  });

  it('get non existing collection while offline with source=server', () => {
    return withTestCollection(persistence, {}, (colRef, db) => {
      return disableNetwork(db)
        .then(() => getDocsFromServer(colRef))
        .then(
          () => expect.fail(),
          () => {}
        );
    });
  });
});
