import has from 'lodash.has';

if (Meteor.isServer) {
  // Set up allow/deny rules for test collections

  var allowCollections = {};

  // We create the collections in the publisher (instead of using a method or
  // something) because if we made them with a method, we'd need to follow the
  // method with some subscribes, and it's possible that the method call would
  // be delayed by a wait method and the subscribe messages would be sent before
  // it and fail due to the collection not yet existing. So we are very hacky
  // and use a publish.
  Meteor.publish("allowTests", function (nonce, idGeneration) {
    check(nonce, String);
    check(idGeneration, String);
    var cursors = [];
    var needToConfigure;

    // helper for defining a collection. we are careful to create just one
    // Mongo.Collection even if the sub body is rerun, by caching them.
    var defineCollection = function(name, insecure, transform) {
      var fullName = name + idGeneration + nonce;

      var collection;
      if (has(allowCollections, fullName)) {
        collection = allowCollections[fullName];
        if (needToConfigure === true)
          throw new Error("collections inconsistently exist");
        needToConfigure = false;
      } else {
        collection = new Mongo.Collection(
          fullName, {idGeneration: idGeneration, transform: transform});
        allowCollections[fullName] = collection;
        if (needToConfigure === false)
          throw new Error("collections inconsistently don't exist");
        needToConfigure = true;
        collection._insecure = insecure;
        var m = {};
        m["clear-collection-" + fullName] = async function() {
          await collection.removeAsync({}, { returnServerResultPromise: true });
        };
        Meteor.methods(m);
      }

      cursors.push(collection.find());
      return collection;
    };

    var insecureCollection = defineCollection(
      "collection-insecure", true /*insecure*/);
    // totally locked down collection
    var lockedDownCollection = defineCollection(
      "collection-locked-down", false /*insecure*/);
    // restricted collection with same allowed modifications, both with and
    // without the `insecure` package
    var restrictedCollectionDefaultSecure = defineCollection(
      "collection-restrictedDefaultSecure", false /*insecure*/);
    var restrictedCollectionDefaultInsecure = defineCollection(
      "collection-restrictedDefaultInsecure", true /*insecure*/);
    var restrictedCollectionForUpdateOptionsTest = defineCollection(
      "collection-restrictedForUpdateOptionsTest", true /*insecure*/);
    var restrictedCollectionForPartialAllowTest = defineCollection(
      "collection-restrictedForPartialAllowTest", true /*insecure*/);
    var restrictedCollectionForPartialDenyTest = defineCollection(
      "collection-restrictedForPartialDenyTest", true /*insecure*/);
    var restrictedCollectionForFetchTest = defineCollection(
      "collection-restrictedForFetchTest", true /*insecure*/);
    var restrictedCollectionForFetchAllTest = defineCollection(
      "collection-restrictedForFetchAllTest", true /*insecure*/);
    var restrictedCollectionWithTransform = defineCollection(
      "withTransform", false, function (doc) {
        return doc.a;
      });
    var restrictedCollectionForInvalidTransformTest = defineCollection(
      "collection-restrictedForInvalidTransform", false /*insecure*/);
    var restrictedCollectionForClientIdTest = defineCollection(
      "collection-restrictedForClientIdTest", false /*insecure*/);

    if (needToConfigure) {
      restrictedCollectionWithTransform.allow({
        insertAsync: function (userId, doc) {
          return doc.foo === "foo";
        },
        updateAsync: function (userId, doc) {
          return doc.foo === "foo";
        },
        removeAsync: function (userId, doc) {
          return doc.bar === "bar";
        }
      });
      restrictedCollectionWithTransform.allow({
        // transform: null means that doc here is the top level, not the 'a'
        // element.
        transform: null,
        insertAsync: function (userId, doc) {
          return !!doc.topLevelField;
        },
        updateAsync: function (userId, doc) {
          return !!doc.topLevelField;
        }
      });
      restrictedCollectionForInvalidTransformTest.allow({
        // transform must return an object which is not a mongo id
        transform: function (doc) { return doc._id; },
        insert: function () { return true; }
      });
      restrictedCollectionForClientIdTest.allow({
        // This test just requires the collection to trigger the restricted
        // case.
        insert: function () { return true; },
        insertAsync: function () { return true; }
      });

      // two calls to allow to verify that either validator is sufficient.
      var allows = [{
        insertAsync: function(userId, doc) {
          return doc.canInsert;
        },
        updateAsync: function(userId, doc) {
          return doc.canUpdate;
        },
        removeAsync: function (userId, doc) {
          return doc.canRemove;
        }
      }, {
        insertAsync: function(userId, doc) {
          return doc.canInsert2;
        },
        updateAsync: function(userId, doc, fields, modifier) {
          return -1 !== fields.indexOf('canUpdate2');
        },
        removeAsync: function(userId, doc) {
          return doc.canRemove2;
        }
      }];

      // two calls to deny to verify that either one blocks the change.
      var denies = [{
        insertAsync: function(userId, doc) {
          return doc.cantInsert;
        },
        removeAsync: function (userId, doc) {
          return doc.cantRemove;
        }
      }, {
        insertAsync: function(userId, doc) {
          // Don't allow explicit ID to be set by the client.
          return has(doc, '_id');
        },
        updateAsync: function(userId, doc, fields, modifier) {
          return -1 !== fields.indexOf('verySecret');
        }
      }];

      [
        restrictedCollectionDefaultSecure,
        restrictedCollectionDefaultInsecure,
        restrictedCollectionForUpdateOptionsTest
      ].forEach(function (collection) {
        allows.forEach(function (allow) {
          collection.allow(allow);
        });
        denies.forEach(function (deny) {
          collection.deny(deny);
        });
      });

      // just restrict one operation so that we can verify that others
      // fail
      restrictedCollectionForPartialAllowTest.allow({
        insert: function() {}
      });
      restrictedCollectionForPartialDenyTest.deny({
        insert: function() {}
      });

      // verify that we only fetch the fields specified - we should
      // be fetching just field1, field2, and field3.
      restrictedCollectionForFetchTest.allow({
        insertAsync: function() { return true; },
        updateAsync: function(userId, doc) {
          // throw fields in doc so that we can inspect them in test
          throw new Meteor.Error(
            999, "Test: Fields in doc: " + Object.keys(doc).sort().join(','));
        },
        removeAsync: function(userId, doc) {
          // throw fields in doc so that we can inspect them in test
          throw new Meteor.Error(
            999, "Test: Fields in doc: " + Object.keys(doc).sort().join(','));
        },
        fetch: ['field1']
      });
      restrictedCollectionForFetchTest.allow({
        fetch: ['field2']
      });
      restrictedCollectionForFetchTest.deny({
        fetch: ['field3']
      });

      // verify that not passing fetch to one of the calls to allow
      // causes all fields to be fetched
      restrictedCollectionForFetchAllTest.allow({
        insertAsync: function() { return true; },
        updateAsync: function(userId, doc) {
          // throw fields in doc so that we can inspect them in test
          throw new Meteor.Error(
            999, "Test: Fields in doc: " + Object.keys(doc).sort().join(','));
        },
        removeAsync: function(userId, doc) {
          // throw fields in doc so that we can inspect them in test
          throw new Meteor.Error(
            999, "Test: Fields in doc: " + Object.keys(doc).sort().join(','));
        },
        fetch: ['field1']
      });
      restrictedCollectionForFetchAllTest.allow({
        updateAsync: function() { return true; }
      });
    }

    return cursors;
  });
}

