// Copyright IBM Corp. 2015,2019. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

// This test written in mocha+should.js
'use strict';

/* global getSchema:false */
const should = require('./init.js');
const async = require('async');
let db, User, options, filter;

describe('crud-with-options', function() {
  before(function(done) {
    db = getSchema();
    User = db.define('User', {
      id: {type: Number, id: true},
      seq: {type: Number, index: true},
      name: {type: String, index: true, sort: true},
      email: {type: String, index: true},
      birthday: {type: Date, index: true},
      role: {type: String, index: true},
      order: {type: Number, index: true, sort: true},
      vip: {type: Boolean},
      address: {type: {city: String, area: String}},
    });
    options = {};
    filter = {fields: ['name', 'id']};

    db.automigrate(['User'], done);
  });

  describe('findById', function() {
    before(function(done) {
      User.destroyAll(done);
    });

    it('should allow findById(id, options, cb)', function(done) {
      User.findById(1, options, function(err, u) {
        should.not.exist(u);
        should.not.exist(err);
        done();
      });
    });

    it('should allow findById(id, filter, cb)', function(done) {
      User.findById(1, filter, function(err, u) {
        should.not.exist(u);
        should.not.exist(err);
        done();
      });
    });

    it('should allow findById(id)', function() {
      User.findById(1);
    });

    it('should allow findById(id, filter)', function() {
      User.findById(1, filter);
    });

    it('should allow findById(id, options)', function() {
      User.findById(1, options);
    });

    it('should allow findById(id, filter, options)', function() {
      User.findById(1, filter, options);
    });

    it('should throw when invalid filter are provided for findById',
      function(done) {
        (function() {
          User.findById(1, '123', function(err, u) {
          });
        }).should.throw('The filter argument must be an object');
        done();
      });

    it('should throw when invalid options are provided for findById',
      function(done) {
        (function() {
          User.findById(1, filter, '123', function(err, u) {
          });
        }).should.throw('The options argument must be an object');
        done();
      });

    it('should report an invalid id via callback for findById',
      function(done) {
        User.findById(undefined, {}, function(err, u) {
          err.should.be.eql(
            new Error('Model::findById requires the id argument'),
          );
          done();
        });
      });

    it('should allow findById(id, filter, cb) for a matching id',
      function(done) {
        User.create({name: 'x', email: 'x@y.com'}, function(err, u) {
          should.not.exist(err);
          should.exist(u.id);
          User.findById(u.id, filter, function(err, u) {
            should.exist(u);
            should.not.exist(err);
            u.should.be.an.instanceOf(User);
            u.should.have.property('name', 'x');
            u.should.have.property('email', undefined);
            done();
          });
        });
      });

    it('should allow findById(id, options, cb) for a matching id',
      function(done) {
        User.create({name: 'y', email: 'y@y.com'}, function(err, u) {
          should.not.exist(err);
          should.exist(u.id);
          User.findById(u.id, options, function(err, u) {
            should.exist(u);
            should.not.exist(err);
            u.should.be.an.instanceOf(User);
            u.should.have.property('name', 'y');
            u.should.have.property('email', 'y@y.com');
            done();
          });
        });
      });

    it('should allow findById(id, filter, options, cb) for a matching id',
      function(done) {
        User.create({name: 'z', email: 'z@y.com'}, function(err, u) {
          should.not.exist(err);
          should.exist(u.id);
          User.findById(u.id, filter, options, function(err, u) {
            should.exist(u);
            should.not.exist(err);
            u.should.be.an.instanceOf(User);
            u.should.have.property('name', 'z');
            u.should.have.property('email', undefined);
            done();
          });
        });
      });

    it('should allow promise-style findById',
      function(done) {
        User.create({id: 15, name: 'w', email: 'w@y.com'}).then(function(u) {
          should.exist(u.id);
          return User.findById(u.id).then(function(u) {
            should.exist(u);
            u.should.be.an.instanceOf(User);
            u.should.have.property('name', 'w');
            u.should.have.property('email', 'w@y.com');
            return u;
          });
        }).then(function(u) {
          should.exist(u);
          should.exist(u.id);
          return User.findById(u.id, filter).then(function(u) {
            should.exist(u);
            u.should.be.an.instanceOf(User);
            u.should.have.property('name', 'w');
            u.should.have.property('email', undefined);
            return u;
          });
        }).then(function(u) {
          should.exist(u);
          should.exist(u.id);
          return User.findById(u.id, options).then(function(u) {
            should.exist(u);
            u.should.be.an.instanceOf(User);
            u.should.have.property('name', 'w');
            u.should.have.property('email', 'w@y.com');
            return u;
          });
        }).then(function(u) {
          should.exist(u);
          should.exist(u.id);
          return User.findById(u.id, filter, options).then(function(u) {
            should.exist(u);
            u.should.be.an.instanceOf(User);
            u.should.have.property('name', 'w');
            u.should.have.property('email', undefined);
            done();
          });
        }).catch(function(err) {
          done(err);
        });
      });
  });

  describe('findByIds', function() {
    before(function(done) {
      const people = [
        {id: 1, name: 'a', vip: true},
        {id: 2, name: 'b'},
        {id: 3, name: 'c'},
        {id: 4, name: 'd', vip: true},
        {id: 5, name: 'e'},
        {id: 6, name: 'f'},
      ];
      // Use automigrate so that serial keys are 1-6
      db.automigrate(['User'], function(err) {
        User.create(people, options, function(err, users) {
          done();
        });
      });
    });

    it('should allow findByIds(ids, cb)', function(done) {
      User.findByIds([3, 2, 1], function(err, users) {
        should.exist(users);
        should.not.exist(err);
        const names = users.map(function(u) { return u.name; });
        names.should.eql(['c', 'b', 'a']);
        done();
      });
    });

    it('should allow findByIds(ids, filter, options, cb)',
      function(done) {
        User.findByIds([4, 3, 2, 1],
          {where: {vip: true}}, options, function(err, users) {
            should.exist(users);
            should.not.exist(err);
            const names = users.map(function(u) {
              return u.name;
            });
            names.should.eql(['d', 'a']);
            done();
          });
      });
  });

  describe('find', function() {
    before(seed);

    it('should allow find(cb)', function(done) {
      User.find(function(err, users) {
        should.exists(users);
        should.not.exists(err);
        users.should.have.lengthOf(6);
        done();
      });
    });

    it('should allow find(filter, cb)', function(done) {
      User.find({limit: 3}, function(err, users) {
        should.exists(users);
        should.not.exists(err);
        users.should.have.lengthOf(3);
        done();
      });
    });

    it('should allow find(filter, options, cb)', function(done) {
      User.find({}, options, function(err, users) {
        should.exists(users);
        should.not.exists(err);
        users.should.have.lengthOf(6);
        done();
      });
    });

    it('should allow find(filter, options)', function() {
      User.find({limit: 3}, options);
    });

    it('should allow find(filter)', function() {
      User.find({limit: 3});
    });

    it('should skip trailing undefined args', function(done) {
      User.find({limit: 3}, function(err, users) {
        should.exists(users);
        should.not.exists(err);
        users.should.have.lengthOf(3);
        done();
      }, undefined, undefined);
    });

    it('should throw on an invalid query arg', function() {
      (function() {
        User.find('invalid query', function(err, users) {
          // noop
        });
      }).should.throw('The query argument must be an object');
    });

    it('should throw on an invalid options arg', function() {
      (function() {
        User.find({limit: 3}, 'invalid option', function(err, users) {
          // noop
        });
      }).should.throw('The options argument must be an object');
    });

    it('should throw on an invalid cb arg', function() {
      (function() {
        User.find({limit: 3}, {}, 'invalid cb');
      }).should.throw('The cb argument must be a function');
    });
  });

  describe('count', function() {
    before(seed);

    it('should allow count(cb)', function(done) {
      User.count(function(err, n) {
        should.not.exist(err);
        should.exist(n);
        n.should.equal(6);
        done();
      });
    });

    it('should allow count(where, cb)', function(done) {
      User.count({role: 'lead'}, function(err, n) {
        should.not.exist(err);
        should.exist(n);
        n.should.equal(2);
        done();
      });
    });

    it('should allow count(where, options, cb)', function(done) {
      User.count({role: 'lead'}, options, function(err, n) {
        should.not.exist(err);
        should.exist(n);
        n.should.equal(2);
        done();
      });
    });
  });

  describe('findOne', function() {
    before(seed);

    it('should allow findOne(cb)', function(done) {
      User.find({order: 'id'}, function(err, users) {
        User.findOne(function(e, u) {
          should.not.exist(e);
          should.exist(u);
          u.id.toString().should.equal(users[0].id.toString());
          done();
        });
      });
    });

    it('should allow findOne(filter, options, cb)', function(done) {
      User.findOne({order: 'order'}, options, function(e, u) {
        should.not.exist(e);
        should.exist(u);
        u.order.should.equal(1);
        u.name.should.equal('Paul McCartney');
        done();
      });
    });

    it('should allow findOne(filter, cb)', function(done) {
      User.findOne({order: 'order'}, function(e, u) {
        should.not.exist(e);
        should.exist(u);
        u.order.should.equal(1);
        u.name.should.equal('Paul McCartney');
        done();
      });
    });

    it('should allow trailing undefined args', function(done) {
      User.findOne({order: 'order'}, function(e, u) {
        should.not.exist(e);
        should.exist(u);
        u.order.should.equal(1);
        u.name.should.equal('Paul McCartney');
        done();
      }, undefined);
    });
  });

  describe('exists', function() {
    before(seed);

    it('should allow exists(id, cb)', function(done) {
      User.findOne(function(e, u) {
        User.exists(u.id, function(err, exists) {
          should.not.exist(err);
          should.exist(exists);
          exists.should.be.ok;
          done();
        });
      });
    });

    it('should allow exists(id, options, cb)', function(done) {
      User.destroyAll(function() {
        User.exists(42, options, function(err, exists) {
          should.not.exist(err);
          exists.should.not.be.ok;
          done();
        });
      });
    });
  });

  describe('save', function() {
    it('should allow save(options, cb)', function(done) {
      const options = {foo: 'bar'};
      let opts;

      User.observe('after save', function(ctx, next) {
        opts = ctx.options;
        next();
      });

      const u = new User();
      u.save(options, function(err) {
        should.not.exist(err);
        options.should.equal(opts);
        done();
      });
    });
  });

  describe('destroyAll with options', function() {
    beforeEach(seed);

    it('should allow destroyAll(where, options, cb)', function(done) {
      User.destroyAll({name: 'John Lennon'}, options, function(err) {
        should.not.exist(err);
        User.find({where: {name: 'John Lennon'}}, function(err, data) {
          should.not.exist(err);
          data.length.should.equal(0);
          User.find({where: {name: 'Paul McCartney'}}, function(err, data) {
            should.not.exist(err);
            data.length.should.equal(1);
            done();
          });
        });
      });
    });

    it('should allow destroyAll(where, cb)', function(done) {
      User.destroyAll({name: 'John Lennon'}, function(err) {
        should.not.exist(err);
        User.find({where: {name: 'John Lennon'}}, function(err, data) {
          should.not.exist(err);
          data.length.should.equal(0);
          User.find({where: {name: 'Paul McCartney'}}, function(err, data) {
            should.not.exist(err);
            data.length.should.equal(1);
            done();
          });
        });
      });
    });

    it('should allow destroyAll(cb)', function(done) {
      User.destroyAll(function(err) {
        should.not.exist(err);
        User.find({where: {name: 'John Lennon'}}, function(err, data) {
          should.not.exist(err);
          data.length.should.equal(0);
          User.find({where: {name: 'Paul McCartney'}}, function(err, data) {
            should.not.exist(err);
            data.length.should.equal(0);
            done();
          });
        });
      });
    });
  });

  describe('updateAll ', function() {
    beforeEach(seed);

    it('should allow updateAll(where, data, cb)', function(done) {
      User.update({name: 'John Lennon'}, {name: 'John Smith'}, function(err) {
        should.not.exist(err);
        User.find({where: {name: 'John Lennon'}}, function(err, data) {
          should.not.exist(err);
          data.length.should.equal(0);
          User.find({where: {name: 'John Smith'}}, function(err, data) {
            should.not.exist(err);
            data.length.should.equal(1);
            done();
          });
        });
      });
    });

    it('should allow updateAll(where, data, options, cb)', function(done) {
      User.update({name: 'John Lennon'}, {name: 'John Smith'}, options,
        function(err) {
          should.not.exist(err);
          User.find({where: {name: 'John Lennon'}}, function(err, data) {
            should.not.exist(err);
            data.length.should.equal(0);
            User.find({where: {name: 'John Smith'}}, function(err, data) {
              should.not.exist(err);
              data.length.should.equal(1);
              done();
            });
          });
        });
    });

    it('should allow updateAll(data, cb)', function(done) {
      User.update({name: 'John Smith'}, function() {
        User.find({where: {name: 'John Lennon'}}, function(err, data) {
          should.not.exist(err);
          data.length.should.equal(0);
          User.find({where: {name: 'John Smith'}}, function(err, data) {
            should.not.exist(err);
            data.length.should.equal(6);
            done();
          });
        });
      });
    });
  });

  describe('updateAttributes', function() {
    beforeEach(seed);
    it('preserves document properties not modified by the patch', function() {
      return User.findOne({where: {name: 'John Lennon'}})
        .then(function(user) {
          return user.updateAttributes({address: {city: 'Volos'}});
        })
        .then(function() {
          return User.findOne({where: {name: 'John Lennon'}}); // retrieve the user again from the db
        })
        .then(function(updatedUser) {
          updatedUser.address.city.should.equal('Volos');
          should(updatedUser.address.area).not.be.exactly(null);
          should(updatedUser.address.area).be.undefined();
        });
    });
  });
});

