// This test written in mocha+should.js
var should = require('./init.js');
var assert = require('assert');

var jdb = require('../');
var ModelBuilder = jdb.ModelBuilder;
var DataSource = jdb.DataSource;

describe('ModelBuilder define model', function () {

    it('should be able to define plain models', function (done) {
        var modelBuilder = new ModelBuilder();

        var User = modelBuilder.define('User', {
            name: String,
            bio: ModelBuilder.Text,
            approved: Boolean,
            joinedAt: Date,
            age: Number
        });

        // define any custom method
        User.prototype.getNameAndAge = function () {
            return this.name + ', ' + this.age;
        };

        modelBuilder.models.should.be.a('object').and.have.property('User', User);
        modelBuilder.definitions.should.be.a('object').and.have.property('User');

        var user = new User({name: 'Joe', age: 20});

        User.modelName.should.equal('User');
        user.should.be.a('object').and.have.property('name', 'Joe');
        user.should.have.property('name', 'Joe');
        user.should.have.property('age', 20);
        user.should.not.have.property('bio');
        done(null, User);
    });

    it('should not take unknown properties in strict mode', function (done) {
        var modelBuilder = new ModelBuilder();

        var User = modelBuilder.define('User', {name: String, bio: String}, {strict: true});

        var user = new User({name: 'Joe', age: 20});

        User.modelName.should.equal('User');
        user.should.be.a('object');
        assert(user.name === 'Joe');
        assert(user.age === undefined);
        assert(user.toObject().age === undefined);
        assert(user.toObject(true).age === undefined);
        assert(user.bio === undefined);
        done(null, User);
    });

    it('should throw when unknown properties are used if strict=throw', function (done) {
        var modelBuilder = new ModelBuilder();

        var User = modelBuilder.define('User', {name: String, bio: String}, {strict: 'throw'});

        try {
            var user = new User({name: 'Joe', age: 20});
            assert(false, 'The code should have thrown an error');
        } catch(e) {
            assert(true, 'The code is expected to throw an error');
        }
        done(null, User);
    });

    it('should be able to define open models', function (done) {
        var modelBuilder = new ModelBuilder();

        var User = modelBuilder.define('User', {}, {strict: false});

        var user = new User({name: 'Joe', age: 20});

        User.modelName.should.equal('User');
        user.should.be.a('object').and.have.property('name', 'Joe');
        user.should.have.property('name', 'Joe');
        user.should.have.property('age', 20);
        user.should.not.have.property('bio');
        done(null, User);
    });

    it('should use false as the default value for strict', function (done) {
        var modelBuilder = new ModelBuilder();

        var User = modelBuilder.define('User', {});

        var user = new User({name: 'Joe', age: 20});

        User.modelName.should.equal('User');
        user.should.be.a('object').and.have.property('name', 'Joe');
        user.should.have.property('name', 'Joe');
        user.should.have.property('age', 20);
        user.should.not.have.property('bio');
        done(null, User);
    });


    it('should be able to define nesting models', function (done) {
        var modelBuilder = new ModelBuilder();

        // simplier way to describe model
        var User = modelBuilder.define('User', {
            name: String,
            bio: ModelBuilder.Text,
            approved: Boolean,
            joinedAt: Date,
            age: Number,
            address: {
                street: String,
                city: String,
                state: String,
                zipCode: String,
                country: String
            },
            emails: [{
                label: String,
                email: String
            }],
            friends: [String]
        });

        // define any custom method
        User.prototype.getNameAndAge = function () {
            return this.name + ', ' + this.age;
        };

        modelBuilder.models.should.be.a('object').and.have.property('User', User);
        modelBuilder.definitions.should.be.a('object').and.have.property('User');

        var user = new User({
                name: 'Joe', age: 20,
                address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'},
                emails: [{label: 'work', email: 'xyz@sample.com'}],
                friends: ['Mary', 'John']
        });

        User.modelName.should.equal('User');
        user.should.be.a('object').and.have.property('name', 'Joe');
        user.should.have.property('name', 'Joe');
        user.should.have.property('age', 20);
        user.should.not.have.property('bio');
        user.should.have.property('address');
        user.address.should.have.property('city', 'San Jose');
        user.address.should.have.property('state', 'CA');

        user = user.toObject();
        user.emails.should.have.property('length', 1);
        user.emails[0].should.have.property('label', 'work');
        user.emails[0].should.have.property('email', 'xyz@sample.com');
        user.friends.should.have.property('length', 2);
        assert.equal(user.friends[0], 'Mary');
        assert.equal(user.friends[1], 'John');
        done(null, User);
    });

    it('should be able to reference models by name before they are defined', function (done) {
        var modelBuilder = new ModelBuilder();

        var User = modelBuilder.define('User', {name: String, address: 'Address'});

        var user;
        try {
            user = new User({name: 'Joe', address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'}});
            assert(false, 'An exception should have been thrown');
        } catch (e) {
            // Ignore
        }

        var Address = modelBuilder.define('Address', {
            street: String,
            city: String,
            state: String,
            zipCode: String,
            country: String
        });

        user = new User({name: 'Joe', address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'}});

        User.modelName.should.equal('User');
        User.definition.properties.address.should.have.property('type', Address);
        user.should.be.a('object');
        assert(user.name === 'Joe');
        user.address.should.have.property('city', 'San Jose');
        user.address.should.have.property('state', 'CA');
        done(null, User);
    });


});


describe('DataSource define model', function () {
    it('should be able to define plain models', function () {
        var ds = new DataSource('memory');

// define models
        var Post = ds.define('Post', {
            title: { type: String, length: 255 },
            content: { type: ModelBuilder.Text },
            date: { type: Date, default: function () {
                return new Date();
            } },
            timestamp: { type: Number, default: Date.now },
            published: { type: Boolean, default: false, index: true }
        });

// simpler way to describe model
        var User = ds.define('User', {
            name: String,
            bio: ModelBuilder.Text,
            approved: Boolean,
            joinedAt: Date,
            age: Number
        });

        var Group = ds.define('Group', {group: String});
        User.mixin(Group);

// define any custom method
        User.prototype.getNameAndAge = function () {
            return this.name + ', ' + this.age;
        };

        var user = new User({name: 'Joe', group: 'G1'});
        assert.equal(user.name, 'Joe');
        assert.equal(user.group, 'G1');

        // setup relationships
        User.hasMany(Post, {as: 'posts', foreignKey: 'userId'});

        Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});

        User.hasAndBelongsToMany('groups');

        var user2 = new User({name: 'Smith'});
        user2.save(function (err) {
            var post = user2.posts.build({title: 'Hello world'});
            post.save(function (err, data) {
                // console.log(err ? err : data);
            });
        });

        Post.findOne({where: {published: false}, order: 'date DESC'}, function (err, data) {
            // console.log(data);
        });

        User.create({name: 'Jeff'}, function (err, data) {
            if (err) {
                console.log(err);
                return;
            }
            var post = data.posts.build({title: 'My Post'});
        });

        User.create({name: 'Ray'}, function (err, data) {
            // console.log(data);
        });

        var Article = ds.define('Article', {title: String});
        var Tag = ds.define('Tag', {name: String});
        Article.hasAndBelongsToMany('tags');

        Article.create(function (e, article) {
            article.tags.create({name: 'popular'}, function (err, data) {
                Article.findOne(function (e, article) {
                    article.tags(function (e, tags) {
                        // console.log(tags);
                    });
                });
            });
        });

// should be able to attach a data source to an existing model
        var modelBuilder = new ModelBuilder();

        var Color = modelBuilder.define('Color', {
            name: String
        });

        Color.should.not.have.property('create');

// attach
        ds.attach(Color);
        Color.should.have.property('create');

        Color.create({name: 'red'});
        Color.create({name: 'green'});
        Color.create({name: 'blue'});

        Color.all(function (err, colors) {
            colors.should.have.lengthOf(3);
        });

    });

    it('should not take unknown properties in strict mode', function (done) {
        var ds = new DataSource('memory');

        var User = ds.define('User', {name: String, bio: String}, {strict: true});

        User.create({name: 'Joe', age: 20}, function (err, user) {

            User.modelName.should.equal('User');
            user.should.be.a('object');
            assert(user.name === 'Joe');
            assert(user.age === undefined);
            assert(user.toObject().age === undefined);
            assert(user.toObject(true).age === undefined);
            assert(user.bio === undefined);
            done(null, User);
        });
    });

    it('should throw when unknown properties are used if strict=throw', function (done) {
        var ds = new DataSource('memory');

        var User = ds.define('User', {name: String, bio: String}, {strict: 'throw'});

        try {
            var user = new User({name: 'Joe', age: 20});
            assert(false, 'The code should have thrown an error');
        } catch(e) {
            assert(true, 'The code is expected to throw an error');
        }
        done(null, User);
    });

    it('should be able to define open models', function (done) {
        var ds = new DataSource('memory');

        var User = ds.define('User', {}, {strict: false});
        User.modelName.should.equal('User');

        User.create({name: 'Joe', age: 20}, function (err, user) {

            user.should.be.a('object').and.have.property('name', 'Joe');
            user.should.have.property('name', 'Joe');
            user.should.have.property('age', 20);
            user.should.not.have.property('bio');

            User.findById(user.id, function(err, user) {
                user.should.be.a('object').and.have.property('name', 'Joe');
                user.should.have.property('name', 'Joe');
                user.should.have.property('age', 20);
                user.should.not.have.property('bio');
                done(null, User);
            });
        });
    });

    it('should use false as the default value for strict', function (done) {
        var ds = new DataSource('memory');

        var User = ds.define('User', {});

        User.create({name: 'Joe', age: 20}, function (err, user) {

            User.modelName.should.equal('User');
            user.should.be.a('object').and.have.property('name', 'Joe');
            user.should.have.property('name', 'Joe');
            user.should.have.property('age', 20);
            user.should.not.have.property('bio');
            done(null, User);
        });
    });

    it('should use true as the default value for strict for relational DBs', function (done) {
        var ds = new DataSource('memory');
        ds.connector.relational = true; // HACK

        var User = ds.define('User', {name: String, bio: String}, {strict: true});

        var user = new User({name: 'Joe', age: 20});

        User.modelName.should.equal('User');
        user.should.be.a('object');
        assert(user.name === 'Joe');
        assert(user.age === undefined);
        assert(user.toObject().age === undefined);
        assert(user.toObject(true).age === undefined);
        assert(user.bio === undefined);
        done(null, User);
    });

    it('should throw when unknown properties are used if strict=false for relational DBs', function (done) {
        var ds = new DataSource('memory');
        ds.connector.relational = true; // HACK

        var User = ds.define('User', {name: String, bio: String}, {strict: 'throw'});

        try {
            var user = new User({name: 'Joe', age: 20});
            assert(false, 'The code should have thrown an error');
        } catch(e) {
            assert(true, 'The code is expected to throw an error');
        }
        done(null, User);
    });


    it('should change the property value for save if strict=false', function (done) {
        var ds = new DataSource('memory');// define models
        var Post = ds.define('Post');

        Post.create({price: 900}, function(err, post) {
            assert.equal(post.price, 900);
            post.price = 1000;
            post.save(function(err, result) {
               assert.equal(1000, result.price);
               done(err, result);
            });
        });
    });

});

describe('Load models with base', function () {
  it('should set up base class', function (done) {
    var ds = new ModelBuilder();

    var User = ds.define('User', {name: String});

    User.staticMethod = function staticMethod() {};
    User.prototype.instanceMethod = function instanceMethod() {};

    var Customer = ds.define('Customer', {vip: Boolean}, {base: 'User'});

    assert(Customer.prototype instanceof User);
    assert(Customer.staticMethod === User.staticMethod);
    assert(Customer.prototype.instanceMethod === User.prototype.instanceMethod);


    try {
      var Customer1 = ds.define('Customer1', {vip: Boolean}, {base: 'User1'});
    } catch(e) {
      assert(e);
    }

    done();
  });
});

describe('Load models with relations', function () {
    it('should set up relations', function (done) {
        var ds = new DataSource('memory');

        var Post = ds.define('Post', {userId: Number, content: String});
        var User = ds.define('User', {name: String}, {relations: {posts: {type: 'hasMany', model: 'Post'}}});

        assert(User.relations['posts']);
        done();
    });

    it('should set up belongsTo relations', function (done) {
        var ds = new DataSource('memory');

        var User = ds.define('User', {name: String});
        var Post = ds.define('Post', {userId: Number, content: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}});

        assert(Post.relations['user']);
        done();
    });

    it('should set up hasMany and belongsTo relations', function (done) {
        var ds = new DataSource('memory');

        var User = ds.define('User', {name: String}, {relations: {posts: {type: 'hasMany', model: 'Post'}, accounts: {type: 'hasMany', model: 'Account'}}});

        assert(!User.relations['posts']);
        assert(!User.relations['accounts']);

        var Post = ds.define('Post', {userId: Number, content: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}});

        var Account = ds.define('Account', {userId: Number, type: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}});

        assert(Post.relations['user']);
        assert.deepEqual(Post.relations['user'], {
            type: 'belongsTo',
            keyFrom: 'userId',
            keyTo: 'id',
            modelTo: User,
            multiple: false
        });
        assert(User.relations['posts']);
        assert.deepEqual(User.relations['posts'], {
            type: 'hasMany',
            keyFrom: 'id',
            keyTo: 'userId',
            modelTo: Post,
            multiple: true
        });
        assert(User.relations['accounts']);
        assert.deepEqual(User.relations['accounts'], {
            type: 'hasMany',
            keyFrom: 'id',
            keyTo: 'userId',
            modelTo: Account,
            multiple: true
        });

        done();
    });

    it('should throw if a relation is missing type', function (done) {
        var ds = new DataSource('memory');

        var Post = ds.define('Post', {userId: Number, content: String});

        try {
            var User = ds.define('User', {name: String}, {relations: {posts: {model: 'Post'}}});
        } catch (e) {
            done();
        }

    });

    it('should throw if the relation type is invalid', function (done) {
        var ds = new DataSource('memory');

        var Post = ds.define('Post', {userId: Number, content: String});

        try {
            var User = ds.define('User', {name: String}, {relations: {posts: {type: 'hasXYZ', model: 'Post'}}});
        } catch (e) {
            done();
        }

    });

    it('should handle hasMany through', function (done) {
        var ds = new DataSource('memory');
        var Physician = ds.createModel('Physician', {
            name: String
        }, {relations: {patients: {model: 'Patient', type: 'hasMany', through: 'Appointment'}}});

        var Patient = ds.createModel('Patient', {
            name: String
        }, {relations: {physicians: {model: 'Physician', type: 'hasMany', through: 'Appointment'}}});

        assert(!Physician.relations['patients']); // Appointment hasn't been resolved yet
        assert(!Patient.relations['physicians']); // Appointment hasn't been resolved yet

        var Appointment = ds.createModel('Appointment', {
            physicianId: Number,
            patientId: Number,
            appointmentDate: Date
        }, {relations: {patient: {type: 'belongsTo', model: 'Patient'}, physician: {type: 'belongsTo', model: 'Physician'}}});

        assert(Physician.relations['patients']);
        assert(Patient.relations['physicians']);
        done();
    });

    it('should set up relations after attach', function (done) {
        var ds = new DataSource('memory');
        var modelBuilder = new ModelBuilder();

        var Post = modelBuilder.define('Post', {userId: Number, content: String});
        var User = modelBuilder.define('User', {name: String}, {relations: {posts: {type: 'hasMany', model: 'Post'}}});

        assert(!User.relations['posts']);
        Post.attachTo(ds);
        User.attachTo(ds);
        assert(User.relations['posts']);
        done();
    });


});