if (Meteor.isClient) {
  ['STRING', 'MONGO'].forEach(function (idGeneration) {
    // Set up a bunch of test collections... on the client! They match the ones
    // created by setUpAllowTestsCollections.

    var nonce = Random.id();
    // Tell the server to make, configure, and publish a set of collections unique
    // to our test run. Since the method does not unblock, this will complete
    // running on the server before anything else happens.
    Meteor.subscribe('allowTests', nonce, idGeneration);

    // helper for defining a collection, subscribing to it, and defining
    // a method to clear it
    var defineCollection = function(name, transform) {
      var fullName = name + idGeneration + nonce;
      var collection = new Mongo.Collection(
        fullName, {idGeneration: idGeneration, transform: transform});

      collection.callClearMethod = async function () {
        await Meteor.callAsync('clear-collection-' + fullName);
      };
      collection.unnoncedName = name + idGeneration;
      return collection;
    };

    // totally insecure collection
    var insecureCollection = defineCollection("collection-insecure");

    // totally locked down collection
    var lockedDownCollection = defineCollection("collection-locked-down");

    // restricted collection with same allowed modifications, both with and
    // without the `insecure` package
    var restrictedCollectionDefaultSecure = defineCollection(
      "collection-restrictedDefaultSecure");
    var restrictedCollectionDefaultInsecure = defineCollection(
      "collection-restrictedDefaultInsecure");
    var restrictedCollectionForUpdateOptionsTest = defineCollection(
      "collection-restrictedForUpdateOptionsTest");
    var restrictedCollectionForPartialAllowTest = defineCollection(
      "collection-restrictedForPartialAllowTest");
    var restrictedCollectionForPartialDenyTest = defineCollection(
      "collection-restrictedForPartialDenyTest");
    var restrictedCollectionForFetchTest = defineCollection(
      "collection-restrictedForFetchTest");
    var restrictedCollectionForFetchAllTest = defineCollection(
      "collection-restrictedForFetchAllTest");
    var restrictedCollectionWithTransform = defineCollection(
      "withTransform", function (doc) {
        return doc.a;
      });
    var restrictedCollectionForInvalidTransformTest = defineCollection(
      "collection-restrictedForInvalidTransform");
    var restrictedCollectionForClientIdTest = defineCollection(
      "collection-restrictedForClientIdTest");

    // test that if allow is called once then the collection is
    // restricted, and that other mutations aren't allowed
    testAsyncMulti('collection - partial allow, ' + idGeneration, [
      async function(test, expect) {
        try {
          await restrictedCollectionForPartialAllowTest.updateAsync(
            'foo',
            { $set: { updated: true } },
            {
              returnServerResultPromise: true,
            }
          );
        } catch (err) {
          test.equal(err.error, 403);
        }
      },
    ]);

    // test that if deny is called once then the collection is
    // restricted, and that other mutations aren't allowed
    testAsyncMulti('collection - partial deny, ' + idGeneration, [
      async function(test, expect) {
        try {
          await restrictedCollectionForPartialDenyTest.updateAsync(
            'foo',
            {
              $set: { updated: true },
            },
            {
              returnServerResultPromise: true,
            }
          );
        } catch (err) {
          test.equal(err.error, 403);
        }
      },
    ]);


    // test that we only fetch the fields specified
    testAsyncMulti('collection - fetch, ' + idGeneration, [
      async function(test, expect) {
        var fetchId = await restrictedCollectionForFetchTest.insertAsync({
          field1: 1,
          field2: 1,
          field3: 1,
          field4: 1,
        },{
          returnServerResultPromise: true,
        });
        var fetchAllId = await restrictedCollectionForFetchAllTest.insertAsync({
          field1: 1,
          field2: 1,
          field3: 1,
          field4: 1,
        },{
          returnServerResultPromise: true,
        });
        await restrictedCollectionForFetchTest
          .updateAsync(
            fetchId,
            { $set: { updated: true } },
            {
              returnServerResultPromise: true,
            }
          )
          .catch(
            expect(function(err) {
              test.equal(
                err.reason,
                'Test: Fields in doc: _id,field1,field2,field3'
              );
            })
          );

        await restrictedCollectionForFetchTest
          .removeAsync(fetchId, {
            returnServerResultPromise: true,
          })
          .catch(
            expect(function(err) {
              test.equal(
                err.reason,
                'Test: Fields in doc: _id,field1,field2,field3'
              );
            })
          );

        await restrictedCollectionForFetchAllTest
          .updateAsync(
            fetchAllId,
            { $set: { updated: true } },
            {
              returnServerResultPromise: true,
            }
          )
          .catch(
            expect(function(err) {
              test.equal(
                err.reason,
                'Test: Fields in doc: _id,field1,field2,field3,field4'
              );
            })
          );
        await restrictedCollectionForFetchAllTest
          .removeAsync(fetchAllId, {
            returnServerResultPromise: true,
          })
          .catch(
            expect(function(err) {
              test.equal(
                err.reason,
                'Test: Fields in doc: _id,field1,field2,field3,field4'
              );
            })
          );
      },
    ]);

    (function() {
      testAsyncMulti('collection - restricted factories ' + idGeneration, [
        async function(test, expect) {
          await restrictedCollectionWithTransform.callClearMethod();
          test.equal(
            await restrictedCollectionWithTransform.find().countAsync(),
            0
          );
        },
        async function(test, expect) {
          var self = this;
          await restrictedCollectionWithTransform
            .insertAsync(
              {
                a: { foo: 'foo', bar: 'bar', baz: 'baz' },
              },
              {
                returnServerResultPromise: true,
              }
            )
            .then(
              expect(function(res) {
                test.isTrue(res);
                self.item1 = res;
              })
            );
          await restrictedCollectionWithTransform
            .insertAsync(
              {
                a: { foo: 'foo', bar: 'quux', baz: 'quux' },
                b: 'potato',
              },
              {
                returnServerResultPromise: true,
              }
            )
            .then(
              expect(function(res) {
                test.isTrue(res);
                self.item2 = res;
              })
            );
          await restrictedCollectionWithTransform
            .insertAsync(
              {
                a: { foo: 'adsfadf', bar: 'quux', baz: 'quux' },
                b: 'potato',
              },
              {
                returnServerResultPromise: true,
              }
            )
            .catch(
              expect(function(e, res) {
                test.isTrue(e);
              })
            );
          await restrictedCollectionWithTransform
            .insertAsync(
              {
                a: { foo: 'bar' },
                topLevelField: true,
              },
              {
                returnServerResultPromise: true,
              }
            )
            .then(
              expect(function(res) {
                test.isTrue(res);
                self.item3 = res;
              })
            );
        },
        async function(test, expect) {
          var self = this;
          // This should work, because there is an update allow for things with
          // topLevelField.
          await restrictedCollectionWithTransform
            .updateAsync(
              self.item3,
              { $set: { xxx: true } },
              {
                returnServerResultPromise: true,
              }
            )
            .then(
              expect(function(res) {
                test.equal(1, res);
              })
            );
        },
        async function(test, expect) {
          var self = this;
          test.equal(
            await restrictedCollectionWithTransform.findOneAsync(self.item1),
            {
              _id: self.item1,
              foo: 'foo',
              bar: 'bar',
              baz: 'baz',
            }
          );
          await restrictedCollectionWithTransform
            .removeAsync(self.item1, {
              returnServerResultPromise: true,
            })
            .then(
              expect(function(res) {
                test.isTrue(res);
              })
            );
          await restrictedCollectionWithTransform
            .removeAsync(self.item2, {
              returnServerResultPromise: true,
            })
            .catch(
              expect(function(e) {
                test.isTrue(e);
              })
            );
        },
      ]);
    })();

    testAsyncMulti('collection - insecure, ' + idGeneration, [
      async function(test, expect) {
        await insecureCollection.callClearMethod();
        test.equal(await insecureCollection.find().countAsync(), 0);
      },
      async function(test, expect) {
        let idThen;
        var id = await insecureCollection
          .insertAsync(
            { foo: 'bar' },
            {
              returnServerResultPromise: true,
            }
          )
          .then(async function(res) {
            idThen = res;
            test.equal(await insecureCollection.find(res).countAsync(), 1);
            test.equal((await insecureCollection.findOneAsync(res)).foo, 'bar');
            return res;
          });
        test.equal(idThen, id);
        test.equal(await insecureCollection.find(id).countAsync(), 1);
        test.equal((await insecureCollection.findOneAsync(id)).foo, 'bar');
      },
    ]);

    testAsyncMulti('collection - locked down, ' + idGeneration, [
      async function(test, expect) {
        await lockedDownCollection.callClearMethod();
        test.equal(await lockedDownCollection.find().countAsync(), 0);
      },
      async function(test, expect) {
        await lockedDownCollection
          .insertAsync(
            { foo: 'bar' },
            {
              returnServerResultPromise: true,
            }
          )
          .catch(async function(err, res) {
            test.equal(err.error, 403);
            test.equal(await lockedDownCollection.find().countAsync(), 0);
          });
      },
    ]);

    (function() {
      var collection = restrictedCollectionForUpdateOptionsTest;
      var id1, id2;
      testAsyncMulti('collection - update options, ' + idGeneration, [
        // init
        async function(test, expect) {
          await collection.callClearMethod().then(async function() {
            test.equal(await collection.find().countAsync(), 0);
          });
        },
        // put a few objects
        async function(test, expect) {
          var doc = { canInsert: true, canUpdate: true };
          id1 = await collection.insertAsync(doc);
          id2 = await collection.insertAsync(doc);
          await collection.insertAsync(doc);
          await collection
            .insertAsync(doc, { returnServerResultPromise: true })
            .then(async function(res) {
              test.equal(await collection.find().countAsync(), 4);
            });
        },
        // update by id
        async function(test, expect) {
          await collection
            .updateAsync(
              id1,
              { $set: { updated: true } },
              { returnServerResultPromise: true }
            )
            .then(async function(res) {
              test.equal(res, 1);
              test.equal(
                await collection.find({ updated: true }).countAsync(),
                1
              );
            });
        },
        // update by id in an object
        async function(test, expect) {
          await collection
            .updateAsync(
              { _id: id2 },
              { $set: { updated: true } },
              { returnServerResultPromise: true }
            )
            .then(async function(res) {
              test.equal(res, 1);
              test.equal(
                await collection.find({ updated: true }).countAsync(),
                2
              );
            });
        },
        // update with replacement operator not allowed, and has nice error.
        async function(test, expect) {
          await collection
            .updateAsync(
              { _id: id2 },
              { _id: id2, updated: true },
              { returnServerResultPromise: true }
            )
            .catch(async function(err) {
              test.equal(err.error, 403);
              test.matches(err.reason, /In a restricted/);
              // unchanged
              test.equal(
                await collection.find({ updated: true }).countAsync(),
                2
              );
            });
        },
        // upsert not allowed, and has nice error.
        async function(test, expect) {
          await collection
            .updateAsync(
              { _id: id2 },
              { $set: { upserted: true } },
              { upsert: true, returnServerResultPromise: true }
            )
            .catch(async function(err) {
              test.equal(err.error, 403);
              test.matches(err.reason, /in a restricted/);
              test.equal(
                await collection.find({ upserted: true }).countAsync(),
                0
              );
            });
        },
        // update with rename operator not allowed, and has nice error.
        async function(test, expect) {
          await collection
            .updateAsync(
              { _id: id2 },
              { $rename: { updated: 'asdf' } },
              { returnServerResultPromise: true }
            )
            .catch(async function(err) {
              test.equal(err.error, 403);
              test.matches(err.reason, /not allowed/);
              // unchanged
              test.equal(
                await collection.find({ updated: true }).countAsync(),
                2
              );
            });
        },
        // update method with a non-ID selector is not allowed
        async function(test, expect) {
          // We shouldn't even send the method...
          await test.throwsAsync(async function() {
            await collection.updateAsync(
              { updated: { $exists: false } },
              { $set: { updated: true } }
            );
          });
          // ... but if we did, the server would reject it too.
          await Meteor.callAsync(
            '/' + collection._name + '/updateAsync',
            { updated: { $exists: false } },
            { $set: { updated: true } }
          ).catch(async function(err, res) {
            test.equal(err.error, 403);
            // unchanged
            test.equal(
              await collection.find({ updated: true }).countAsync(),
              2
            );
          });
        },
        // make sure it doesn't think that {_id: 'foo', something: else} is ok.
        async function(test, expect) {
          await test.throwsAsync(async function() {
            await collection.updateAsync(
              { _id: id1, updated: { $exists: false } },
              { $set: { updated: true } }
            );
          });
        },
        // remove method with a non-ID selector is not allowed
        async function(test, expect) {
          // We shouldn't even send the method...
          await test.throwsAsync(async function() {
            await collection.removeAsync({ updated: true });
          });
          // ... but if we did, the server would reject it too.
          await Meteor.callAsync(
            '/' + collection._name + '/removeAsync',
            {
              updated: true,
            }
          ).catch(async function(err) {
            test.equal(err.error, 403);
            // unchanged
            test.equal(
              await collection.find({ updated: true }).countAsync(),
              2
            );
          });
        },
      ]);
    })();


    [restrictedCollectionDefaultInsecure, restrictedCollectionDefaultSecure].forEach(
      function(collection) {
        var canUpdateId, canRemoveId;

        testAsyncMulti('collection - ' + collection.unnoncedName, [
          // init
          async function(test, expect) {
            await collection.callClearMethod().then(async function() {
              test.equal(await collection.find().countAsync(), 0);
            });
          },

          // insert with no allows passing. request is denied.
          async function(test, expect) {
            await collection
              .insertAsync({}, { returnServerResultPromise: true })
              .catch(async function(err, res) {
                test.equal(err.error, 403);
                test.equal(await collection.find().countAsync(), 0);
              });
          },
          // insert with one allow and one deny. denied.
          async function(test, expect) {
            await collection
              .insertAsync(
                { canInsert: true, cantInsert: true },
                { returnServerResultPromise: true }
              )
              .catch(async function(err, res) {
                test.equal(err.error, 403);
                test.equal(await collection.find().countAsync(), 0);
              });
          },
          // insert with one allow and other deny. denied.
          async function(test, expect) {
            await collection
              .insertAsync(
                { canInsert: true, _id: Random.id() },
                { returnServerResultPromise: true }
              )
              .catch(async function(err) {
                test.equal(err.error, 403);
                test.equal(await collection.find().countAsync(), 0);
              });
          },
          // insert one allow passes. allowed.
          async function(test, expect) {
            await collection
              .insertAsync(
                { canInsert: true },
                { returnServerResultPromise: true }
              )
              .then(async function(err, res) {
                test.equal(await collection.find().countAsync(), 1);
              });
          },
          // insert other allow passes. allowed.
          // includes canUpdate for later.
          async function(test, expect) {
            canUpdateId = await collection
              .insertAsync(
                { canInsert2: true, canUpdate: true },
                { returnServerResultPromise: true }
              )
              .then(async function(res) {
                test.equal(await collection.find().countAsync(), 2);
                return res;
              });
          },
          // yet a third insert executes. this one has canRemove and
          // cantRemove set for later.
          async function(test, expect) {
            canRemoveId = await collection
              .insertAsync(
                { canInsert: true, canRemove: true, cantRemove: true },
                { returnServerResultPromise: true }
              )
              .then(async function(res) {
                test.equal(await collection.find().countAsync(), 3);
                return res;
              });
          },

          // can't update with a non-operator mutation
          async function(test, expect) {
            await collection
              .updateAsync(
                canUpdateId,
                { newObject: 1 },
                { returnServerResultPromise: true }
              )
              .catch(async function(err, res) {
                test.equal(err.error, 403);
                test.equal(await collection.find().countAsync(), 3);
              });
          },

          // updating dotted fields works as if we are changing their
          // top part
          async function(test, expect) {
            await collection
              .updateAsync(
                canUpdateId,
                { $set: { 'dotted.field': 1 } },
                { returnServerResultPromise: true }
              )
              .then(async function(res) {
                test.equal(res, 1);
                test.equal(
                  (await collection.findOneAsync(canUpdateId)).dotted.field,
                  1
                );
              });
          },
          async function(test, expect) {
            await collection
              .updateAsync(
                canUpdateId,
                { $set: { 'verySecret.field': 1 } },
                { returnServerResultPromise: true }
              )
              .catch(async function(err, res) {
                test.equal(err.error, 403);
                test.equal(
                  await collection
                    .find({ verySecret: { $exists: true } })
                    .countAsync(),
                  0
                );
              });
          },

          // update doesn't do anything if no docs match
          async function(test, expect) {
            await collection
              .updateAsync(
                "doesn't exist",
                { $set: { updated: true } },
                { returnServerResultPromise: true }
              )
              .then(async function(res) {
                console.log('res', res);
                test.equal(res, 0);
                // nothing has changed
                test.equal(await collection.find().countAsync(), 3);
                test.equal(
                  await collection.find({ updated: true }).countAsync(),
                  0
                );
              });
          },
          // update fails when access is denied trying to set `verySecret`
          async function(test, expect) {
            await collection
              .updateAsync(
                canUpdateId,
                { $set: { verySecret: true } },
                { returnServerResultPromise: true }
              )
              .catch(async function(err) {
                test.equal(err.error, 403);
                // nothing has changed
                test.equal(await collection.find().countAsync(), 3);
                test.equal(
                  await collection.find({ updated: true }).countAsync(),
                  0
                );
              });
          },
          // update fails when trying to set two fields, one of which is
          // `verySecret`
          async function(test, expect) {
            await collection
              .updateAsync(
                canUpdateId,
                { $set: { updated: true, verySecret: true } },
                { returnServerResultPromise: true }
              )
              .catch(async function(err, res) {
                test.equal(err.error, 403);
                // nothing has changed
                test.equal(await collection.find().countAsync(), 3);
                test.equal(
                  await collection.find({ updated: true }).countAsync(),
                  0
                );
              });
          },
          // update fails when trying to modify docs that don't
          // have `canUpdate` set
          async function(test, expect) {
            await collection
              .updateAsync(
                canRemoveId,
                { $set: { updated: true } },
                { returnServerResultPromise: true }
              )
              .catch(async function(err, res) {
                test.equal(err.error, 403);
                // nothing has changed
                test.equal(await collection.find().countAsync(), 3);
                test.equal(
                  await collection.find({ updated: true }).countAsync(),
                  0
                );
              });
          },
          // update executes when it should
          async function(test, expect) {
            await collection
              .updateAsync(
                canUpdateId,
                { $set: { updated: true } },
                { returnServerResultPromise: true }
              )
              .then(async function(res) {
                test.equal(res, 1);
                test.equal(
                  await collection.find({ updated: true }).countAsync(),
                  1
                );
              });
          },

          // remove fails when trying to modify a doc with no `canRemove` set
          async function(test, expect) {
            await collection
              .removeAsync(canUpdateId, { returnServerResultPromise: true })
              .catch(async function(err, res) {
                test.equal(err.error, 403);
                // nothing has changed
                test.equal(await collection.find().countAsync(), 3);
              });
          },
          // remove fails when trying to modify an doc with `cantRemove`
          // set
          async function(test, expect) {
            await collection
              .removeAsync(canRemoveId, { returnServerResultPromise: true })
              .catch(async function(err, res) {
                test.equal(err.error, 403);
                // nothing has changed
                test.equal(await collection.find().countAsync(), 3);
              });
          },

          // update the doc to remove cantRemove.
          async function(test, expect) {
            await collection
              .updateAsync(
                canRemoveId,
                { $set: { cantRemove: false, canUpdate2: true } },
                { returnServerResultPromise: true }
              )
              .then(async function(res) {
                test.equal(res, 1);
                test.equal(
                  await collection.find({ cantRemove: true }).countAsync(),
                  0
                );
              });
          },

          // now remove can remove it.
          async function(test, expect) {
            await collection
              .removeAsync(canRemoveId, { returnServerResultPromise: true })
              .then(async function(res) {
                test.equal(res, 1);
                // successfully removed
                test.equal(await collection.find().countAsync(), 2);
              });
          },

          // try to remove a doc that doesn't exist. see we remove no docs.
          async function(test, expect) {
            await collection
              .removeAsync('some-random-id-that-never-matches', {
                returnServerResultPromise: true,
              })
              .then(async function(res) {
                test.equal(res, 0);
                // nothing removed
                test.equal(await collection.find().countAsync(), 2);
              });
          },

          // methods can still bypass restrictions
          async function(test, expect) {
            await collection.callClearMethod().then(async function(err, res) {
              // successfully removed
              test.equal(await collection.find().countAsync(), 0);
            });
          },
        ]);
      }
    );
    testAsyncMulti(
      'collection - allow/deny transform must return object, ' + idGeneration,
      [
        async function(test, expect) {
          await restrictedCollectionForInvalidTransformTest
            .insertAsync({}, { returnServerResultPromise: true })
            .catch(function(err) {
              test.isTrue(err);
            });
        },
      ]
    );
    testAsyncMulti(
      'collection - restricted collection allows client-side id, ' +
      idGeneration,
      [
        async function(test, expect) {
          var self = this;
          self.id = Random.id();
          await restrictedCollectionForClientIdTest
            .insertAsync({ _id: self.id }, { returnServerResultPromise: true })
            .then(async function(res) {
              test.equal(res, self.id);
              test.equal(
                await restrictedCollectionForClientIdTest.findOneAsync(self.id),
                {
                  _id: self.id,
                }
              );
            });
        },
      ]
    );
  });  // end idGeneration loop
}  // end if isClient