describe('upsertWithWhere', function() {
  beforeEach(seed);
  it('rejects upsertWithWhere (options,cb)', function(done) {
    try {
      User.upsertWithWhere({}, function(err) {
        if (err) return done(err);
      });
    } catch (ex) {
      ex.message.should.equal('The data argument must be an object');
      done();
    }
  });

  it('rejects upsertWithWhere (cb)', function(done) {
    try {
      User.upsertWithWhere(function(err) {
        if (err) return done(err);
      });
    } catch (ex) {
      ex.message.should.equal('The where argument must be an object');
      done();
    }
  });

  it('allows upsertWithWhere by accepting where,data and cb as arguments', function(done) {
    User.upsertWithWhere({name: 'John Lennon'}, {name: 'John Smith'}, function(err) {
      if (err) return done(err);
      User.find({where: {name: 'John Lennon'}}, function(err, data) {
        if (err) return done(err);
        data.length.should.equal(0);
        User.find({where: {name: 'John Smith'}}, function(err, data) {
          if (err) return done(err);
          data.length.should.equal(1);
          data[0].name.should.equal('John Smith');
          data[0].email.should.equal('john@b3atl3s.co.uk');
          data[0].role.should.equal('lead');
          data[0].order.should.equal(2);
          data[0].vip.should.equal(true);
          done();
        });
      });
    });
  });

  it('allows upsertWithWhere by accepting where, data, options, and cb as arguments', function(done) {
    options = {};
    User.upsertWithWhere({name: 'John Lennon'}, {name: 'John Smith'}, options, function(err) {
      if (err) return done(err);
      User.find({where: {name: 'John Smith'}}, function(err, data) {
        if (err) return done(err);
        data.length.should.equal(1);
        data[0].name.should.equal('John Smith');
        data[0].seq.should.equal(0);
        data[0].email.should.equal('john@b3atl3s.co.uk');
        data[0].role.should.equal('lead');
        data[0].order.should.equal(2);
        data[0].vip.should.equal(true);
        done();
      });
    });
  });
});

function seed(done) {
  const beatles = [
    {
      id: 0,
      seq: 0,
      name: 'John Lennon',
      email: 'john@b3atl3s.co.uk',
      role: 'lead',
      birthday: new Date('1980-12-08'),
      order: 2,
      vip: true,
    },
    {
      id: 1,
      seq: 1,
      name: 'Paul McCartney',
      email: 'paul@b3atl3s.co.uk',
      role: 'lead',
      birthday: new Date('1942-06-18'),
      order: 1,
      vip: true,
    },
    {id: 2, seq: 2, name: 'George Harrison', order: 5, vip: false},
    {id: 3, seq: 3, name: 'Ringo Starr', order: 6, vip: false},
    {id: 4, seq: 4, name: 'Pete Best', order: 4},
    {id: 5, seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true},
  ];

  async.series([
    User.destroyAll.bind(User),
    function(cb) {
      async.each(beatles, User.create.bind(User), cb);
    },
  ], done);
}