// Copyright IBM Corp. 2011,2016. 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

'use strict';

const Schema = require('../index').Schema;
const Text = Schema.Text;

let nbSchemaRequests = 0;

let batch;
let schemaName;

function it(name, cases) {
  batch[schemaName][name] = cases;
}

function skip(name) {
  delete batch[schemaName][name];
}

module.exports = function testSchema(exportCasesHere, dataSource) {
  batch = exportCasesHere;
  schemaName = dataSource.name;
  if (dataSource.name.match(/^\/.*\/test\/\.\.$/)) {
    schemaName = schemaName.split('/').slice(-3).shift();
  }
  let start;

  batch['should connect to database'] = function(test) {
    start = Date.now();
    if (dataSource.connected) return test.done();
    dataSource.on('connected', test.done);
  };

  dataSource.log = function(a) {
    console.log(a);
    nbSchemaRequests++;
  };

  batch[schemaName] = {};

  testOrm(dataSource);

  batch['all tests done'] = function(test) {
    test.done();
    process.nextTick(allTestsDone);
  };

  function allTestsDone() {
    // dataSource.disconnect();
    console.log('Test done in %dms\n', Date.now() - start);
  }
};

Object.defineProperty(module.exports, 'it', {
  writable: true,
  enumerable: false,
  configurable: true,
  value: it,
});

Object.defineProperty(module.exports, 'skip', {
  writable: true,
  enumerable: false,
  configurable: true,
  value: skip,
});

function clearAndCreate(model, data, callback) {
  const createdItems = [];
  model.destroyAll(function() {
    nextItem(null, null);
  });

  let itemIndex = 0;

  function nextItem(err, lastItem) {
    if (lastItem !== null) {
      createdItems.push(lastItem);
    }
    if (itemIndex >= data.length) {
      callback(createdItems);
      return;
    }
    model.create(data[itemIndex], nextItem);
    itemIndex++;
  }
}