// A few simple server-only tests which don't need to coordinate collections
// with the client..
if (Meteor.isServer) {
  Tinytest.add("collection - allow and deny validate options", function (test) {
    var collection = new Mongo.Collection(null);

    test.throws(function () {
      collection.allow({invalidOption: true});
    });
    test.throws(function () {
      collection.deny({invalidOption: true});
    });

    ['insert', 'update', 'remove', 'fetch'].forEach(function (key) {
      var options = {};
      options[key] = true;
      test.throws(function () {
        collection.allow(options);
      });
      test.throws(function () {
        collection.deny(options);
      });
    });

    ['insert', 'update', 'remove'].forEach(function (key) {
      var options = {};
      options[key] = false;
      test.throws(function () {
        collection.allow(options);
      });
      test.throws(function () {
        collection.deny(options);
      });
    });

    ['insert', 'update', 'remove'].forEach(function (key) {
      var options = {};
      options[key] = undefined;
      test.throws(function () {
        collection.allow(options);
      });
      test.throws(function () {
        collection.deny(options);
      });
    });

    ['insert', 'update', 'remove'].forEach(function (key) {
      var options = {};
      options[key] = ['an array']; // this should be a function, not an array
      test.throws(function () {
        collection.allow(options);
      });
      test.throws(function () {
        collection.deny(options);
      });
    });

    test.throws(function () {
      collection.allow({fetch: function () {}}); // this should be an array
    });
  });

  Tinytest.add("collection - calling allow restricts", function (test) {
    var collection = new Mongo.Collection(null);
    test.equal(collection._restricted, false);
    collection.allow({
      insert: function() {}
    });
    test.equal(collection._restricted, true);
  });

  Tinytest.add("collection - global insecure", function (test) {
    // note: This test alters the global insecure status, by sneakily hacking
    // the global Package object!
    var insecurePackage = Package.insecure;

    Package.insecure = {};
    var collection = new Mongo.Collection(null);
    test.equal(collection._isInsecure(), true);

    Package.insecure = undefined;
    test.equal(collection._isInsecure(), false);

    delete Package.insecure;
    test.equal(collection._isInsecure(), false);

    collection._insecure = true;
    test.equal(collection._isInsecure(), true);

    if (insecurePackage)
      Package.insecure = insecurePackage;
    else
      delete Package.insecure;
  });
}