describe('Load models from json', function () {
    it('should be able to define models from json', function () {
        var path = require('path'),
            fs = require('fs');


        /**
         * Load LDL schemas from a json doc
         * @param schemaFile The dataSource json file
         * @returns A map of schemas keyed by name
         */
        function loadSchemasSync(schemaFile, dataSource) {
            // Set up the data source
            if (!dataSource) {
                dataSource = new DataSource('memory');
            }

            // Read the dataSource JSON file
            var schemas = JSON.parse(fs.readFileSync(schemaFile));

            return dataSource.modelBuilder.buildModels(schemas);

        }

        var models = loadSchemasSync(path.join(__dirname, 'test1-schemas.json'));

        models.should.have.property('AnonymousModel_0');
        models.AnonymousModel_0.should.have.property('modelName', 'AnonymousModel_0');

        var m1 = new models.AnonymousModel_0({title: 'Test'});
        m1.should.have.property('title', 'Test');
        m1.should.have.property('author', 'Raymond');

        models = loadSchemasSync(path.join(__dirname, 'test2-schemas.json'));
        models.should.have.property('Address');
        models.should.have.property('Account');
        models.should.have.property('Customer');
        for (var s in models) {
            var m = models[s];
            assert(new m());
        }
    });

    it('should be able to extend models', function (done) {
        var modelBuilder = new ModelBuilder();

        var User = modelBuilder.define('User', {
            name: String,
            bio: ModelBuilder.Text,
            approved: Boolean,
            joinedAt: Date,
            age: Number
        });

        var Customer = User.extend('Customer', {customerId: {type: String, id: true}});

        var customer = new Customer({name: 'Joe', age: 20, customerId: 'c01'});

        customer.should.be.a('object').and.have.property('name', 'Joe');
        customer.should.have.property('name', 'Joe');
        customer.should.have.property('age', 20);
        customer.should.have.property('customerId', 'c01');
        customer.should.not.have.property('bio');

        // The properties are defined at prototype level
        assert.equal(Object.keys(customer).length, 0);
        var count = 0;
        for(var p in customer) {
            if(typeof customer[p] !== 'function') {
                count++;
            }
        }
        assert.equal(count, 7); // Please note there is an injected id from User prototype
        assert.equal(Object.keys(customer.toObject()).length, 6);

        done(null, customer);
    });
});

describe('DataSource constructor', function(){
    it('Takes url as the settings', function() {
        var ds = new DataSource('memory://localhost/mydb?x=1');
        assert.equal(ds.connector.name, 'memory');
    });

    it('Takes connector name', function() {
        var ds = new DataSource('memory');
        assert.equal(ds.connector.name, 'memory');
    });

    it('Takes settings object', function() {
        var ds = new DataSource({connector: 'memory'});
        assert.equal(ds.connector.name, 'memory');
    });

    it('Takes settings object and name', function() {
        var ds = new DataSource('x', {connector: 'memory'});
        assert.equal(ds.connector.name, 'memory');
    });
});