/*
 * Copyright (c) 2016-present Invertase Limited & Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this library 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.
 *
 */

describe('storage() -> StorageReference', () => {
  describe('toString()', () => {
    it('returns the correct bucket path to the file', () => {
      const app = firebase.app();
      firebase
        .storage()
        .ref('/uploadNope.jpeg')
        .toString()
        .should.equal(`gs://${app.options.storageBucket}/uploadNope.jpeg`);
    });
  });

  describe('properties', () => {
    describe('fullPath', () => {
      it('returns the full path as a string', () => {
        firebase
          .storage()
          .ref('/foo/uploadNope.jpeg')
          .fullPath.should.equal('foo/uploadNope.jpeg');
        firebase
          .storage()
          .ref('foo/uploadNope.jpeg')
          .fullPath.should.equal('foo/uploadNope.jpeg');
      });
    });

    describe('storage', () => {
      it('returns the instance of storage', () => {
        firebase
          .storage()
          .ref('/foo/uploadNope.jpeg')
          .storage.ref.should.be.a.Function();
      });
    });

    describe('bucket', () => {
      it('returns the storage bucket as a string', () => {
        const app = firebase.app();
        firebase
          .storage()
          .ref('/foo/uploadNope.jpeg')
          .bucket.should.equal(app.options.storageBucket);
      });
    });

    describe('name', () => {
      it('returns the file name as a string', () => {
        const ref = firebase.storage().ref('/foo/uploadNope.jpeg');
        ref.name.should.equal('uploadNope.jpeg');
      });
    });

    describe('parent', () => {
      it('returns the parent directory as a reference', () => {
        firebase
          .storage()
          .ref('/foo/uploadNope.jpeg')
          .parent.fullPath.should.equal('foo');
      });

      it('returns null if already at root', () => {
        const ref = firebase.storage().ref('/');
        should.equal(ref.parent, null);
      });
    });

    describe('root', () => {
      it('returns a reference to the root of the bucket', () => {
        firebase
          .storage()
          .ref('/foo/uploadNope.jpeg')
          .root.fullPath.should.equal('/');
      });
    });
  });

  describe('child()', () => {
    it('returns a reference to a child path', () => {
      const parentRef = firebase.storage().ref('/foo');
      const childRef = parentRef.child('someFile.json');
      childRef.fullPath.should.equal('foo/someFile.json');
    });
  });

  describe('delete()', () => {
    before(async () => {
      await firebase
        .storage()
        .ref('/ok.jpeg')
        .writeToFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/deleteMe.jpeg`);
      await firebase
        .storage()
        .ref('/deleteMe.jpeg')
        .putFile(`${firebase.utils.FilePath.DOCUMENT_DIRECTORY}/deleteMe.jpeg`);
    });

    it('should delete a file', async () => {
      const storageReference = firebase.storage().ref('/deleteMe.jpeg');
      await storageReference.delete();

      try {
        await storageReference.getMetadata();
        return Promise.reject(new Error('Did not throw'));
      } catch (error) {
        error.code.should.equal('storage/object-not-found');
        error.message.should.equal(
          '[storage/object-not-found] No object exists at the desired reference.',
        );
        return Promise.resolve();
      }
    });

    it('throws error if file does not exist', async () => {
      const storageReference = firebase.storage().ref('/iDoNotExist.jpeg');

      try {
        await storageReference.delete();
        return Promise.reject(new Error('Did not throw'));
      } catch (error) {
        error.code.should.equal('storage/object-not-found');
        error.message.should.equal(
          '[storage/object-not-found] No object exists at the desired reference.',
        );
        return Promise.resolve();
      }
    });

    it('throws error if no write permission', async () => {
      const storageReference = firebase.storage().ref('/uploadNope.jpeg');

      try {
        await storageReference.delete();
        return Promise.reject(new Error('Did not throw'));
      } catch (error) {
        error.code.should.equal('storage/unauthorized');
        error.message.should.equal(
          '[storage/unauthorized] User is not authorized to perform the desired action.',
        );
        return Promise.resolve();
      }
    });
  });

  describe('getDownloadURL', () => {
    it('should return a download url for a file', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      const downloadUrl = await storageReference.getDownloadURL();
      downloadUrl.should.be.a.String();
      downloadUrl.should.containEql('/ok.jpeg');
      downloadUrl.should.containEql(firebase.app().options.projectId);
    });

    it('throws error if file does not exist', async () => {
      const storageReference = firebase.storage().ref('/iDoNotExist.jpeg');

      try {
        await storageReference.getDownloadURL();
        return Promise.reject(new Error('Did not throw'));
      } catch (error) {
        error.code.should.equal('storage/object-not-found');
        error.message.should.equal(
          '[storage/object-not-found] No object exists at the desired reference.',
        );
        return Promise.resolve();
      }
    });

    it('throws error if no write permission', async () => {
      const storageReference = firebase.storage().ref('/writeOnly.jpeg');

      try {
        await storageReference.getDownloadURL();
        return Promise.reject(new Error('Did not throw'));
      } catch (error) {
        error.code.should.equal('storage/unauthorized');
        error.message.should.equal(
          '[storage/unauthorized] User is not authorized to perform the desired action.',
        );
        return Promise.resolve();
      }
    });
  });

  describe('getMetadata', () => {
    it('should return a metadata for a file', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      const metadata = await storageReference.getMetadata();
      metadata.generation.should.be.a.String();
      metadata.fullPath.should.equal('ok.jpeg');
      metadata.name.should.equal('ok.jpeg');
      metadata.size.should.be.a.Number();
      should.equal(metadata.size > 0, true);
      metadata.updated.should.be.a.String();
      metadata.timeCreated.should.be.a.String();
      metadata.contentEncoding.should.be.a.String();
      metadata.contentDisposition.should.be.a.String();
      metadata.contentType.should.equal('image/jpeg');
      metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`);
      metadata.metageneration.should.be.a.String();
      metadata.md5Hash.should.be.a.String();
      should.equal(metadata.cacheControl, null);
      should.equal(metadata.contentLanguage, null);
      should.equal(metadata.customMetadata, null);
    });
  });

  describe('list', () => {
    it('should return list results', async () => {
      const storageReference = firebase.storage().ref('/list');
      const result = await storageReference.list();

      result.constructor.name.should.eql('StorageListResult');
      result.should.have.property('nextPageToken');

      result.items.should.be.Array();
      result.items.length.should.be.greaterThan(0);
      result.items[0].constructor.name.should.eql('StorageReference');

      result.prefixes.should.be.Array();
      result.prefixes.length.should.be.greaterThan(0);
      result.prefixes[0].constructor.name.should.eql('StorageReference');
    });

    it('throws if options is not an object', () => {
      try {
        const storageReference = firebase.storage().ref('/');
        storageReference.list(123);
        return Promise.reject(new Error('Did not throw'));
      } catch (error) {
        error.message.should.containEql("'options' expected an object value");
        return Promise.resolve();
      }
    });

    describe('maxResults', () => {
      it('should limit with maxResults are passed', async () => {
        const storageReference = firebase.storage().ref('/list');
        const result = await storageReference.list({
          maxResults: 1,
        });

        result.nextPageToken.should.be.String();

        result.items.should.be.Array();
        result.items.length.should.eql(1);
        result.items[0].constructor.name.should.eql('StorageReference');

        result.prefixes.should.be.Array();
        // todo length?
      });

      it('throws if maxResults is not a number', () => {
        try {
          const storageReference = firebase.storage().ref('/list');
          storageReference.list({
            maxResults: '123',
          });
          return Promise.reject(new Error('Did not throw'));
        } catch (error) {
          error.message.should.containEql("'options.maxResults' expected a number value");
          return Promise.resolve();
        }
      });

      it('throws if maxResults is not a valid number', () => {
        try {
          const storageReference = firebase.storage().ref('/list');
          storageReference.list({
            maxResults: 2000,
          });
          return Promise.reject(new Error('Did not throw'));
        } catch (error) {
          error.message.should.containEql(
            "'options.maxResults' expected a number value between 1-1000",
          );
          return Promise.resolve();
        }
      });
    });

    describe('pageToken', () => {
      it('throws if pageToken is not a string', () => {
        try {
          const storageReference = firebase.storage().ref('/list');
          storageReference.list({
            pageToken: 123,
          });
          return Promise.reject(new Error('Did not throw'));
        } catch (error) {
          error.message.should.containEql("'options.pageToken' expected a string value");
          return Promise.resolve();
        }
      });

      it('should return and use a page token', async () => {
        const storageReference = firebase.storage().ref('/list');
        const result1 = await storageReference.list({
          maxResults: 1,
        });

        const item1 = result1.items[0].fullPath;

        const result2 = await storageReference.list({
          maxResults: 1,
          pageToken: result1.nextPageToken,
        });

        const item2 = result2.items[0].fullPath;

        if (item1 === item2) {
          throw new Error('Expected item results to be different.');
        }
      });
    });
  });

  describe('listAll', () => {
    it('should return all results', async () => {
      const storageReference = firebase.storage().ref('/list');
      const result = await storageReference.listAll();

      should.equal(result.nextPageToken, null);

      result.items.should.be.Array();
      result.items.length.should.be.greaterThan(0);
      result.items[0].constructor.name.should.eql('StorageReference');

      result.prefixes.should.be.Array();
      result.prefixes.length.should.be.greaterThan(0);
      result.prefixes[0].constructor.name.should.eql('StorageReference');
    });
  });

  describe('updateMetadata', () => {
    it('should return the updated metadata for a file', async () => {
      const storageReference = firebase.storage().ref('/writeOnly.jpeg');
      const metadata = await storageReference.updateMetadata({
        contentType: 'image/jpeg',
        customMetadata: {
          hello: 'world',
        },
      });

      metadata.customMetadata.hello.should.equal('world');
      metadata.generation.should.be.a.String();
      metadata.fullPath.should.equal('writeOnly.jpeg');
      metadata.name.should.equal('writeOnly.jpeg');
      metadata.size.should.be.a.Number();
      should.equal(metadata.size > 0, true);
      metadata.updated.should.be.a.String();
      metadata.timeCreated.should.be.a.String();
      metadata.contentEncoding.should.be.a.String();
      metadata.contentDisposition.should.be.a.String();
      metadata.contentType.should.equal('image/jpeg');
      metadata.bucket.should.equal(`${firebase.app().options.projectId}.appspot.com`);
      metadata.metageneration.should.be.a.String();
      metadata.md5Hash.should.be.a.String();
      should.equal(metadata.cacheControl, null);
      should.equal(metadata.contentLanguage, null);
      metadata.customMetadata.should.be.an.Object();
    });

    it('should remove customMetadata properties by setting the value to null', async () => {
      const storageReference = firebase.storage().ref('/writeOnly.jpeg');
      const metadata = await storageReference.updateMetadata({
        contentType: 'image/jpeg',
        customMetadata: {
          removeMe: 'please',
        },
      });

      metadata.customMetadata.removeMe.should.equal('please');

      const metadataAfterRemove = await storageReference.updateMetadata({
        contentType: 'image/jpeg',
        customMetadata: {
          removeMe: null,
        },
      });

      should.equal(metadataAfterRemove.customMetadata.removeMe, undefined);
    });
  });

  describe('putFile', () => {
    it('errors if file path is not a string', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.putFile(1337);
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql('expects a string value');
        return Promise.resolve();
      }
    });

    it('errors if metadata is not an object', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.putFile('foo', 123);
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql('must be an object value');
        return Promise.resolve();
      }
    });

    it('errors if metadata contains an unsupported property', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.putFile('foo', { foo: true });
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql("unknown property 'foo'");
        return Promise.resolve();
      }
    });

    it('errors if metadata property value is not a string or null value', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.putFile('foo', { contentType: true });
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql('should be a string or null value');
        return Promise.resolve();
      }
    });

    it('errors if metadata.customMetadata is not an object', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.putFile('foo', { customMetadata: true });
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql(
          'customMetadata must be an object of keys and string values',
        );
        return Promise.resolve();
      }
    });
  });

  describe('putString', () => {
    it('errors if metadata is not an object', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.putString('foo', 'raw', 123);
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql('must be an object value');
        return Promise.resolve();
      }
    });

    it('errors if metadata contains an unsupported property', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.putString('foo', 'raw', { foo: true });
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql("unknown property 'foo'");
        return Promise.resolve();
      }
    });

    it('errors if metadata property value is not a string or null value', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.putString('foo', 'raw', { contentType: true });
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql('should be a string or null value');
        return Promise.resolve();
      }
    });

    it('errors if metadata.customMetadata is not an object', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.putString('foo', 'raw', { customMetadata: true });
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql(
          'customMetadata must be an object of keys and string values',
        );
        return Promise.resolve();
      }
    });
  });

  describe('put', () => {
    it('errors if metadata is not an object', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.put(new jet.context.window.ArrayBuffer(), 123);
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql('must be an object value');
        return Promise.resolve();
      }
    });

    it('errors if metadata contains an unsupported property', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.put(new jet.context.window.ArrayBuffer(), { foo: true });
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql("unknown property 'foo'");
        return Promise.resolve();
      }
    });

    it('errors if metadata property value is not a string or null value', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.put(new jet.context.window.ArrayBuffer(), { contentType: true });
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql('should be a string or null value');
        return Promise.resolve();
      }
    });

    it('errors if metadata.customMetadata is not an object', async () => {
      const storageReference = firebase.storage().ref('/ok.jpeg');
      try {
        storageReference.put(new jet.context.window.ArrayBuffer(), { customMetadata: true });
        return Promise.reject(new Error('Did not error!'));
      } catch (error) {
        error.message.should.containEql(
          'customMetadata must be an object of keys and string values',
        );
        return Promise.resolve();
      }
    });
  });
});