var AllowAsyncValidateCollection;

Tinytest.addAsync(
  "collection - validate server operations when using allow-deny rules on the client",
  async function (test) {
    AllowAsyncValidateCollection =
      AllowAsyncValidateCollection ||
      new Mongo.Collection(`allowdeny-async-validation`);
    if (Meteor.isServer) {
      await AllowAsyncValidateCollection.removeAsync();
    }
    AllowAsyncValidateCollection.allow({
      insertAsync() {
        return true;
      },
      insert() {
        return true;
      },
      updateAsync() {
        return true;
      },
      update() {
        return true;
      },
      removeAsync() {
        return true;
      },
      remove() {
        return true;
      },
    });

    if (Meteor.isClient) {
      /* sync tests */
      var id = await new Promise((resolve, reject) => {
        AllowAsyncValidateCollection.insert({ num: 1 }, (error, result) =>
          error ? reject(error) : resolve(result)
        );
      });
      console.log('id', id);
      await new Promise((resolve, reject) => {
        AllowAsyncValidateCollection.update(
          id,
          { $set: { num: 11 } },
          (error, result) => (error ? reject(error) : resolve(result))
        );
      });
      await new Promise((resolve, reject) => {
        AllowAsyncValidateCollection.remove(id, (error, result) =>
          error ? reject(error) : resolve(result)
        );
      });

      /* async tests */
      id = await AllowAsyncValidateCollection.insertAsync({ num: 2 });
      await AllowAsyncValidateCollection.updateAsync(id, { $set: { num: 22 } });
      await AllowAsyncValidateCollection.removeAsync(id);
    }
  }
);

