/**
 * A firebase configuaration of a project made only for those tests.
 */
const firebaseConfig = {
  apiKey: 'AIzaSyBwPnGpfEBXDjwQrWfU0wqgp4m9qEt7YM8',
  authDomain: 'gdtest-e11a5.firebaseapp.com',
  databaseURL: 'https://gdtest-e11a5.firebaseio.com',
  projectId: 'gdtest-e11a5',
  storageBucket: 'gdtest-e11a5.appspot.com',
  messagingSenderId: '254035412678',
  appId: '1:254035412678:web:2ddd6b83019b7f259b79c7',
  measurementId: 'G-4REML26L59',
};

/**
 * Turns a callback variable into a promise.
 * @param {(callbackVariable: {setString: (result: "ok" | string) => void}, result: gdjs.Variable) => any} executor
 * @returns {Promise<gdjs.Variable>}
 */
const promisifyCallbackVariables = (executor) =>
  new Promise((done, err) => {
    const callbackResult = new gdjs.Variable();
    executor(
      {
        setString: (string) => {
          if (string === 'ok') done(callbackResult);
          else err(new Error(string));
        },
      },
      callbackResult
    );
  });

/** A complex variable using all variables types. */
const variable = new gdjs.Variable().fromJSObject({
  my: 'test',
  object: { with: 'all', types: 2 },
  it: ['is', true],
});

