var async = require('async');
var loopback = require('../');
var ACL = loopback.ACL;
var Change = loopback.Change;
var defineModelTestsWithDataSource = require('./util/model-tests');
var PersistedModel = loopback.PersistedModel;

var describe = require('./util/describe');

describe('Model / PersistedModel', function() {
  defineModelTestsWithDataSource({
    dataSource: {
      connector: loopback.Memory
    }
  });

  describe('Model.validatesUniquenessOf(property, options)', function() {
    it("Ensure the value for `property` is unique", function(done) {
      var User = PersistedModel.extend('user', {
        'first': String,
        'last': String,
        'age': Number,
        'password': String,
        'gender': String,
        'domain': String,
        'email': String
      });

      var dataSource = loopback.createDataSource({
        connector: loopback.Memory
      });

      User.attachTo(dataSource);
      
      User.validatesUniquenessOf('email', {message: 'email is not unique'});
      
      var joe = new User({email: 'joe@joe.com'});
      var joe2 = new User({email: 'joe@joe.com'});

      joe.save(function () {
        joe2.save(function (err) {
          assert(err, 'should get a validation error');
          assert(joe2.errors.email, 'model should have email error');
          
          done();
        });
      });
    });
  });

  describe('Model.attachTo(dataSource)', function() {
    it("Attach a model to a [DataSource](#data-source)", function() {
      var MyModel = loopback.createModel('my-model', {name: String});
      var dataSource = loopback.createDataSource({
        connector: loopback.Memory
      });
      
      MyModel.attachTo(dataSource);
      
      MyModel.find(function(err, results) {
        assert(results.length === 0, 'should have data access methods after attaching to a data source');
      });
    });
  });
});