/* eslint-disable mocha/handle-done-callback */
function testOrm(dataSource) {
  const requestsAreCounted = dataSource.name !== 'mongodb';

  let Post, User, Passport, Log, Dog;

  it('should define class', function(test) {
    User = dataSource.define('User', {
      name: {type: String, index: true},
      email: {type: String, index: true},
      bio: Text,
      approved: Boolean,
      joinedAt: Date,
      age: Number,
      passwd: {type: String, index: true},
    });

    Dog = dataSource.define('Dog', {
      name: {type: String, limit: 64, allowNull: false},
    });

    Log = dataSource.define('Log', {
      ownerId: {type: Number, allowNull: true},
      name: {type: String, limit: 64, allowNull: false},
    });

    Log.belongsTo(Dog, {as: 'owner', foreignKey: 'ownerId'});

    dataSource.extendModel('User', {
      settings: {type: Schema.JSON},
      extra: Object,
    });

    const newuser = new User({settings: {hey: 'you'}});
    test.ok(newuser.settings);

    Post = dataSource.define('Post', {
      title: {type: String, length: 255, index: true},
      subject: {type: String},
      content: {type: Text},
      date: {type: Date, default: function() {
        return new Date;
      }, index: true},
      published: {type: Boolean, default: false, index: true},
      likes: [],
      related: [RelatedPost],
    }, {table: 'posts'});

    function RelatedPost() {
    }

    RelatedPost.prototype.someMethod = function() {
      return this.parent;
    };

    Post.validateAsync('title', function(err, done) {
      process.nextTick(done);
    });

    User.hasMany(Post, {as: 'posts', foreignKey: 'userId'});
    // creates instance methods:
    // user.posts(conds)
    // user.posts.build(data) // like new Post({userId: user.id});
    // user.posts.create(data) // build and save
    // user.posts.find

    // User.hasOne('latestPost', {model: Post, foreignKey: 'postId'});

    // User.hasOne(Post,    {as: 'latestPost', foreignKey: 'latestPostId'});
    // creates instance methods:
    // user.latestPost()
    // user.latestPost.build(data)
    // user.latestPost.create(data)

    Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
    // creates instance methods:
    // post.author(callback) -- getter when called with function
    // post.author() -- sync getter when called without params
    // post.author(user) -- setter when called with object

    Passport = dataSource.define('Passport', {
      number: String,
    });

    Passport.belongsTo(User, {as: 'owner', foreignKey: 'ownerId'});
    User.hasMany(Passport, {as: 'passports', foreignKey: 'ownerId'});

    const user = new User;

    test.ok(User instanceof Function);

    // class methods
    test.ok(User.find instanceof Function);
    test.ok(User.create instanceof Function);

    // instance methods
    test.ok(user.save instanceof Function);

    dataSource.automigrate(function(err) {
      if (err) {
        console.log('Error while migrating');
        console.log(err);
      } else {
        test.done();
      }
    });
  });

  it('should initialize object properly', function(test) {
    const hw = 'Hello word',
      now = Date.now(),
      post = new Post({title: hw}),
      anotherPost = Post({title: 'Resig style constructor'});

    test.equal(post.title, hw);
    test.ok(!post.propertyChanged('title'), 'property changed: title');
    post.title = 'Goodbye, Lenin';
    test.equal(post.title_was, hw);
    test.ok(post.propertyChanged('title'));
    test.strictEqual(post.published, false);
    test.ok(post.date >= now);
    test.ok(post.isNewRecord());
    test.ok(anotherPost instanceof Post);
    test.ok(anotherPost.title, 'Resig style constructor');
    test.done();
  });

  it('should save object', function(test) {
    const title = 'Initial title', title2 = 'Hello world',
      date = new Date;

    Post.create({
      title: title,
      date: date,
    }, function(err, obj) {
      test.ok(obj.id, 'Object id should present');
      test.equals(obj.title, title);
      // test.equals(obj.date, date);
      obj.title = title2;
      test.ok(obj.propertyChanged('title'), 'Title changed');
      obj.save(function(err, obj) {
        test.equal(obj.title, title2);
        test.ok(!obj.propertyChanged('title'));

        const p = new Post({title: 1});
        p.title = 2;
        p.save(function(err, obj) {
          test.ok(!p.propertyChanged('title'));
          p.title = 3;
          test.ok(p.propertyChanged('title'));
          test.equal(p.title_was, 2);
          p.save(function() {
            test.equal(p.title_was, 3);
            test.ok(!p.propertyChanged('title'));
            test.done();
          });
        });
      });
    });
  });

  it('should create object with initial data', function(test) {
    const title = 'Initial title',
      date = new Date;

    Post.create({
      title: title,
      date: date,
    }, function(err, obj) {
      test.ok(obj.id);
      test.equals(obj.title, title);
      test.equals(obj.date, date);
      Post.findById(obj.id, function() {
        test.equal(obj.title, title);
        test.equal(obj.date.toString(), date.toString());
        test.done();
      });
    });
  });

  it('should save only dataSource-defined field in database', function(test) {
    Post.create({title: '1602', nonSchemaField: 'some value'}, function(err, post) {
      test.ok(!post.nonSchemaField);
      post.a = 1;
      post.save(function() {
        test.ok(post.a);
        post.reload(function(err, psto) {
          test.ok(!psto.a);
          test.done();
        });
      });
    });
  });

  /*
   it('should not create new instances for the same object', function (test) {
   var title = 'Initial title';
   Post.create({ title: title }, function (err, post) {
   test.ok(post.id, 'Object should have id');
   test.equals(post.title, title);
   Post.findById(post.id, function (err, foundPost) {
   if (err) throw err;
   test.equal(post.title, title);
   test.strictEqual(post, foundPost);
   test.done();
   });
   });
   });
   */

  it('should not re-instantiate object on saving', function(test) {
    const title = 'Initial title';
    const post = new Post({title: title});
    post.save(function(err, savedPost) {
      test.strictEqual(post, savedPost);
      test.done();
    });
  });

  it('should destroy object', function(test) {
    Post.create(function(err, post) {
      Post.exists(post.id, function(err, exists) {
        test.ok(exists, 'Object exists');
        post.destroy(function() {
          Post.exists(post.id, function(err, exists) {
            if (err) console.log(err);
            test.ok(!exists, 'Hey! ORM told me that object exists, ' +
              ' but it looks like it doesn\'t. Something went wrong...');
            Post.findById(post.id, function(err, obj) {
              test.equal(obj, null, 'Param obj should be null');
              test.done();
            });
          });
        });
      });
    });
  });

  it('should handle virtual attributes', function(test) {
    const salt = 's0m3s3cr3t5a1t';

    User.setter.passwd = function(password) {
      this._passwd = calcHash(password, salt);
    };

    function calcHash(pass, salt) {
      const crypto = require('crypto');
      const hash = crypto.createHash('sha256');
      hash.update(pass);
      hash.update(salt);
      return hash.digest('base64');
    }

    const u = new User;
    u.passwd = 's3cr3t';
    test.equal(u.passwd, calcHash('s3cr3t', salt));
    test.done();
  });

  // it('should serialize JSON type', function (test) {
  //     User.create({settings: {hello: 'world'}}, function (err, user) {
  //         test.ok(user.id);
  //         test.equal(user.settings.hello, 'world');
  //         User.find(user.id, function (err, u) {
  //             console.log(u.settings);
  //             test.equal(u.settings.hello, 'world');
  //             test.done();
  //         });
  //     });
  // });

  it('should update single attribute', function(test) {
    Post.create({title: 'title', content: 'content', published: true}, function(err, post) {
      post.content = 'New content';
      post.updateAttribute('title', 'New title', function() {
        test.equal(post.title, 'New title');
        test.ok(!post.propertyChanged('title'));
        test.equal(post.content, 'New content', 'dirty state saved');
        test.ok(post.propertyChanged('content'));
        post.reload(function(err, post) {
          test.equal(post.title, 'New title');
          test.ok(!post.propertyChanged('title'), 'title not changed');
          test.equal(post.content, 'content', 'real value turned back');
          test.ok(!post.propertyChanged('content'), 'content unchanged');
          test.done();
        });
      });
    });
  });

  let countOfposts, countOfpostsFiltered;
  it('should fetch collection', function(test) {
    Post.all(function(err, posts) {
      countOfposts = posts.length;
      test.ok(countOfposts > 0);
      test.ok(posts[0] instanceof Post);
      countOfpostsFiltered = posts.filter(function(p) {
        return p.title === 'title';
      }).length;
      test.done();
    });
  });

  it('should find records filtered with multiple attributes', function(test) {
    const d = new Date;
    Post.create({title: 'title', content: 'content', published: true, date: d}, function(err, post) {
      Post.all({where: {title: 'title', date: d, published: true}}, function(err, res) {
        test.equals(res.length, 1, 'Filtering Posts returns one post');
        test.done();
      });
    });
  });

  if (
    !dataSource.name.match(/redis/) &&
      dataSource.name !== 'memory' &&
      dataSource.name !== 'neo4j' &&
      dataSource.name !== 'cradle'
  )
    it('relations key is working', function(test) {
      test.ok(User.relations, 'Relations key should be defined');
      test.ok(User.relations.posts, 'posts relation should exist on User');
      test.equal(User.relations.posts.type, 'hasMany', 'Type of hasMany relation is hasMany');
      test.equal(User.relations.posts.multiple, true, 'hasMany relations are multiple');
      test.equal(User.relations.posts.keyFrom, 'id', 'keyFrom is primary key of model table');
      test.equal(User.relations.posts.keyTo, 'userId', 'keyTo is foreign key of related model table');

      test.ok(Post.relations, 'Relations key should be defined');
      test.ok(Post.relations.author, 'author relation should exist on Post');
      test.equal(Post.relations.author.type, 'belongsTo', 'Type of belongsTo relation is belongsTo');
      test.equal(Post.relations.author.multiple, false, 'belongsTo relations are not multiple');
      test.equal(Post.relations.author.keyFrom, 'userId', 'keyFrom is foreign key of model table');
      test.equal(Post.relations.author.keyTo, 'id', 'keyTo is primary key of related model table');
      test.done();
    });

  it('should handle hasMany relationship', function(test) {
    User.create(function(err, u) {
      if (err) return console.log(err);
      test.ok(u.posts, 'Method defined: posts');
      test.ok(u.posts.build, 'Method defined: posts.build');
      test.ok(u.posts.create, 'Method defined: posts.create');
      u.posts.create(function(err, post) {
        if (err) return console.log(err);
        u.posts(function(err, posts) {
          test.equal(posts.pop().id.toString(), post.id.toString());
          test.done();
        });
      });
    });
  });

  it('should navigate variations of belongsTo regardless of column name', function(test) {
    Dog.create({name: 'theDog'}, function(err, obj) {
      test.ok(obj instanceof Dog);
      Log.create({name: 'theLog', ownerId: obj.id}, function(err, obj) {
        test.ok(obj instanceof Log);
        obj.owner(function(err, obj) {
          test.ok(!err, 'Should not have an error.'); // Before cba174b this would be 'Error: Permission denied'
          if (err) {
            console.log('Found: ' + err);
          }
          test.ok(obj, 'Should not find null or undefined.'); // Before cba174b this could be null or undefined.
          test.ok(obj instanceof Dog, 'Should find a Dog.');
          if (obj) { // Since test won't stop on fail, have to check before accessing obj.name.
            test.ok(obj.name, 'Should have a name.');
          }
          if (obj && obj.name) {
            test.equal(obj.name, 'theDog', 'The owner of theLog is theDog.');
          }
          test.done();
        });
      });
    });
  });

  it('hasMany should support additional conditions', function(test) {
    User.create(function(e, u) {
      u.posts.create({}, function(e, p) {
        u.posts({where: {id: p.id}}, function(e, posts) {
          test.equal(posts.length, 1, 'There should be only 1 post.');
          test.done();
        });
      });
    });
  });

  /* eslint-disable max-len */
  it('hasMany should be cached', function(test) {
    // User.create(function (e, u) {
    //    u.posts.create({}, function (e, p) {
    // find all posts for a user.
    // Finding one post with an existing author associated
    Post.all(function(err, posts) {
      // We try to get the first post with a userId != NULL
      for (let i = 0; i < posts.length; i++) {
        const post = posts[i];
        if (post.userId) {
          // We could get the user with belongs to relationship but it is better if there is no interactions.
          User.findById(post.userId, function(err, user) {
            User.create(function(err, voidUser) {
              Post.create({userId: user.id}, function() {
                // There can't be any concurrency because we are counting requests
                // We are first testing cases when user has posts
                user.posts(function(err, data) {
                  const nbInitialRequests = nbSchemaRequests;
                  user.posts(function(err, data2) {
                    test.equal(data.length, 2, 'There should be 2 posts.');
                    test.equal(data.length, data2.length, 'Posts should be the same, since we are loading on the same object.');
                    requestsAreCounted && test.equal(nbInitialRequests, nbSchemaRequests, 'There should not be any request because value is cached.');

                    if (dataSource.name === 'mongodb') { // for the moment mongodb doesn\'t support additional conditions on hasMany relations (see above)
                      test.done();
                    } else {
                      user.posts({where: {id: data[0].id}}, function(err, data) {
                        test.equal(data.length, 1, 'There should be only one post.');
                        requestsAreCounted && test.equal(nbInitialRequests + 1, nbSchemaRequests, 'There should be one additional request since we added conditions.');

                        user.posts(function(err, data) {
                          test.equal(data.length, 2, 'Previous get shouldn\'t have changed cached value though, since there was additional conditions.');
                          requestsAreCounted && test.equal(nbInitialRequests + 1, nbSchemaRequests, 'There should not be any request because value is cached.');

                          // We are now testing cases when user doesn't have any post
                          voidUser.posts(function(err, data) {
                            const nbInitialRequests = nbSchemaRequests;
                            voidUser.posts(function(err, data2) {
                              test.equal(data.length, 0, 'There shouldn\'t be any posts (1/2).');
                              test.equal(data2.length, 0, 'There shouldn\'t be any posts (2/2).');
                              requestsAreCounted && test.equal(nbInitialRequests, nbSchemaRequests, 'There should not be any request because value is cached.');

                              voidUser.posts(true, function(err, data3) {
                                test.equal(data3.length, 0, 'There shouldn\'t be any posts.');
                                requestsAreCounted && test.equal(nbInitialRequests + 1, nbSchemaRequests, 'There should be one additional request since we forced refresh.');

                                test.done();
                              });
                            });
                          });
                        });
                      });
                    }
                  });
                });
              });
            });
          });
          break;
        }
      }
    });
  });
  /* eslint-enable max-len */

  // it('should handle hasOne relationship', function (test) {
  //     User.create(function (err, u) {
  //         if (err) return console.log(err);
  //     });
  // });

  it('should support scopes', function(test) {
    let wait = 2;

    test.ok(Post.scope, 'Scope supported');
    Post.scope('published', {where: {published: true}});
    test.ok(typeof Post.published === 'function');
    test.ok(Post.published._scope.where.published === true);
    const post = Post.published.build();
    test.ok(post.published, 'Can build');
    test.ok(post.isNewRecord());
    Post.published.create(function(err, psto) {
      if (err) return console.log(err);
      test.ok(psto.published);
      test.ok(!psto.isNewRecord());
      done();
    });

    User.create(function(err, u) {
      if (err) return console.log(err);
      test.ok(typeof u.posts.published == 'function');
      test.ok(u.posts.published._scope.where.published);
      console.log(u.posts.published._scope);
      test.equal(u.posts.published._scope.where.userId, u.id);
      done();
    });

    function done() {
      if (--wait === 0) test.done();
    }
  });

  it('should return type of property', function(test) {
    test.equal(Post.getPropertyType('title'), 'String');
    test.equal(Post.getPropertyType('content'), 'Text');
    const p = new Post;
    test.equal(p.getPropertyType('title'), 'String');
    test.equal(p.getPropertyType('content'), 'Text');
    test.done();
  });

  it('should handle ORDER clause', function(test) {
    const titles = [
      {title: 'Title A', subject: 'B'},
      {title: 'Title Z', subject: 'A'},
      {title: 'Title M', subject: 'C'},
      {title: 'Title A', subject: 'A'},
      {title: 'Title B', subject: 'A'},
      {title: 'Title C', subject: 'D'},
    ];
    const isRedis = Post.dataSource.name === 'redis';
    const dates = isRedis ? [5, 9, 0, 17, 10, 9] : [
      new Date(1000 * 5),
      new Date(1000 * 9),
      new Date(1000 * 0),
      new Date(1000 * 17),
      new Date(1000 * 10),
      new Date(1000 * 9),
    ];
    titles.forEach(function(t, i) {
      Post.create({title: t.title, subject: t.subject, date: dates[i]}, done);
    });

    let i = 0, tests = 0;

    function done(err, obj) {
      if (++i === titles.length) {
        doFilterAndSortTest();
        doFilterAndSortReverseTest();
        doStringTest();
        doNumberTest();

        if (dataSource.name == 'mongoose') {
          doMultipleSortTest();
          doMultipleReverseSortTest();
        }
      }
    }

    function compare(a, b) {
      if (a.title < b.title) return -1;
      if (a.title > b.title) return 1;
      return 0;
    }

    // Post.dataSource.log = console.log;

    function doStringTest() {
      tests += 1;
      Post.all({order: 'title'}, function(err, posts) {
        if (err) console.log(err);
        test.equal(posts.length, 6);
        titles.sort(compare).forEach(function(t, i) {
          if (posts[i]) test.equal(posts[i].title, t.title);
        });
        finished();
      });
    }

    function doNumberTest() {
      tests += 1;
      Post.all({order: 'date'}, function(err, posts) {
        if (err) console.log(err);
        test.equal(posts.length, 6);
        dates.sort(numerically).forEach(function(d, i) {
          if (posts[i])
            test.equal(posts[i].date.toString(), d.toString(), 'doNumberTest');
        });
        finished();
      });
    }

    function doFilterAndSortTest() {
      tests += 1;
      Post.all({where: {date: new Date(1000 * 9)}, order: 'title', limit: 3}, function(err, posts) {
        if (err) console.log(err);
        console.log(posts.length);
        test.equal(posts.length, 2, 'Exactly 2 posts returned by query');
        ['Title C', 'Title Z'].forEach(function(t, i) {
          if (posts[i]) {
            test.equal(posts[i].title, t, 'doFilterAndSortTest');
          }
        });
        finished();
      });
    }

    function doFilterAndSortReverseTest() {
      tests += 1;
      Post.all({where: {date: new Date(1000 * 9)}, order: 'title DESC', limit: 3}, function(err, posts) {
        if (err) console.log(err);
        test.equal(posts.length, 2, 'Exactly 2 posts returned by query');
        ['Title Z', 'Title C'].forEach(function(t, i) {
          if (posts[i]) {
            test.equal(posts[i].title, t, 'doFilterAndSortReverseTest');
          }
        });
        finished();
      });
    }

    function doMultipleSortTest() {
      tests += 1;
      Post.all({order: 'title ASC, subject ASC'}, function(err, posts) {
        if (err) console.log(err);
        test.equal(posts.length, 6);
        test.equal(posts[0].title, 'Title A');
        test.equal(posts[0].subject, 'A');
        test.equal(posts[1].title, 'Title A');
        test.equal(posts[1].subject, 'B');
        test.equal(posts[5].title, 'Title Z');
        finished();
      });
    }

    function doMultipleReverseSortTest() {
      tests += 1;
      Post.all({order: 'title ASC, subject DESC'}, function(err, posts) {
        if (err) console.log(err);
        test.equal(posts.length, 6);
        test.equal(posts[0].title, 'Title A');
        test.equal(posts[0].subject, 'B');
        test.equal(posts[1].title, 'Title A');
        test.equal(posts[1].subject, 'A');
        test.equal(posts[5].title, 'Title Z');
        finished();
      });
    }

    let fin = 0;

    function finished() {
      if (++fin === tests) {
        test.done();
      }
    }

    // TODO: do mixed test, do real dates tests, ensure that dates stored in UNIX timestamp format

    function numerically(a, b) {
      return a - b;
    }
  });

  // if (
  //     !dataSource.name.match(/redis/) &&
  //     dataSource.name !== 'memory' &&
  //     dataSource.name !== 'neo4j' &&
  //     dataSource.name !== 'cradle' &&
  //     dataSource.name !== 'nano'
  // )
  // it('should allow advanced queying: lt, gt, lte, gte, between', function (test) {
  //     Post.destroyAll(function () {
  //         Post.create({date: new Date('Wed, 01 Feb 2012 13:56:12 GMT')}, done);
  //         Post.create({date: new Date('Thu, 02 Feb 2012 13:56:12 GMT')}, done);
  //         Post.create({date: new Date('Fri, 03 Feb 2012 13:56:12 GMT')}, done);
  //         Post.create({date: new Date('Sat, 04 Feb 2012 13:56:12 GMT')}, done);
  //         Post.create({date: new Date('Sun, 05 Feb 2012 13:56:12 GMT')}, done);
  //         Post.create({date: new Date('Mon, 06 Feb 2012 13:56:12 GMT')}, done);
  //         Post.create({date: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}, done);
  //         Post.create({date: new Date('Wed, 08 Feb 2012 13:56:12 GMT')}, done);
  //         Post.create({date: new Date('Thu, 09 Feb 2012 13:56:12 GMT')}, done);
  //     });

  //     var posts = 9;
  //     function done() {
  //         if (--posts === 0) makeTest();
  //     }

  //     function makeTest() {
  //         // gt
  //         Post.all({where: {date: {gt: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
  //             test.equal(posts.length, 2, 'gt');
  //             ok();
  //         });

  //         // gte
  //         Post.all({where: {date: {gte: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
  //             test.equal(posts.length, 3, 'gte');
  //             ok();
  //         });

  //         // lte
  //         Post.all({where: {date: {lte: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
  //             test.equal(posts.length, 7, 'lte');
  //             ok();
  //         });

  //         // lt
  //         Post.all({where: {date: {lt: new Date('Tue, 07 Feb 2012 13:56:12 GMT')}}}, function (err, posts) {
  //             test.equal(posts.length, 6, 'lt');
  //             ok();
  //         });

  //         // between
  //         Post.all({where: {date: {between: [new Date('Tue, 05 Feb 2012 13:56:12 GMT'), new Date('Tue, 09 Feb 2012 13:56:12 GMT')]}}}, function (err, posts) {
  //             test.equal(posts.length, 5, 'between');
  //             ok();
  //         });
  //     }

  //     var tests = 5;
  //     function ok() {
  //         if (--tests === 0) test.done();
  //     }
  // });

  // if (
  //     dataSource.name === 'mysql' ||
  //     dataSource.name === 'postgres'
  // )
  // it('should allow IN or NOT IN', function (test) {
  //     User.destroyAll(function () {
  //         User.create({name: 'User A', age: 21}, done);
  //         User.create({name: 'User B', age: 22}, done);
  //         User.create({name: 'User C', age: 23}, done);
  //         User.create({name: 'User D', age: 24}, done);
  //         User.create({name: 'User E', age: 25}, done);
  //     });

  //     var users = 5;
  //     function done() {
  //         if (--users === 0) makeTest();
  //     }

  //     function makeTest() {
  //         // IN with empty array should return nothing
  //         User.all({where: {name: {inq: []}}}, function (err, users) {
  //             test.equal(users.length, 0, 'IN with empty array returns nothing');
  //             ok();
  //         });

  //         // NOT IN with empty array should return everything
  //         User.all({where: {name: {nin: []}}}, function (err, users) {
  //             test.equal(users.length, 5, 'NOT IN with empty array returns everything');
  //             ok();
  //         });

  //         // IN [User A] returns user with name = User A
  //         User.all({where: {name: {inq: ['User A']}}}, function (err, users) {
  //             test.equal(users.length, 1, 'IN searching one existing value returns 1 user');
  //             test.equal(users[0].name, 'User A', 'IN [User A] returns user with name = User A');
  //             ok();
  //         });

  //         // NOT IN [User A] returns users with name != User A
  //         User.all({where: {name: {nin: ['User A']}}}, function (err, users) {
  //             test.equal(users.length, 4, 'IN [User A] returns users with name != User A');
  //             ok();
  //         });

  //         // IN [User A, User B] returns users with name = User A OR name = User B
  //         User.all({where: {name: {inq: ['User A', 'User B']}}}, function (err, users) {
  //             test.equal(users.length, 2, 'IN searching two existing values returns 2 users');
  //             ok();
  //         });

  //         // NOT IN [User A, User B] returns users with name != User A AND name != User B
  //         User.all({where: {name: {nin: ['User A', 'User B']}}}, function (err, users) {
  //             test.equal(users.length, 3, 'NOT IN searching two existing values returns users with name != User A AND name != User B');
  //             ok();
  //         });

  //         // IN works with numbers too
  //         User.all({where: {age: {inq: [21, 22]}}}, function (err, users) {
  //             test.equal(users.length, 2, 'IN works with numbers too');
  //             ok();
  //         });

  //         // NOT IN works with numbers too
  //         User.all({where: {age: {nin: [21, 22]}}}, function (err, users) {
  //             test.equal(users.length, 3, 'NOT IN works with numbers too');
  //             ok();
  //         });
  //     }

  //     var tests = 8;
  //     function ok() {
  //         if (--tests === 0) test.done();
  //     }
  // });

  it('should handle order clause with direction', function(test) {
    let wait = 0;
    const emails = [
      'john@hcompany.com',
      'tom@hcompany.com',
      'admin@hcompany.com',
      'tin@hcompany.com',
      'mike@hcompany.com',
      'susan@hcompany.com',
      'test@hcompany.com',
    ];
    User.destroyAll(function() {
      emails.forEach(function(email) {
        wait += 1;
        User.create({email: email, name: 'Nick'}, done);
      });
    });
    let tests = 2;

    function done() {
      process.nextTick(function() {
        if (--wait === 0) {
          doSortTest();
          doReverseSortTest();
        }
      });
    }

    function doSortTest() {
      User.all({order: 'email ASC', where: {name: 'Nick'}}, function(err, users) {
        const _emails = emails.sort();
        users.forEach(function(user, i) {
          test.equal(_emails[i], user.email, 'ASC sorting');
        });
        testDone();
      });
    }

    function doReverseSortTest() {
      User.all({order: 'email DESC', where: {name: 'Nick'}}, function(err, users) {
        const _emails = emails.sort().reverse();
        users.forEach(function(user, i) {
          test.equal(_emails[i], user.email, 'DESC sorting');
        });
        testDone();
      });
    }

    function testDone() {
      if (--tests === 0) test.done();
    }
  });

  it('should return id in find result even after updateAttributes', function(test) {
    Post.create(function(err, post) {
      const id = post.id;
      test.ok(post.published === false);
      post.updateAttributes({title: 'hey', published: true}, function() {
        Post.find(id, function(err, post) {
          test.ok(!!post.published, 'Update boolean field');
          test.ok(post.id);
          test.done();
        });
      });
    });
  });

  it('should handle belongsTo correctly', function(test) {
    const passport = new Passport({ownerId: 16});
    // sync getter
    test.equal(passport.owner(), 16);
    // sync setter
    passport.owner(18);
    test.equal(passport.owner(), 18);
    test.done();
  });

  it('should query one record', function(test) {
    test.expect(4);
    Post.findOne(function(err, post) {
      test.ok(post && post.id);
      Post.findOne({where: {title: 'hey'}}, function(err, post) {
        if (err) {
          console.log(err);
          return test.done();
        }
        test.equal(post && post.constructor.modelName, 'Post');
        test.equal(post && post.title, 'hey');
        Post.findOne({where: {title: 'not exists'}}, function(err, post) {
          test.ok(post === null);
          test.done();
        });
      });
    });
  });

  // if (
  //     !dataSource.name.match(/redis/) &&
  //         dataSource.name !== 'memory' &&
  //         dataSource.name !== 'neo4j' &&
  //         dataSource.name !== 'cradle' &&
  //         dataSource.name !== 'nano'
  //     )
  // it('belongsTo should be cached', function (test) {
  //     User.findOne(function(err, user) {

  //         var passport = new Passport({ownerId: user.id});
  //         var passport2 = new Passport({ownerId: null});

  //         // There can't be any concurrency because we are counting requests
  //         // We are first testing cases when passport has an owner
  //         passport.owner(function(err, data) {
  //             var nbInitialRequests = nbSchemaRequests;
  //             passport.owner(function(err, data2) {
  //                 test.equal(data.id, data2.id, 'The value should remain the same');
  //                 requestsAreCounted && test.equal(nbInitialRequests, nbSchemaRequests, 'There should not be any request because value is cached.');

  //                 // We are now testing cases when passport has not an owner
  //                 passport2.owner(function(err, data) {
  //                     var nbInitialRequests2 = nbSchemaRequests;
  //                     passport2.owner(function(err, data2) {
  //                         test.equal(data, null, 'The value should be null since there is no owner');
  //                         test.equal(data, data2, 'The value should remain the same (null)');
  //                         requestsAreCounted && test.equal(nbInitialRequests2, nbSchemaRequests, 'There should not be any request because value is cached.');

  //                         passport2.owner(user.id);
  //                         passport2.owner(function(err, data3) {
  //                             test.equal(data3.id, user.id, 'Owner should now be the user.');
  //                             requestsAreCounted && test.equal(nbInitialRequests2 + 1, nbSchemaRequests, 'If we changed owner id, there should be one more request.');

  //                             passport2.owner(true, function(err, data4) {
  //                                 test.equal(data3.id, data3.id, 'The value should remain the same');
  //                                 requestsAreCounted && test.equal(nbInitialRequests2 + 2, nbSchemaRequests, 'If we forced refreshing, there should be one more request.');
  //                                 test.done();
  //                             });
  //                         });
  //                     });
  //                 });

  //             });
  //         });
  //     });

  // });

  if (dataSource.name !== 'mongoose' && dataSource.name !== 'neo4j')
    it('should update or create record', function(test) {
      const newData = {
        id: 1,
        title: 'New title (really new)',
        content: 'Some example content (updated)',
      };
      Post.updateOrCreate(newData, function(err, updatedPost) {
        if (err) throw err;
        test.ok(updatedPost);
        if (!updatedPost) throw Error('No post!');

        if (dataSource.name !== 'mongodb') {
          test.equal(newData.id, updatedPost.toObject().id);
        }
        test.equal(newData.title, updatedPost.toObject().title);
        test.equal(newData.content, updatedPost.toObject().content);

        Post.findById(updatedPost.id, function(err, post) {
          if (err) throw err;
          if (!post) throw Error('No post!');
          if (dataSource.name !== 'mongodb') {
            test.equal(newData.id, post.toObject().id);
          }
          test.equal(newData.title, post.toObject().title);
          test.equal(newData.content, post.toObject().content);
          Post.updateOrCreate({id: 100001, title: 'hey'}, function(err, post) {
            if (dataSource.name !== 'mongodb') test.equal(post.id, 100001);
            test.equal(post.title, 'hey');
            Post.findById(post.id, function(err, post) {
              if (!post) throw Error('No post!');
              test.done();
            });
          });
        });
      });
    });

  it('should work with custom setters and getters', function(test) {
    User.dataSource.defineForeignKey('User', 'passwd');
    User.setter.passwd = function(pass) {
      this._passwd = pass + 'salt';
    };
    const u = new User({passwd: 'qwerty'});
    test.equal(u.passwd, 'qwertysalt');
    u.save(function(err, user) {
      User.findById(user.id, function(err, user) {
        test.ok(user !== u);
        test.equal(user.passwd, 'qwertysalt');
        User.all({where: {passwd: 'qwertysalt'}}, function(err, users) {
          test.ok(users[0] !== user);
          test.equal(users[0].passwd, 'qwertysalt');
          User.create({passwd: 'asalat'}, function(err, usr) {
            test.equal(usr.passwd, 'asalatsalt');
            User.upsert({passwd: 'heyman'}, function(err, us) {
              test.equal(us.passwd, 'heymansalt');
              User.findById(us.id, function(err, user) {
                test.equal(user.passwd, 'heymansalt');
                test.done();
              });
            });
          });
        });
      });
    });
  });

  it('should work with typed and untyped nested collections', function(test) {
    const post = new Post;
    const like = post.likes.push({foo: 'bar'});
    test.equal(like.constructor.name, 'ListItem');
    const related = post.related.push({hello: 'world'});
    test.ok(related.someMethod);
    post.save(function(err, p) {
      test.equal(p.likes.nextid, 2);
      p.likes.push({second: 2});
      p.likes.push({third: 3});
      p.save(function(err) {
        Post.findById(p.id, function(err, pp) {
          test.equal(pp.likes.length, 3);
          test.ok(pp.likes[3].third);
          test.ok(pp.likes[2].second);
          test.ok(pp.likes[1].foo);
          pp.likes.remove(2);
          test.equal(pp.likes.length, 2);
          test.ok(!pp.likes[2]);
          pp.likes.remove(pp.likes[1]);
          test.equal(pp.likes.length, 1);
          test.ok(!pp.likes[1]);
          test.ok(pp.likes[3]);
          pp.save(function() {
            Post.findById(p.id, function(err, pp) {
              test.equal(pp.likes.length, 1);
              test.ok(!pp.likes[1]);
              test.ok(pp.likes[3]);
              test.done();
            });
          });
        });
      });
    });
  });

  it('should find or create', function(test) {
    const email = 'some email ' + Math.random();
    User.findOrCreate({where: {email: email}}, function(err, u, created) {
      test.ok(u);
      test.ok(!u.age);
      test.ok(created);
      User.findOrCreate({where: {email: email}}, {age: 21}, function(err, u2, created) {
        test.equals(u.id.toString(), u2.id.toString(), 'Same user ids');
        test.ok(!u2.age);
        test.ok(!created);
        test.done();
      });
    });
  });
}