// The tests require an internet connection, as a real Firebase instance is used.
const describeIfOnline = navigator.onLine ? describe : describe.skip;
describeIfOnline('Firebase extension end-to-end tests', function () {
  // Increase the timeout to work on low connections as well.
  this.timeout('5s');

  const namespace = `temp-test-${Math.random()
    .toString()
    .replace('.', '-')}-${Date.now()}`;

  before(function setupFirebase() {
    gdjs.evtTools.firebaseTools._setupFirebase({
      getGame: () => ({
        getExtensionProperty: () => JSON.stringify(firebaseConfig),
      }),
    });
  });

  describe('Firebase database', () => {
    it('should write and read back a variable from the database', async () => {
      const path = namespace + '/variable';
      await promisifyCallbackVariables((callback) => {
        gdjs.evtTools.firebaseTools.database.writeVariable(
          path,
          variable,
          callback
        );
      });

      const fetchedVariable = await promisifyCallbackVariables(
        (callback, result) =>
          gdjs.evtTools.firebaseTools.database.getVariable(
            path,
            result,
            callback
          )
      );

      expect(fetchedVariable).to.eql(variable);
    });

    it('should write and read back a field from the database', async function () {
      const path = namespace + '/field';
      await promisifyCallbackVariables((callback) => {
        gdjs.evtTools.firebaseTools.database.writeField(
          path,
          'hello',
          1,
          callback
        );
      });

      const fetchedVariable = await promisifyCallbackVariables(
        (callback, result) =>
          gdjs.evtTools.firebaseTools.database.getField(
            path,
            'hello',
            result,
            callback
          )
      );

      expect(fetchedVariable.getType()).to.be.string('number');
      expect(fetchedVariable.getAsNumber()).to.be.equal(1);
    });

    it('should update a field from the database', async function () {
      const path = namespace + '/update';

      // Populate with data to update
      await firebase
        .database()
        .ref(path)
        .set({ hello: 'Hello', goodbye: 'Goodbye' });

      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.database.updateField(
          path,
          'hello',
          'Good day',
          callback
        )
      );
      expect((await firebase.database().ref(path).get()).val()).to.eql({
        hello: 'Good day',
        goodbye: 'Goodbye',
      });

      const updater = new gdjs.Variable().fromJSObject({
        goodbye: 'See you later',
      });

      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.database.updateVariable(
          path,
          updater,
          callback
        )
      );

      expect((await firebase.database().ref(path).get()).val()).to.eql({
        hello: 'Good day',
        goodbye: 'See you later',
      });
    });

    it('should delete and detect presence of a field from the database', async function () {
      const path = namespace + '/delete';

      // Populate with data to delete
      await firebase
        .database()
        .ref(path)
        .set({ hello: 'Hello', goodbye: 'Goodbye' });

      expect(
        (
          await promisifyCallbackVariables((callback, result) =>
            gdjs.evtTools.firebaseTools.database.hasVariable(
              path,
              result,
              callback
            )
          )
        ).getValue()
      ).to.be.ok();

      expect(
        (
          await promisifyCallbackVariables((callback, result) =>
            gdjs.evtTools.firebaseTools.database.hasField(
              path,
              'hello',
              result,
              callback
            )
          )
        ).getValue()
      ).to.be.ok();

      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.database.deleteField(
          path,
          'hello',
          callback
        )
      );

      expect(
        (
          await promisifyCallbackVariables((callback, result) =>
            gdjs.evtTools.firebaseTools.database.hasField(
              path,
              'hello',
              result,
              callback
            )
          )
        ).getValue()
      ).to.not.be.ok();

      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.database.deleteVariable(path, callback)
      );

      expect(
        (
          await promisifyCallbackVariables((callback, result) =>
            gdjs.evtTools.firebaseTools.database.hasVariable(
              path,
              result,
              callback
            )
          )
        ).getValue()
      ).to.not.be.ok();
    });

    // Delete the temporary namespace to not bloat the DB
    after(() => firebase.database().ref(namespace).remove());
  });

  describe('Cloud Firestore', () => {
    describe('Base read/write operations', () => {
      it('should write and read back a variable from Firestore', async () => {
        await promisifyCallbackVariables((callback) => {
          gdjs.evtTools.firebaseTools.firestore.writeDocument(
            namespace,
            'variable',
            variable,
            callback
          );
        });

        const fetchedVariable = await promisifyCallbackVariables(
          (callback, result) =>
            gdjs.evtTools.firebaseTools.firestore.getDocument(
              namespace,
              'variable',
              result,
              callback
            )
        );

        expect(fetchedVariable).to.eql(variable);
      });

      it('should write and read back a field from Firestore', async function () {
        await promisifyCallbackVariables((callback) => {
          gdjs.evtTools.firebaseTools.firestore.writeField(
            namespace,
            'field',
            'hello',
            1,
            callback
          );
        });

        const fetchedVariable = await promisifyCallbackVariables(
          (callback, result) =>
            gdjs.evtTools.firebaseTools.firestore.getField(
              namespace,
              'field',
              'hello',
              result,
              callback
            )
        );

        expect(fetchedVariable.getType()).to.be.string('number');
        expect(fetchedVariable.getAsNumber()).to.be.equal(1);
      });

      it('should update a field from Firestore', async function () {
        const doc = firebase.firestore().doc(namespace + '/update');

        // Populate with data to update
        await doc.set({ hello: 'Hello', goodbye: 'Goodbye' });

        await promisifyCallbackVariables((callback) =>
          gdjs.evtTools.firebaseTools.firestore.updateField(
            namespace,
            'update',
            'hello',
            'Good day',
            callback
          )
        );
        expect((await doc.get()).data()).to.eql({
          hello: 'Good day',
          goodbye: 'Goodbye',
        });

        const updater = new gdjs.Variable().fromJSObject({
          goodbye: 'See you later',
        });

        await promisifyCallbackVariables((callback) =>
          gdjs.evtTools.firebaseTools.firestore.updateDocument(
            namespace,
            'update',
            updater,
            callback
          )
        );
        expect((await doc.get()).data()).to.eql({
          hello: 'Good day',
          goodbye: 'See you later',
        });
      });

      it('should delete and detect presence of a field from Firestore', async function () {
        const doc = firebase.firestore().doc(namespace + '/delete');

        // Populate with data to delete
        await doc.set({ hello: 'Hello', goodbye: 'Goodbye' });

        expect(
          (
            await promisifyCallbackVariables((callback, result) =>
              gdjs.evtTools.firebaseTools.firestore.hasDocument(
                namespace,
                'delete',
                result,
                callback
              )
            )
          ).getValue()
        ).to.be.ok();

        expect(
          (
            await promisifyCallbackVariables((callback, result) =>
              gdjs.evtTools.firebaseTools.firestore.hasField(
                namespace,
                'delete',
                'hello',
                result,
                callback
              )
            )
          ).getValue()
        ).to.be.ok();

        await promisifyCallbackVariables((callback) =>
          gdjs.evtTools.firebaseTools.firestore.deleteField(
            namespace,
            'delete',
            'hello',
            callback
          )
        );

        expect(
          (
            await promisifyCallbackVariables((callback, result) =>
              gdjs.evtTools.firebaseTools.firestore.hasField(
                namespace,
                'delete',
                'hello',
                result,
                callback
              )
            )
          ).getValue()
        ).to.not.be.ok();

        await promisifyCallbackVariables((callback) =>
          gdjs.evtTools.firebaseTools.firestore.deleteDocument(
            namespace,
            'delete',
            callback
          )
        );

        expect(
          (
            await promisifyCallbackVariables((callback, result) =>
              gdjs.evtTools.firebaseTools.firestore.hasDocument(
                namespace,
                'delete',
                result,
                callback
              )
            )
          ).getValue()
        ).to.not.be.ok();
      });

      it('should be able to use server timestamps', async () => {
        await promisifyCallbackVariables((callback) => {
          gdjs.evtTools.firebaseTools.firestore.writeField(
            namespace,
            'field',
            'timestamp',
            gdjs.evtTools.firebaseTools.firestore.getServerTimestamp(),
            callback
          );
        });

        expect(
          (
            await firebase
              .firestore()
              .doc(namespace + '/field')
              .get()
          ).get('timestamp', { serverTimestamps: 'estimate' })
        ).to.be.an(firebase.firestore.Timestamp);
      });
    });

    describe('Firestore Queries', async () => {
      it('can get a list of documents from the database', async () => {
        const subcollection = namespace + '/collection/sub';
        await firebase
          .firestore()
          .doc(subcollection + '/doc1')
          .set({ index: 2, val: 2 });
        await firebase
          .firestore()
          .doc(subcollection + '/doc2')
          .set({ index: 1, val: 2 });
        await firebase
          .firestore()
          .doc(subcollection + '/doc3')
          .set({ index: 3, val: 1 });

        const executeQuery = async (query) =>
          (
            await promisifyCallbackVariables((callback, result) =>
              gdjs.evtTools.firebaseTools.firestore.executeQuery(
                query,
                result,
                callback
              )
            )
          ).toJSObject();

        // Empty query
        gdjs.evtTools.firebaseTools.firestore.startQuery('main', subcollection);
        expect(await executeQuery('main')).to.eql({
          size: 3,
          empty: false,
          docs: [
            { id: 'doc1', data: { index: 2, val: 2 }, exists: true },
            { id: 'doc2', data: { index: 1, val: 2 }, exists: true },
            { id: 'doc3', data: { index: 3, val: 1 }, exists: true },
          ],
        });

        // Query with selector fitler
        gdjs.evtTools.firebaseTools.firestore.startQueryFrom(
          'selector',
          'main'
        );
        gdjs.evtTools.firebaseTools.firestore.queryWhere(
          'selector',
          'index',
          '>',
          1
        );
        expect(await executeQuery('selector')).to.eql({
          size: 2,
          empty: false,
          docs: [
            { id: 'doc1', data: { index: 2, val: 2 }, exists: true },
            { id: 'doc3', data: { index: 3, val: 1 }, exists: true },
          ],
        });

        // Query with order filter
        gdjs.evtTools.firebaseTools.firestore.startQueryFrom('order', 'main');
        gdjs.evtTools.firebaseTools.firestore.queryOrderBy(
          'order',
          'index',
          'desc'
        );
        expect(await executeQuery('order')).to.eql({
          size: 3,
          empty: false,
          docs: [
            { id: 'doc3', data: { index: 3, val: 1 }, exists: true },
            { id: 'doc1', data: { index: 2, val: 2 }, exists: true },
            { id: 'doc2', data: { index: 1, val: 2 }, exists: true },
          ],
        });

        // Query with limit filter
        gdjs.evtTools.firebaseTools.firestore.startQueryFrom('limit', 'order');
        gdjs.evtTools.firebaseTools.firestore.queryLimit('limit', 2, true);
        expect(await executeQuery('limit')).to.eql({
          size: 2,
          empty: false,
          docs: [
            { id: 'doc1', data: { index: 2, val: 2 }, exists: true },
            { id: 'doc2', data: { index: 1, val: 2 }, exists: true },
          ],
        });

        // Query with skip some filter
        gdjs.evtTools.firebaseTools.firestore.startQueryFrom('skip', 'order');
        gdjs.evtTools.firebaseTools.firestore.querySkipSome(
          'skip',
          2,
          true,
          true
        );
        expect(await executeQuery('skip')).to.eql({
          size: 2,
          empty: false,
          docs: [
            { id: 'doc3', data: { index: 3, val: 1 }, exists: true },
            { id: 'doc1', data: { index: 2, val: 2 }, exists: true },
          ],
        });
      });
    });

    // Delete the temporary namespace to not bloat the DB
    after(async () =>
      (
        await firebase.firestore().collection(namespace).get()
      ).forEach(({ ref }) => ref.delete())
    );
  });

  describe('Firebase authentication', () => {
    // Generate basic credentials
    const email = `test${Math.random().toString(
      16
    )}${Date.now()}@test-account.com`;
    const password = `myPass${Math.random().toString(16)}${Date.now()}!`;
    const password2 = `myNewPass${Math.random().toString(16)}${Date.now()}!`;

    const expectToNotLogin = async (password) => {
      if (gdjs.evtTools.firebaseTools.auth.isAuthentified())
        await firebase.auth().signOut();

      let errors = false;
      try {
        await promisifyCallbackVariables((callback) =>
          gdjs.evtTools.firebaseTools.auth.signInWithEmail(
            email,
            password,
            callback
          )
        );
      } catch (e) {
        errors = true;
      }

      // No error was throwm, there is an issue
      if (!errors)
        throw new Error('Expected wrong credentials to prevent login');

      expect(gdjs.evtTools.firebaseTools.auth.isAuthentified()).to.not.be.ok();
    };

    const expectToLogin = async (password) => {
      if (gdjs.evtTools.firebaseTools.auth.isAuthentified())
        await firebase.auth().signOut();

      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.auth.signInWithEmail(
          email,
          password,
          callback
        )
      );

      expect(gdjs.evtTools.firebaseTools.auth.isAuthentified()).to.be.ok();
    };

    before(async () => firebase.auth().signOut());

    it('let users create accounts', async () => {
      expect(gdjs.evtTools.firebaseTools.auth.isAuthentified()).to.not.be.ok();

      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.auth.createAccountWithEmail(
          email,
          password,
          callback
        )
      );

      expect(gdjs.evtTools.firebaseTools.auth.isAuthentified()).to.be.ok();
    });

    it('let users log out', async () => {
      expect(gdjs.evtTools.firebaseTools.auth.isAuthentified()).to.be.ok();
      await firebase.auth().signOut();
      expect(gdjs.evtTools.firebaseTools.auth.isAuthentified()).to.not.be.ok();
    });

    it('prevents logging in with invalid credentials', async () =>
      expectToNotLogin('InvalidPassword321'));

    it('let users log in', async () => expectToLogin(password));

    it('Let users get/set basic profile data', async () => {
      await gdjs.evtTools.firebaseTools.auth.userManagement.setDisplayName(
        'Hello'
      );
      expect(
        gdjs.evtTools.firebaseTools.auth.userManagement.getDisplayName()
      ).to.be.string('Hello');

      await gdjs.evtTools.firebaseTools.auth.userManagement.setDisplayName(
        'World'
      );
      expect(
        gdjs.evtTools.firebaseTools.auth.userManagement.getDisplayName()
      ).to.be.string('World');
    });

    it('let users change their password', async () => {
      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.auth.userManagement.dangerous.changePassword(
          email,
          password,
          password2,
          callback
        )
      );

      await expectToNotLogin(password);
      await expectToLogin(password2);
    });

    it('let users delete their account', async () => {
      await expectToLogin(password2);

      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.auth.userManagement.dangerous.deleteUser(
          email,
          password2,
          callback
        )
      );

      return expectToNotLogin(password2);
    });
  });

  describe('Firebase storage', () => {
    const filename = `MyImage-${Math.random().toString(16)}.png`;
    it('uploads an image', async () => {
      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.storage.uploadFile(
          'myUpload',
          'data:image/jpeg;base64, ',
          filename,
          'data_url',
          callback
        )
      );
    });

    it('gets download url for an image', async () => {
      const url = await promisifyCallbackVariables((callback, result) =>
        gdjs.evtTools.firebaseTools.storage.getDownloadURL(
          filename,
          result,
          callback
        )
      );
      expect((await fetch(url.getAsString())).ok).to.be.ok();
    });

    it('deletes an image', async () => {
      const url = await firebase.storage().ref(filename).getDownloadURL();
      expect((await fetch(url)).ok).to.be.ok();

      await promisifyCallbackVariables((callback) =>
        gdjs.evtTools.firebaseTools.storage.deleteFile(filename, callback)
      );

      expect((await fetch(url)).ok).to.not.be.ok();
    });
  });
});