describe.onServer('Remote Methods', function(){

  var User;
  var dataSource;
  var app;

  beforeEach(function () {
    User = PersistedModel.extend('user', {
      'first': String,
      'last': String,
      'age': Number,
      'password': String,
      'gender': String,
      'domain': String,
      'email': String
    }, {
      trackChanges: true
    });

    dataSource = loopback.createDataSource({
      connector: loopback.Memory
    });

    User.attachTo(dataSource);

    User.login = function (username, password, fn) {
      if(username === 'foo' && password === 'bar') {
        fn(null, 123);
      } else {
        throw new Error('bad username and password!');
      }
    }

    loopback.remoteMethod(
      User.login,
      {
        accepts: [
          {arg: 'username', type: 'string', required: true},
          {arg: 'password', type: 'string', required: true}
        ],
        returns: {arg: 'sessionId', type: 'any', root: true},
        http: {path: '/sign-in', verb: 'get'}
      }
    );

    app = loopback();
    app.use(loopback.rest());
    app.model(User);
  });
  
  describe('Model.destroyAll(callback)', function() {
    it("Delete all Model instances from data source", function(done) {
      (new TaskEmitter())
        .task(User, 'create', {first: 'jill'})
        .task(User, 'create', {first: 'bob'})
        .task(User, 'create', {first: 'jan'})
        .task(User, 'create', {first: 'sam'})
        .task(User, 'create', {first: 'suzy'})
        .on('done', function () {
          User.count(function (err, count) {
            User.destroyAll(function () {
              User.count(function (err, count) {
                assert.equal(count, 0);
                done();
              });
            });
          });
        });
    });
  });

  describe('Example Remote Method', function () {
    it('Call the method using HTTP / REST', function(done) {
      request(app)
        .get('/users/sign-in?username=foo&password=bar')
        .expect('Content-Type', /json/)
        .expect(200)
        .end(function(err, res){
          if(err) return done(err);
          assert.equal(res.body, 123);
          done();
        });
    });

    it('Converts null result of findById to 404 Not Found', function(done) {
      request(app)
        .get('/users/not-found')
        .expect(404)
        .end(done);
    });
  });

  describe('Model.beforeRemote(name, fn)', function(){
    it('Run a function before a remote method is called by a client', function(done) {
      var hookCalled = false;
      
      User.beforeRemote('create', function(ctx, user, next) {
        hookCalled = true;
        next();
      });
      
      // invoke save
      request(app)
        .post('/users')
        .send({data: {first: 'foo', last: 'bar'}})
        .expect('Content-Type', /json/)
        .expect(200)
        .end(function(err, res) {
          if(err) return done(err);
          assert(hookCalled, 'hook wasnt called');
          done();
        });
    });
  });

  describe('Model.afterRemote(name, fn)', function(){
    it('Run a function after a remote method is called by a client', function(done) {
      var beforeCalled = false;
      var afterCalled = false;
      
      User.beforeRemote('create', function(ctx, user, next) {
        assert(!afterCalled);
        beforeCalled = true;
        next();
      });
      User.afterRemote('create', function(ctx, user, next) {
        assert(beforeCalled);
        afterCalled = true;
        next();
      });
      
      // invoke save
      request(app)
        .post('/users')
        .send({data: {first: 'foo', last: 'bar'}})
        .expect('Content-Type', /json/)
        .expect(200)
        .end(function(err, res) {
          if(err) return done(err);
          assert(beforeCalled, 'before hook was not called');
          assert(afterCalled, 'after hook was not called');
          done();
        });
    });
  });

  describe('Remote Method invoking context', function () {
    describe('ctx.req', function() {
      it("The express ServerRequest object", function(done) {
        var hookCalled = false;
        
        User.beforeRemote('create', function(ctx, user, next) {
          hookCalled = true;
          assert(ctx.req);
          assert(ctx.req.url);
          assert(ctx.req.method);
          assert(ctx.res);
          assert(ctx.res.write);
          assert(ctx.res.end);
          next();
        });
      
        // invoke save
        request(app)
          .post('/users')
          .send({data: {first: 'foo', last: 'bar'}})
          .expect('Content-Type', /json/)
          .expect(200)
          .end(function(err, res) {
            if(err) return done(err);
            assert(hookCalled);
            done();
          });
      });
    });

    describe('ctx.res', function() {
      it("The express ServerResponse object", function(done) {
        var hookCalled = false;
        
        User.beforeRemote('create', function(ctx, user, next) {
          hookCalled = true;
          assert(ctx.req);
          assert(ctx.req.url);
          assert(ctx.req.method);
          assert(ctx.res);
          assert(ctx.res.write);
          assert(ctx.res.end);
          next();
        });
      
        // invoke save
        request(app)
          .post('/users')
          .send({data: {first: 'foo', last: 'bar'}})
          .expect('Content-Type', /json/)
          .expect(200)
          .end(function(err, res) {
            if(err) return done(err);
            assert(hookCalled);
            done();
          });
      });
    });
  })

  describe('Model.hasMany(Model)', function() {
    it("Define a one to many relationship", function(done) {
      var Book = dataSource.createModel('book', {title: String, author: String});
      var Chapter = dataSource.createModel('chapter', {title: String});
      
      // by referencing model
      Book.hasMany(Chapter);
      
      Book.create({title: 'Into the Wild', author: 'Jon Krakauer'}, function(err, book) {
        // using 'chapters' scope for build:
        var c = book.chapters.build({title: 'Chapter 1'});
        book.chapters.create({title: 'Chapter 2'}, function () {
          c.save(function () {
            Chapter.count({bookId: book.id}, function (err, count) {
              assert.equal(count, 2);
              book.chapters({where: {title: 'Chapter 1'}}, function(err, chapters) {
                assert.equal(chapters.length, 1);
                assert.equal(chapters[0].title, 'Chapter 1');
                done();
              });
            });
          });
        });
      });
    });
  });
  
  describe('Model.properties', function(){
    it('Normalized properties passed in originally by loopback.createModel()', function() {
      var props = {
        s: String,
        n: {type: 'Number'},
        o: {type: 'String', min: 10, max: 100},
        d: Date,
        g: loopback.GeoPoint
      };
      
      var MyModel = loopback.createModel('foo', props);
      
      Object.keys(MyModel.definition.properties).forEach(function (key) {
        var p = MyModel.definition.properties[key];
        var o = MyModel.definition.properties[key];
        assert(p);
        assert(o);
        assert(typeof p.type === 'function');
        
        if(typeof o === 'function') {
          // the normalized property
          // should match the given property
          assert(
            p.type.name === o.name
            ||
            p.type.name === o
          )
        }
      });
    });
  });
    
  describe('Model.extend()', function(){
    it('Create a new model by extending an existing model', function() {
      var User = loopback.PersistedModel.extend('test-user', {
        email: String
      });
      
      User.foo = function () {
        return 'bar';
      }
      
      User.prototype.bar = function () {
        return 'foo';
      }
      
      var MyUser = User.extend('my-user', {
        a: String,
        b: String
      });
      
      assert.equal(MyUser.prototype.bar, User.prototype.bar);
      assert.equal(MyUser.foo, User.foo);
      
      var user = new MyUser({
        email: 'foo@bar.com',
        a: 'foo',
        b: 'bar'
      });
      
      assert.equal(user.email, 'foo@bar.com');
      assert.equal(user.a, 'foo');
      assert.equal(user.b, 'bar');
    });
  });

  describe('Model.extend() events', function() {
    it('create isolated emitters for subclasses', function() {
      var User1 = loopback.createModel('User1', {
        'first': String,
        'last': String
      });

      var User2 = loopback.createModel('User2', {
        'name': String
      });

      var user1Triggered = false;
      User1.once('x', function(event) {
        user1Triggered = true;
      });


      var user2Triggered = false;
      User2.once('x', function(event) {
        user2Triggered = true;
      });

      assert(User1.once !== User2.once);
      assert(User1.once !== loopback.Model.once);

      User1.emit('x', User1);

      assert(user1Triggered);
      assert(!user2Triggered);
    });

  });

  describe('Model.checkAccessTypeForMethod(remoteMethod)', function () {
    shouldReturn('create', ACL.WRITE);
    shouldReturn('updateOrCreate', ACL.WRITE);
    shouldReturn('upsert', ACL.WRITE);
    shouldReturn('exists', ACL.READ);
    shouldReturn('findById', ACL.READ);
    shouldReturn('find', ACL.READ);
    shouldReturn('findOne', ACL.READ);
    shouldReturn('destroyById', ACL.WRITE);
    shouldReturn('deleteById', ACL.WRITE);
    shouldReturn('removeById', ACL.WRITE);
    shouldReturn('count', ACL.READ);
    shouldReturn('unkown-model-method', ACL.EXECUTE);

    function shouldReturn(methodName, expectedAccessType) {
      describe(methodName, function () {
        it('should return ' + expectedAccessType, function() {
          var remoteMethod = {name: methodName};
          assert.equal(
            User._getAccessTypeForMethod(remoteMethod),
            expectedAccessType
          );
        });
      });
    }
  });

  describe('Model.getChangeModel()', function() {
    it('Get the Change Model', function () {
      var UserChange = User.getChangeModel();
      var change = new UserChange();
      assert(change instanceof Change);
    });
  });

  describe('Model.getSourceId(callback)', function() {
    it('Get the Source Id', function (done) {
      User.getSourceId(function(err, id) {
        assert.equal('memory-user', id);
        done();
      });
    });
  });

  describe('Model.checkpoint(callback)', function() {
    it('Create a checkpoint', function (done) {
      var Checkpoint = User.getChangeModel().getCheckpointModel();
      var tasks = [
        getCurrentCheckpoint,
        checkpoint
      ];
      var result;
      var current;

      async.series(tasks, function(err) {
        if(err) return done(err);

        assert.equal(result, current + 1);
        done();
      });

      function getCurrentCheckpoint(cb) {
        Checkpoint.current(function(err, cp) {
          current = cp;
          cb(err);
        });
      }

      function checkpoint(cb) {
        User.checkpoint(function(err, cp) {
          result = cp.seq;
          cb(err);
        });
      }
    });
  });

  describe('Model._getACLModel()', function() {
    it('should return the subclass of ACL', function() {
      var Model = require('../').Model;
      var acl = ACL.extend('acl');
      Model._ACL(null); // Reset the ACL class for the base model
      var model = Model._ACL();
      assert.equal(model, acl);
    });
  });

  describe('PersistelModel remote methods', function() {
    it('includes all aliases', function() {
      var app = loopback();
      var model = PersistedModel.extend('persistedModel');
      app.dataSource('db', { connector: 'memory' });
      app.model(model, { dataSource: 'db' });

      // this code is used by loopback-sdk-angular codegen
      var metadata = app.handler('rest')
        .adapter
        .getClasses()
        .filter(function(c) { return c.name === 'persistedModel'; })[0];

      var methodNames = [];
      metadata.methods.forEach(function(method) {
        methodNames.push(method.name);
        methodNames = methodNames.concat(method.sharedMethod.aliases || []);
      });

      expect(methodNames).to.have.members([
        'destroyAll', 'deleteAll', 'remove',
        'create',
        'upsert', 'updateOrCreate',
        'exists',
        'findById',
        'find',
        'findOne',
        'updateAll', 'update',
        'deleteById',
        'destroyById',
        'removeById',
        'count',
        'prototype.updateAttributes'
      ]);
    });
  });
});