function configAllAllowDeny(collection, configType = 'allow', enabled, isAsync = true) {
  const handler = isAsync
    ? {
      async insertAsync(selector, doc) {
        if (doc.force) return configType === 'allow';
        await Meteor._sleepForMs(100);
        return enabled;
      },
      async updateAsync() {
        await Meteor._sleepForMs(100);
        return enabled;
      },
      async removeAsync() {
        await Meteor._sleepForMs(100);
        return enabled;
      },
    }
    : {
      insert(selector, doc) {
        if (doc.force) return configType === 'allow';
        return enabled;
      },
      update() {
        return enabled;
      },
      remove() {
        return enabled;
      },
    };

  collection[configType](handler);
}

async function runAllExpect(test, collection, allow, isAsync = true) {
  let id;

  const resolveSyncCallback = (resolve, reject) =>
    (error, result) => {
    if (error) {
      reject(error);
      return;
    }
    resolve(result);
  };
  const methods = isAsync
    ? {
      insert: async (doc) => await collection.insertAsync(doc),
      update: async (id, modifier) => await collection.updateAsync(id, modifier),
      remove: async (id) => await collection.removeAsync(id),
    }
    : {
      insert: (doc) =>
        new Promise((resolve, reject) =>
          collection.insert(doc, resolveSyncCallback(resolve, reject))
        ),
      update: (id, modifier) =>
        new Promise((resolve, reject) =>
          collection.update(id, modifier, resolveSyncCallback(resolve, reject))
        ),
      remove: (id) =>
        new Promise((resolve, reject) =>
          collection.remove(id, resolveSyncCallback(resolve, reject))
        ),
    };

  try {
    id = await methods.insert({ num: 2 });
    test.isTrue(allow);
  } catch (e) {
    test.isTrue(!allow);
  }

  try {
    id = await methods.insert({ force: true });
    await methods.update(id, { $set: { num: 22 } });
    test.isTrue(allow);
  } catch (e) {
    test.isTrue(!allow);
  }

  try {
    id = await methods.insert({ force: true });
    await methods.remove(id);
    test.isTrue(allow);
  } catch (e) {
    test.isTrue(!allow);
  }
}

const AllowDenyRulesCollections = {};

function createAllowDenyRulesTest(collections, isAsync = true) {
  return [
    async function (test) {
      const collectionName = `allowdeny-${isAsync ? "async" : "sync"}-rules-noRules`;
      collections.noRules =
        collections.noRules || new Mongo.Collection(collectionName);
      if (Meteor.isServer) {
        await collections.noRules.removeAsync();
      }

      if (Meteor.isClient) {
        await runAllExpect(test, collections.noRules, collections.noRules._isInsecure(), isAsync);
      }
    },
    async function (test) {
      const collectionName = `allowdeny-${isAsync ? "async" : "sync"}-rules-allowed`;
      collections.allowed =
        collections.allowed || new Mongo.Collection(collectionName);
      if (Meteor.isServer) {
        await collections.allowed.removeAsync();
      }

      configAllAllowDeny(collections.allowed, 'allow', true, isAsync);
      if (Meteor.isClient) {
        await runAllExpect(test, collections.allowed, true, isAsync);
      }
    },
    async function (test) {
      const collectionName = `allowdeny-${isAsync ? "async" : "sync"}-rules-notAllowed`;
      collections.notAllowed =
        collections.notAllowed || new Mongo.Collection(collectionName);
      if (Meteor.isServer) {
        await collections.notAllowed.removeAsync();
      }

      configAllAllowDeny(collections.notAllowed, 'allow', false, isAsync);
      if (Meteor.isClient) {
        await runAllExpect(test, collections.notAllowed, false, isAsync);
      }
    },
    async function (test) {
      const collectionName = `allowdeny-${isAsync ? "async" : "sync"}-rules-denied`;
      collections.denied =
        collections.denied || new Mongo.Collection(collectionName);
      if (Meteor.isServer) {
        await collections.denied.removeAsync();
      }

      configAllAllowDeny(collections.denied, 'deny', true, isAsync);
      if (Meteor.isClient) {
        await runAllExpect(test, collections.denied, false, isAsync);
      }
    },
    async function (test) {
      const collectionName = `allowdeny-${isAsync ? "async" : "sync"}-rules-allowThenDeny`;
      collections.allowThenDeny =
        collections.allowThenDeny || new Mongo.Collection(collectionName);
      if (Meteor.isServer) {
        await collections.allowThenDeny.removeAsync();
      }

      configAllAllowDeny(collections.allowThenDeny, 'allow', true, isAsync);
      configAllAllowDeny(collections.allowThenDeny, 'deny', true, isAsync);
      if (Meteor.isClient) {
        await runAllExpect(test, collections.allowThenDeny, false, isAsync);
      }
    },
    async function (test) {
      const collectionName = `allowdeny-${isAsync ? "async" : "sync"}-rules-allowThenNotDenied`;
      collections.allowThenNotDenied =
        collections.allowThenNotDenied || new Mongo.Collection(collectionName);
      if (Meteor.isServer) {
        await collections.allowThenNotDenied.removeAsync();
      }

      configAllAllowDeny(collections.allowThenNotDenied, 'allow', true, isAsync);
      configAllAllowDeny(collections.allowThenNotDenied, 'deny', false, isAsync);
      if (Meteor.isClient) {
        await runAllExpect(test, collections.allowThenNotDenied, true, isAsync);
      }
    },
  ];
}

testAsyncMulti("collection - async definitions on allow/deny rules", createAllowDenyRulesTest(AllowDenyRulesCollections, true));

testAsyncMulti("collection - sync definitions on allow/deny rules", createAllowDenyRulesTest(AllowDenyRulesCollections, false));
