/* jshint camelcase: false */
/* jshint expr: true */
var chai      = require('chai')
  , Sequelize = require('../index')
  , expect    = chai.expect
  , Support   = require(__dirname + '/support')
  , DataTypes = require(__dirname + "/../lib/data-types")
  , dialect   = Support.getTestDialect()
  , config    = require(__dirname + "/config/config")
  , sinon     = require('sinon')
  , datetime  = require('chai-datetime')
  , _         = require('lodash')
  , moment    = require('moment')
  , async     = require('async')

chai.use(datetime)
chai.Assertion.includeStack = true

describe(Support.getTestDialectTeaser("DAOFactory"), function () {
  beforeEach(function(done) {
    this.User = this.sequelize.define('User', {
      username:     DataTypes.STRING,
      secretValue:  DataTypes.STRING,
      data:         DataTypes.STRING,
      intVal:       DataTypes.INTEGER,
      theDate:      DataTypes.DATE,
      aBool:        DataTypes.BOOLEAN
    })

    this.User.sync({ force: true }).success(function() {
      done()
    })
  })

  describe('constructor', function() {
    it("uses the passed dao name as tablename if freezeTableName", function(done) {
      var User = this.sequelize.define('FrozenUser', {}, { freezeTableName: true })
      expect(User.tableName).to.equal('FrozenUser')
      done()
    })

    it("uses the pluralized dao name as tablename unless freezeTableName", function(done) {
      var User = this.sequelize.define('SuperUser', {}, { freezeTableName: false })
      expect(User.tableName).to.equal('SuperUsers')
      done()
    })

    it("uses checks to make sure dao factory isnt leaking on multiple define", function(done) {
      this.sequelize.define('SuperUser', {}, { freezeTableName: false })
      var factorySize = this.sequelize.daoFactoryManager.all.length

      this.sequelize.define('SuperUser', {}, { freezeTableName: false })
      var factorySize2 = this.sequelize.daoFactoryManager.all.length

      expect(factorySize).to.equal(factorySize2)
      done()
    })

    it("attaches class and instance methods", function(done) {
      var User = this.sequelize.define('UserWithClassAndInstanceMethods', {}, {
        classMethods: { doSmth: function(){ return 1 } },
        instanceMethods: { makeItSo: function(){ return 2}}
      })

      expect(User.doSmth).to.exist
      expect(User.doSmth()).to.equal(1)
      expect(User.makeItSo).not.to.exist

      expect(User.build().doSmth).not.to.exist
      expect(User.build().makeItSo).to.exist
      expect(User.build().makeItSo()).to.equal(2)
      done()
    })

    it('allows us us to predefine the ID column with our own specs', function(done) {
      var User = this.sequelize.define('UserCol', {
        id: {
          type: Sequelize.STRING,
          defaultValue: 'User',
          primaryKey: true
        }
      })

      User.sync({ force: true }).success(function() {
        User.create({id: 'My own ID!'}).success(function(user) {
          expect(user.id).to.equal('My own ID!')
          done()
        })
      })
    })

    it("throws an error if 2 autoIncrements are passed", function(done) {
      var self = this
      expect(function() {
        self.sequelize.define('UserWithTwoAutoIncrements', {
          userid:    { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
          userscore: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }
        })
      }).to.throw(Error, 'Invalid DAO definition. Only one autoincrement field allowed.')
      done()
    })

    it('throws an error if a custom model-wide validation is not a function', function(done) {
      var self = this
      expect(function() {
        self.sequelize.define('Foo', {
          field: Sequelize.INTEGER
        }, {
          validate: {
            notFunction: 33
          }
        })
      }).to.throw(Error, 'Members of the validate option must be functions. Model: Foo, error with validate member notFunction')
      done()
    })

    it('throws an error if a custom model-wide validation has the same name as a field', function(done) {
      var self = this
      expect(function() {
        self.sequelize.define('Foo', {
          field: Sequelize.INTEGER
        }, {
          validate: {
            field: function() {}
          }
        })
      }).to.throw(Error, 'A model validator function must not have the same name as a field. Model: Foo, field/validation name: field')
      done()
    })

    it('should allow me to set a default value for createdAt and updatedAt', function(done) {
      var UserTable = this.sequelize.define('UserCol', {
        aNumber: Sequelize.INTEGER,
        createdAt: {
          type: Sequelize.DATE,
          defaultValue: moment('2012-01-01').toDate()
        },
        updatedAt: {
          type: Sequelize.DATE,
          defaultValue: moment('2012-01-02').toDate()
        }
      }, { timestamps: true })

      UserTable.sync({ force: true }).success(function() {
        UserTable.create({aNumber: 5}).success(function(user) {
          UserTable.bulkCreate([
            {aNumber: 10},
            {aNumber: 12}
          ]).success(function() {
            UserTable.all({where: {aNumber: { gte: 10 }}}).success(function(users) {
              expect(moment(user.createdAt).format('YYYY-MM-DD')).to.equal('2012-01-01')
              expect(moment(user.updatedAt).format('YYYY-MM-DD')).to.equal('2012-01-02')
              users.forEach(function(u) {
                expect(moment(u.createdAt).format('YYYY-MM-DD')).to.equal('2012-01-01')
                expect(moment(u.updatedAt).format('YYYY-MM-DD')).to.equal('2012-01-02')
              })
              done()
            })
          })
        })
      })
    })

    it('should allow me to set a function as default value', function(done) {
      var defaultFunction = sinon.stub().returns(5)
      var UserTable = this.sequelize.define('UserCol', {
        aNumber: {
          type: Sequelize.INTEGER,
          defaultValue: defaultFunction
        }
      }, { timestamps: true })

      UserTable.sync({ force: true }).success(function() {
        UserTable.create().success(function(user) {
            UserTable.create().success(function(user2) {
              expect(user.aNumber).to.equal(5)
              expect(user2.aNumber).to.equal(5)
              expect(defaultFunction.callCount).to.equal(2)
              done()
            })
          })
      })
    })

    it('should allow me to override updatedAt, createdAt, and deletedAt fields', function(done) {
      var UserTable = this.sequelize.define('UserCol', {
        aNumber: Sequelize.INTEGER
      }, {
        timestamps: true,
        updatedAt: 'updatedOn',
        createdAt: 'dateCreated',
        deletedAt: 'deletedAtThisTime',
        paranoid: true
      })

      UserTable.sync({force: true}).success(function() {
        UserTable.create({aNumber: 4}).success(function(user) {
          expect(user.updatedOn).to.exist
          expect(user.dateCreated).to.exist
          user.destroy().success(function(user) {
            expect(user.deletedAtThisTime).to.exist
            done()
          })
        })
      })
    })

    it('should allow me to override updatedAt, createdAt, and deletedAt fields with underscored being true', function(done) {
      var UserTable = this.sequelize.define('UserCol', {
        aNumber: Sequelize.INTEGER
      }, {
        timestamps: true,
        updatedAt: 'updatedOn',
        createdAt: 'dateCreated',
        deletedAt: 'deletedAtThisTime',
        paranoid: true,
        underscored: true
      })

      UserTable.sync({force: true}).success(function() {
        UserTable.create({aNumber: 4}).success(function(user) {
          expect(user.updated_on).to.exist
          expect(user.date_created).to.exist
          user.destroy().success(function(user) {
            expect(user.deleted_at_this_time).to.exist
            done()
          })
        })
      })
    })

    it('should work with both paranoid and underscored being true', function(done) {
      var UserTable = this.sequelize.define('UserCol', {
        aNumber: Sequelize.INTEGER
      }, {
        paranoid: true,
        underscored: true
      })

      UserTable.sync({force: true}).success(function() {
        UserTable.create({aNumber: 30}).success(function(user) {
          UserTable.count().success(function(c) {
            expect(c).to.equal(1)
            done()
          })
        })
      })
    })
  })

  describe('build', function() {
    it("doesn't create database entries", function(done) {
      this.User.build({ username: 'John Wayne' })
      this.User.all().success(function(users) {
        expect(users).to.have.length(0)
        done()
      })
    })

    it("fills the objects with default values", function(done) {
      var Task = this.sequelize.define('TaskBuild', {
        title:  {type: Sequelize.STRING, defaultValue: 'a task!'},
        foo:    {type: Sequelize.INTEGER, defaultValue: 2},
        bar:    {type: Sequelize.DATE},
        foobar: {type: Sequelize.TEXT, defaultValue: 'asd'},
        flag:   {type: Sequelize.BOOLEAN, defaultValue: false}
      })
      expect(Task.build().title).to.equal('a task!')
      expect(Task.build().foo).to.equal(2)
      expect(Task.build().bar).to.not.be.ok
      expect(Task.build().foobar).to.equal('asd')
      expect(Task.build().flag).to.be.false
      done()
    })

    it("fills the objects with default values", function(done) {
      var Task = this.sequelize.define('TaskBuild', {
        title:  {type: Sequelize.STRING, defaultValue: 'a task!'},
        foo:    {type: Sequelize.INTEGER, defaultValue: 2},
        bar:    {type: Sequelize.DATE},
        foobar: {type: Sequelize.TEXT, defaultValue: 'asd'},
        flag:   {type: Sequelize.BOOLEAN, defaultValue: false}
      }, { timestamps: false })
      expect(Task.build().title).to.equal('a task!')
      expect(Task.build().foo).to.equal(2)
      expect(Task.build().bar).to.not.be.ok
      expect(Task.build().foobar).to.equal('asd')
      expect(Task.build().flag).to.be.false
      done()
    })

    it("stores the passed values in a special variable", function(done) {
      var user = this.User.build({ username: 'John Wayne' })
      expect(user.selectedValues).to.deep.equal({ username: 'John Wayne' })
      done()
    })

    it("attaches getter and setter methods from attribute definition", function(done) {
      var Product = this.sequelize.define('ProductWithSettersAndGetters1', {
        price: {
          type: Sequelize.INTEGER,
          get : function() {
            return 'answer = ' + this.getDataValue('price')
          },
          set : function(v) {
            return this.setDataValue('price', v + 42)
          }
        }
      })

      expect(Product.build({price: 42}).price).to.equal('answer = 84')

      var p = Product.build({price: 1})
      expect(p.price).to.equal('answer = 43');

      p.price = 0
      expect(p.price).to.equal('answer = 42')
      done()
    })

    it("attaches getter and setter methods from options", function(done) {
      var Product = this.sequelize.define('ProductWithSettersAndGetters2', {
        priceInCents: Sequelize.INTEGER
      },{
        setterMethods: {
          price: function(value) {
            this.dataValues.priceInCents = value * 100
          }
        },
        getterMethods: {
          price: function() {
            return '$' + (this.getDataValue('priceInCents') / 100)
          },

          priceInCents: function() {
            return this.dataValues.priceInCents
          }
        }
      });

      expect(Product.build({price: 20}).priceInCents).to.equal(20 * 100)
      expect(Product.build({priceInCents: 30 * 100}).price).to.equal('$' + 30)
      done()
    })

    it("attaches getter and setter methods from options only if not defined in attribute", function(done) {
      var Product = this.sequelize.define('ProductWithSettersAndGetters3', {
        price1: {
          type: Sequelize.INTEGER,
          set : function(v) { this.setDataValue('price1', v * 10) }
        },
        price2: {
          type: Sequelize.INTEGER,
          get : function() { return this.getDataValue('price2') * 10 }
        }
      },{
        setterMethods: {
          price1: function(v) { this.setDataValue('price1', v * 100) }
        },
        getterMethods: {
          price2: function() { return '$' + this.getDataValue('price2') }
        }
      });

      var p = Product.build({ price1: 1, price2: 2 })

      expect(p.price1).to.equal(10)
      expect(p.price2).to.equal(20)
      done()
    })
  })

  describe('findOrInitialize', function() {
    it('supports transactions', function(done) {
      Support.prepareTransactionTest(this.sequelize, function(sequelize) {
        var User = sequelize.define('User', { username: Sequelize.STRING, foo: Sequelize.STRING })

        User.sync({ force: true }).success(function() {
          sequelize.transaction(function(t) {
            User.create({ username: 'foo' }, { transaction: t }).success(function() {
              User.findOrInitialize({ username: 'foo' }).success(function(user1) {
                User.findOrInitialize({ username: 'foo' }, { transaction: t }).success(function(user2) {
                  User.findOrInitialize({ username: 'foo' }, { foo: 'asd' }, { transaction: t }).success(function(user3) {
                    expect(user1.isNewRecord).to.be.true
                    expect(user2.isNewRecord).to.be.false
                    expect(user3.isNewRecord).to.be.false
                    t.commit().success(function() { done() })
                  })
                })
              })
            })
          })
        })
      })
    })

    describe('returns an instance if it already exists', function() {
      it('with a single find field', function (done) {
        var self = this

        this.User.create({ username: 'Username' }).success(function (user) {
          self.User.findOrInitialize({
            username: user.username
          }).success(function (_user, initialized) {
            expect(_user.id).to.equal(user.id)
            expect(_user.username).to.equal('Username')
            expect(initialized).to.be.false
            done()
          })
        })
      })

      it('with multiple find fields', function(done) {
        var self = this

        this.User.create({ username: 'Username', data: 'data' }).success(function (user) {
          self.User.findOrInitialize({
            username: user.username,
            data: user.data
          }).success(function (_user, initialized) {
            expect(_user.id).to.equal(user.id)
            expect(_user.username).to.equal('Username')
            expect(_user.data).to.equal('data')
            expect(initialized).to.be.false
            done()
          })
        })
      })

      it('builds a new instance with default value.', function(done) {
        var data = {
            username: 'Username'
          },
          default_values = {
            data: 'ThisIsData'
          }

        this.User.findOrInitialize(data, default_values).success(function(user, initialized) {
          expect(user.id).to.be.null
          expect(user.username).to.equal('Username')
          expect(user.data).to.equal('ThisIsData')
          expect(initialized).to.be.true
          expect(user.isNewRecord).to.be.true
          expect(user.isDirty).to.be.true
          done()
        })
      })
    })
  })

  describe('update', function() {
    it('supports transactions', function(done) {
      Support.prepareTransactionTest(this.sequelize, function(sequelize) {
        var User = sequelize.define('User', { username: Sequelize.STRING })

        User.sync({ force: true }).success(function() {
          User.create({ username: 'foo' }).success(function() {
            sequelize.transaction(function(t) {
              User.update({ username: 'bar' }, {}, { transaction: t }).success(function() {
                User.all().success(function(users1) {
                  User.all({ transaction: t }).success(function(users2) {
                    expect(users1[0].username).to.equal('foo')
                    expect(users2[0].username).to.equal('bar')
                    t.rollback().success(function(){ done() })
                  })
                })
              })
            })
          })
        })
      })
    })

    it('updates the attributes that we select only without updating createdAt', function(done) {
      var User = this.sequelize.define('User1', {
        username: Sequelize.STRING,
        secretValue: Sequelize.STRING
      }, {
          paranoid:true
        })

      User.sync({ force: true }).success(function() {
        User.create({username: 'Peter', secretValue: '42'}).success(function(user) {
          user.updateAttributes({ secretValue: '43' }, ['secretValue']).on('sql', function(sql) {
            expect(sql).to.match(/UPDATE\s+[`"]+User1s[`"]+\s+SET\s+[`"]+secretValue[`"]='43',[`"]+updatedAt[`"]+='[^`",]+'\s+WHERE [`"]+id[`"]+=1/)
            done()
          })
        })
      })
    })

    it('allows sql logging of updated statements', function(done) {
      var User = this.sequelize.define('User', {
        name: Sequelize.STRING,
        bio: Sequelize.TEXT
      }, {
          paranoid:true
        })

      User.sync({ force: true }).success(function() {
        User.create({ name: 'meg', bio: 'none' }).success(function(u) {
          expect(u).to.exist
          expect(u).not.to.be.null
          u.updateAttributes({name: 'brian'}).on('sql', function(sql) {
            expect(sql).to.exist
            expect(sql.toUpperCase().indexOf("UPDATE")).to.be.above(-1)
            done()
          })
        })
      })
    })

    it('updates only values that match filter', function(done) {
      var self = this
        , data = [{ username: 'Peter', secretValue: '42' },
                  { username: 'Paul',  secretValue: '42' },
                  { username: 'Bob',   secretValue: '43' }]

      this.User.bulkCreate(data).success(function() {

        self.User.update({username: 'Bill'}, {secretValue: '42'})
          .success(function() {
            self.User.findAll({order: 'id'}).success(function(users) {
              expect(users.length).to.equal(3)

              users.forEach(function (user) {
                if (user.secretValue == '42') {
                  expect(user.username).to.equal("Bill")
                } else {
                  expect(user.username).to.equal("Bob")
                }
              })

              done()
            })
          })
      })
    })

    it('updates with casting', function (done) {
      var self = this

      this.User.create({
        username: 'John'
      }).success(function(user) {
        self.User.update({username: self.sequelize.cast('1', 'char')}, {username: 'John'}).success(function() {
          self.User.all().success(function(users) {
            expect(users[0].username).to.equal('1')
            done()
          })
        })
      })
    })

    it('updates with function and column value', function (done) {
      var self = this

      this.User.create({
        username: 'John'
      }).success(function(user) {
        self.User.update({username: self.sequelize.fn('upper', self.sequelize.col('username'))}, {username: 'John'}).success(function () {
          self.User.all().success(function(users) {
            expect(users[0].username).to.equal('JOHN')
            done()
          })
        })
      })
    })

    it('sets updatedAt to the current timestamp', function(done) {
      var self = this
        , data = [{ username: 'Peter', secretValue: '42' },
                  { username: 'Paul',  secretValue: '42' },
                  { username: 'Bob',   secretValue: '43' }]

      this.User.bulkCreate(data).success(function() {
        self.User.update({username: 'Bill'}, {secretValue: '42'}).success(function() {
          self.User.findAll({order: 'id'}).success(function(users) {
            expect(users.length).to.equal(3)

            expect(users[0].username).to.equal("Bill")
            expect(users[1].username).to.equal("Bill")
            expect(users[2].username).to.equal("Bob")

            expect(parseInt(+users[0].updatedAt/5000, 10)).to.be.closeTo(parseInt(+new Date()/5000, 10), 1)
            expect(parseInt(+users[1].updatedAt/5000, 10)).to.be.closeTo(parseInt(+new Date()/5000, 10), 1)

            done()
          })
        })
      })
    })
  })

  describe('destroy', function() {
    it('supports transactions', function(done) {
      Support.prepareTransactionTest(this.sequelize, function(sequelize) {
        var User = sequelize.define('User', { username: Sequelize.STRING })

        User.sync({ force: true }).success(function() {
          User.create({ username: 'foo' }).success(function() {
            sequelize.transaction(function(t) {
              User.destroy({}, { transaction: t }).success(function() {
                User.count().success(function(count1) {
                  User.count({ transaction: t }).success(function(count2) {
                    expect(count1).to.equal(1)
                    expect(count2).to.equal(0)
                    t.rollback().success(function(){ done() })
                  })
                })
              })
            })
          })
        })
      })
    })

    it('deletes values that match filter', function(done) {
      var self = this
        , data = [{ username: 'Peter', secretValue: '42' },
                  { username: 'Paul',  secretValue: '42' },
                  { username: 'Bob',   secretValue: '43' }]

      this.User.bulkCreate(data).success(function() {
        self.User.destroy({secretValue: '42'})
          .success(function() {
            self.User.findAll({order: 'id'}).success(function(users) {
              expect(users.length).to.equal(1)
              expect(users[0].username).to.equal("Bob")
              done()
            })
          })
      })
    })

    it('sets deletedAt to the current timestamp if paranoid is true', function(done) {
      var self = this
        , ident = self.sequelize.queryInterface.QueryGenerator.quoteIdentifier
        , escape = self.sequelize.queryInterface.QueryGenerator.quote
        , ParanoidUser = self.sequelize.define('ParanoidUser', {
          username:     Sequelize.STRING,
          secretValue:  Sequelize.STRING,
          data:         Sequelize.STRING,
          intVal:       { type: Sequelize.INTEGER, defaultValue: 1}
        }, {
            paranoid: true
          })
        , data = [{ username: 'Peter', secretValue: '42' },
                  { username: 'Paul',  secretValue: '42' },
                  { username: 'Bob',   secretValue: '43' }]

      ParanoidUser.sync({ force: true }).success(function() {
        ParanoidUser.bulkCreate(data).success(function() {
          // since we save in UTC, let's format to UTC time
          var date = moment().utc().format('YYYY-MM-DD h:mm')
          ParanoidUser.destroy({secretValue: '42'}).success(function() {
            ParanoidUser.findAll({order: 'id'}).success(function(users) {
              expect(users.length).to.equal(1)
              expect(users[0].username).to.equal("Bob")

              self.sequelize.query('SELECT * FROM ' + ident('ParanoidUsers') + ' WHERE ' + ident('deletedAt') + ' IS NOT NULL ORDER BY ' + ident('id'), null, {raw: true}).success(function(users) {
                expect(users[0].username).to.equal("Peter")
                expect(users[1].username).to.equal("Paul")

                if (dialect === "sqlite") {
                  expect(moment(users[0].deletedAt).format('YYYY-MM-DD h:mm')).to.equal(date)
                  expect(moment(users[1].deletedAt).format('YYYY-MM-DD h:mm')).to.equal(date)
                } else {
                  expect(moment(users[0].deletedAt).utc().format('YYYY-MM-DD h:mm')).to.equal(date)
                  expect(moment(users[1].deletedAt).utc().format('YYYY-MM-DD h:mm')).to.equal(date)
                }
                done()
              })
            })
          })
        })
      })
    })

    describe("can't find records marked as deleted with paranoid being true", function() {
      it('with the DAOFactory', function(done) {
        var User = this.sequelize.define('UserCol', {
          username: Sequelize.STRING
        }, { paranoid: true })

        User.sync({ force: true }).success(function() {
          User.bulkCreate([
            {username: 'Toni'},
            {username: 'Tobi'},
            {username: 'Max'}
          ]).success(function() {
            User.find(1).success(function(user) {
              user.destroy().success(function() {
                User.find(1).success(function(user) {
                  expect(user).to.be.null
                  User.count().success(function(cnt) {
                    expect(cnt).to.equal(2)
                    User.all().success(function(users) {
                      expect(users).to.have.length(2)
                      done()
                    })
                  })
                })
              })
            })
          })
        })
      })
    })

    it('should delete a paranoid record if I set force to true', function(done) {
      var self = this
      var User = this.sequelize.define('paranoiduser', {
        username: Sequelize.STRING
      }, { paranoid: true })

      User.sync({ force: true }).success(function() {
        User.bulkCreate([
          {username: 'Bob'},
          {username: 'Tobi'},
          {username: 'Max'},
          {username: 'Tony'}
        ]).success(function() {
          User.find({where: {username: 'Bob'}}).success(function(user) {
            user.destroy({force: true}).success(function() {
              User.find({where: {username: 'Bob'}}).success(function(user) {
                expect(user).to.be.null
                User.find({where: {username: 'Tobi'}}).success(function(tobi) {
                  tobi.destroy().success(function() {
                    self.sequelize.query('SELECT * FROM paranoidusers WHERE username=\'Tobi\'', null, {raw: true, plain: true}).success(function(result) {
                      expect(result.username).to.equal('Tobi')
                      User.destroy({username: 'Tony'}).success(function() {
                        self.sequelize.query('SELECT * FROM paranoidusers WHERE username=\'Tony\'', null, {raw: true, plain: true}).success(function(result) {
                          expect(result.username).to.equal('Tony')
                          User.destroy({username: ['Tony', 'Max']}, {force: true}).success(function() {
                            self.sequelize.query('SELECT * FROM paranoidusers', null, {raw: true}).success(function(users) {
                              expect(users).to.have.length(1)
                              expect(users[0].username).to.equal('Tobi')
                              done()
                            })
                          })
                        })
                      })
                    })
                  })
                })
              })
            })
          })
        })
      })
    })
  })

  describe('equals', function() {
    it("correctly determines equality of objects", function(done) {
      this.User.create({username: 'hallo', data: 'welt'}).success(function(u) {
        expect(u.equals(u)).to.be.ok
        done()
      })
    })

    // sqlite can't handle multiple primary keys
    if(dialect !== "sqlite") {
      it("correctly determines equality with multiple primary keys", function(done) {
        var userKeys = this.sequelize.define('userkeys', {
          foo: {type: Sequelize.STRING, primaryKey: true},
          bar: {type: Sequelize.STRING, primaryKey: true},
          name: Sequelize.STRING,
          bio: Sequelize.TEXT
        })

        userKeys.sync({ force: true }).success(function() {
          userKeys.create({foo: '1', bar: '2', name: 'hallo', bio: 'welt'}).success(function(u) {
            expect(u.equals(u)).to.be.ok
            done()
          })
        })
      })
    }
  })

  describe('equalsOneOf', function() {
    // sqlite can't handle multiple primary keys
    if (dialect !== "sqlite") {
      beforeEach(function(done) {
        this.userKey = this.sequelize.define('userKeys', {
          foo: {type: Sequelize.STRING, primaryKey: true},
          bar: {type: Sequelize.STRING, primaryKey: true},
          name: Sequelize.STRING,
          bio: Sequelize.TEXT
        })

        this.userKey.sync({ force: true }).success(function(){
          done()
        })
      })

      it('determines equality if one is matching', function(done) {
        this.userKey.create({foo: '1', bar: '2', name: 'hallo', bio: 'welt'}).success(function(u) {
          expect(u.equalsOneOf([u, {a: 1}])).to.be.ok
          done()
        })
      })

      it("doesn't determine equality if none is matching", function(done) {
        this.userKey.create({foo: '1', bar: '2', name: 'hallo', bio: 'welt'}).success(function(u) {
          expect(u.equalsOneOf([{b: 2}, {a: 1}])).to.not.be.ok
          done()
        })
      })
    }
  })

  describe('count', function() {
    it('supports transactions', function(done) {
      Support.prepareTransactionTest(this.sequelize, function(sequelize) {
        var User = sequelize.define('User', { username: Sequelize.STRING })

        User.sync({ force: true }).success(function() {
          sequelize.transaction(function(t) {
            User.create({ username: 'foo' }, { transaction: t }).success(function() {
              User.count().success(function(count1) {
                User.count({ transaction: t }).success(function(count2) {
                  expect(count1).to.equal(0)
                  expect(count2).to.equal(1)
                  t.rollback().success(function(){ done() })
                })
              })
            })
          })
        })
      })
    })

    it('counts all created objects', function(done) {
      var self = this
      this.User.bulkCreate([{username: 'user1'}, {username: 'user2'}]).success(function() {
        self.User.count().success(function(count) {
          expect(count).to.equal(2)
          done()
        })
      })
    })

    it('does not modify the passed arguments', function (done) {
      var options = { where: ['username = ?', 'user1']}

      this.User.count(options).success(function(count) {
        expect(options).to.deep.equal({ where: ['username = ?', 'user1']})
        done()
      })
    })

    it('allows sql logging', function(done) {
      this.User.count().on('sql', function(sql) {
        expect(sql).to.exist
        expect(sql.toUpperCase().indexOf("SELECT")).to.be.above(-1)
        done()
      })
    })

    it('filters object', function(done) {
      var self = this
      this.User.create({username: 'user1'}).success(function() {
        self.User.create({username: 'foo'}).success(function() {
          self.User.count({where: "username LIKE '%us%'"}).success(function(count) {
            expect(count).to.equal(1)
            done()
          })
        })
      })
    })
  })

  describe('min', function() {
    beforeEach(function(done) {
      var self = this
      this.UserWithAge = this.sequelize.define('UserWithAge', {
        age: Sequelize.INTEGER
      })

      this.UserWithDec = this.sequelize.define('UserWithDec', {
        value: Sequelize.DECIMAL(10, 3)
      })

      this.UserWithAge.sync({ force: true }).success(function() {
        self.UserWithDec.sync({ force: true }).success(function() {
          done()
        })
      })
    })

    it('supports transactions', function(done) {
      Support.prepareTransactionTest(this.sequelize, function(sequelize) {
        var User = sequelize.define('User', { age: Sequelize.INTEGER })

        User.sync({ force: true }).success(function() {
          sequelize.transaction(function(t) {
            User.bulkCreate([{ age: 2 }, { age: 5 }, { age: 3 }], { transaction: t }).success(function() {
              User.min('age').success(function(min1) {
                User.min('age', { transaction: t }).success(function(min2) {
                  expect(min1).to.be.not.ok
                  expect(min2).to.equal(2)
                  t.rollback().success(function(){ done() })
                })
              })
            })
          })
        })
      })
    })

    it("should return the min value", function(done) {
      var self = this
      this.UserWithAge.bulkCreate([{age: 3}, { age: 2 }]).success(function() {
        self.UserWithAge.min('age').success(function(min) {
          expect(min).to.equal(2)
          done()
        })
      })
    })

    it('allows sql logging', function(done) {
      this.UserWithAge.min('age').on('sql', function(sql) {
        expect(sql).to.exist
        expect(sql.toUpperCase().indexOf("SELECT")).to.be.above(-1)
        done()
      })
    })

    it("should allow decimals in min", function(done){
      var self = this
      this.UserWithDec.bulkCreate([{value: 5.5}, {value: 3.5}]).success(function(){
        self.UserWithDec.min('value').success(function(min){
          expect(min).to.equal(3.5)
          done()
        })
      })
    })
  })

  describe('max', function() {
    beforeEach(function(done) {
      var self = this
      this.UserWithAge = this.sequelize.define('UserWithAge', {
        age: Sequelize.INTEGER,
        order: Sequelize.INTEGER
      })

      this.UserWithDec = this.sequelize.define('UserWithDec', {
        value: Sequelize.DECIMAL(10, 3)
      })

      this.UserWithAge.sync({ force: true }).success(function() {
        self.UserWithDec.sync({ force: true }).success(function() {
          done()
        })
      })
    })

    it('supports transactions', function(done) {
      Support.prepareTransactionTest(this.sequelize, function(sequelize) {
        var User = sequelize.define('User', { age: Sequelize.INTEGER })

        User.sync({ force: true }).success(function() {
          sequelize.transaction(function(t) {
            User.bulkCreate([{ age: 2 }, { age: 5 }, { age: 3 }], { transaction: t }).success(function() {
              User.max('age').success(function(min1) {
                User.max('age', { transaction: t }).success(function(min2) {
                  expect(min1).to.be.not.ok
                  expect(min2).to.equal(5)
                  t.rollback().success(function(){ done() })
                })
              })
            })
          })
        })
      })
    })

    it("should return the max value for a field named the same as an SQL reserved keyword", function(done) {
      var self = this
      this.UserWithAge.bulkCreate([{age: 2, order: 3}, {age: 3, order: 5}]).success(function(){
        self.UserWithAge.max('order').success(function(max) {
          expect(max).to.equal(5)
          done()
        })
      })
    })

    it("should return the max value", function(done) {
      var self = this
      self.UserWithAge.bulkCreate([{age: 2}, {age: 3}]).success(function() {
        self.UserWithAge.max('age').success(function(max) {
          expect(max).to.equal(3)
          done()
        })
      })
    })

    it("should allow decimals in max", function(done) {
      var self = this
      this.UserWithDec.bulkCreate([{value: 3.5}, {value: 5.5}]).success(function(){
        self.UserWithDec.max('value').success(function(max){
          expect(max).to.equal(5.5)
          done()
        })
      })
    })

    it('allows sql logging', function(done) {
      this.UserWithAge.max('age').on('sql', function(sql) {
        expect(sql).to.exist
        expect(sql.toUpperCase().indexOf("SELECT")).to.be.above(-1)
        done()
      })
    })
  })

  describe('schematic support', function() {
    beforeEach(function(done){
      var self = this;

      this.UserPublic = this.sequelize.define('UserPublic', {
        age: Sequelize.INTEGER
      })

      this.UserSpecial = this.sequelize.define('UserSpecial', {
        age: Sequelize.INTEGER
      })

      self.sequelize.dropAllSchemas().success(function(){
        self.sequelize.createSchema('schema_test').success(function(){
          self.sequelize.createSchema('special').success(function(){
            self.UserSpecial.schema('special').sync({force: true}).success(function(UserSpecialSync) {
              self.UserSpecialSync = UserSpecialSync
              done()
            })
          })
        })
      })
    })

    it("should be able to list schemas", function(done){
      this.sequelize.showAllSchemas().success(function(schemas) {
        expect(schemas).to.exist
        expect(schemas[0]).to.be.instanceof(Array)
        // sqlite & MySQL doesn't actually create schemas unless Model.sync() is called
        // Postgres supports schemas natively
        expect(schemas[0]).to.have.length((dialect === "postgres" ? 2 : 1))
        done()
      })
    })

    if (Support.dialectIsMySQL() || dialect === "sqlite") {
      it("should take schemaDelimiter into account if applicable", function(done){
        var UserSpecialUnderscore = this.sequelize.define('UserSpecialUnderscore', {age: Sequelize.INTEGER}, {schema: 'hello', schemaDelimiter: '_'})
        var UserSpecialDblUnderscore = this.sequelize.define('UserSpecialDblUnderscore', {age: Sequelize.INTEGER})
        UserSpecialUnderscore.sync({force: true}).success(function(User){
          UserSpecialDblUnderscore.schema('hello', '__').sync({force: true}).success(function(DblUser){
            DblUser.create({age: 3}).on('sql', function(dblSql){
              User.create({age: 3}).on('sql', function(sql){
                expect(dblSql).to.exist
                expect(dblSql.indexOf('INSERT INTO `hello__UserSpecialDblUnderscores`')).to.be.above(-1)
                expect(sql).to.exist
                expect(sql.indexOf('INSERT INTO `hello_UserSpecialUnderscores`')).to.be.above(-1)
                done()
              })
            })
          })
        })
      })
    }

    it("should describeTable using the default schema settings", function(done) {
      var self = this
        , UserPublic = this.sequelize.define('Public', {
        username: Sequelize.STRING
      })

      var _done = _.after(2, function() {
        done()
      })

      UserPublic.sync({ force: true }).success(function() {
        UserPublic.schema('special').sync({ force: true }).success(function() {
          self.sequelize.queryInterface.describeTable('Publics')
          .on('sql', function(sql) {
            if (dialect === "sqlite" || Support.dialectIsMySQL()) {
              expect(sql).to.not.contain('special')
              _done()
            }
          })
          .success(function(table) {
            if (dialect === "postgres") {
              expect(table.id.defaultValue).to.not.contain('special')
              _done()
            }

            self.sequelize.queryInterface.describeTable('Publics', 'special')
            .on('sql', function(sql) {
              if (dialect === "sqlite" || Support.dialectIsMySQL()) {
                expect(sql).to.contain('special')
                _done()
              }
            })
            .success(function(table) {
              if (dialect === "postgres") {
                expect(table.id.defaultValue).to.contain('special')
                _done()
              }
            })
          })
        })
      })
    })

    it('should be able to reference a table with a schema set', function(done) {
      var self = this
      var sequelize = this.sequelize

      var UserPub = this.sequelize.define('UserPub', {
        username: Sequelize.STRING
      }, { schema: 'prefix' })

      var ItemPub = this.sequelize.define('ItemPub', {
        name: Sequelize.STRING
      }, { schema: 'prefix' })

      UserPub.hasMany(ItemPub, {
        foreignKeyConstraint: true
      })

      var run = function() {
        UserPub.sync({ force: true }).success(function() {
          ItemPub.sync({ force: true }).on('sql', function(sql) {
            if (dialect === "postgres") {
              expect(sql).to.match(/REFERENCES\s+"prefix"\."UserPubs" \("id"\)/)
            } else {
              expect(sql).to.match(/REFERENCES\s+`prefix\.UserPubs` \(`id`\)/)
            }
            done()
          })
        })
      }

      if (dialect === "postgres") {
        this.sequelize.queryInterface.createSchema('prefix').success(function() {
          run.call(self)
        })
      } else {
        run.call(self)
      }
    })

    it("should be able to create and update records under any valid schematic", function(done){
      var self = this

      self.UserPublic.sync({ force: true }).success(function(UserPublicSync){
        UserPublicSync.create({age: 3}).on('sql', function(UserPublic){
          self.UserSpecialSync.schema('special').create({age: 3})
          .on('sql', function(UserSpecial){
            expect(UserSpecial).to.exist
            expect(UserPublic).to.exist
            if (dialect === "postgres") {
              expect(self.UserSpecialSync.getTableName()).to.equal('"special"."UserSpecials"');
              expect(UserSpecial.indexOf('INSERT INTO "special"."UserSpecials"')).to.be.above(-1)
              expect(UserPublic.indexOf('INSERT INTO "UserPublics"')).to.be.above(-1)
            } else if (dialect === "sqlite") {
              expect(self.UserSpecialSync.getTableName()).to.equal('`special`.`UserSpecials`');
              expect(UserSpecial.indexOf('INSERT INTO `special.UserSpecials`')).to.be.above(-1)
              expect(UserPublic.indexOf('INSERT INTO `UserPublics`')).to.be.above(-1)
            } else {
              expect(self.UserSpecialSync.getTableName()).to.equal('`special.UserSpecials`');
              expect(UserSpecial.indexOf('INSERT INTO `special.UserSpecials`')).to.be.above(-1)
              expect(UserPublic.indexOf('INSERT INTO `UserPublics`')).to.be.above(-1)
            }
          })
          .success(function(UserSpecial){
            UserSpecial.updateAttributes({age: 5})
            .on('sql', function(user){
              expect(user).to.exist
              if (dialect === "postgres") {
                expect(user.indexOf('UPDATE "special"."UserSpecials"')).to.be.above(-1)
              } else {
                expect(user.indexOf('UPDATE `special.UserSpecials`')).to.be.above(-1)
              }
              done()
            })
          })
        })
      })
    })
  })

  describe('references', function() {
    beforeEach(function(done) {
      var self = this

      this.sequelize.getQueryInterface().dropTable('posts', { force: true }).success(function() {
        self.sequelize.getQueryInterface().dropTable('authors', { force: true }).success(function() {
          self.Author = self.sequelize.define('author', { firstName: Sequelize.STRING })
          self.Author.sync().success(function() {
            done()
          })
        })
      })
    })

    afterEach(function(done) {
      var self = this

      this.sequelize.getQueryInterface().dropTable('posts', { force: true }).success(function() {
        self.sequelize.getQueryInterface().dropTable('authors', { force: true }).success(function() {
          done()
        })
      })
    })

    it('uses an existing dao factory and references the author table', function(done) {
      var self    = this
        , Post    = this.sequelize.define('post', {
            title:    Sequelize.STRING,
            authorId: {
              type:          Sequelize.INTEGER,
              references:    this.Author,
              referencesKey: "id"
            }
          })

      this.Author.hasMany(Post)
      Post.belongsTo(this.Author)

      // The posts table gets dropped in the before filter.
      Post.sync().on('sql', function(sql) {
        if (dialect === 'postgres') {
          expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/)
        } else if (Support.dialectIsMySQL()) {
          expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/)
        } else if (dialect === 'sqlite') {
          expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/)
        } else {
          throw new Error('Undefined dialect!')
        }

        done()
      })
    })

    it('uses a table name as a string and references the author table', function(done) {
      var self    = this
        , Post    = self.sequelize.define('post', {
            title:    Sequelize.STRING,
            authorId: {
              type:          Sequelize.INTEGER,
              references:    'authors',
              referencesKey: "id"
            }
          })

      this.Author.hasMany(Post)
      Post.belongsTo(this.Author)

      // The posts table gets dropped in the before filter.
      Post.sync().on('sql', function(sql) {
        if (dialect === 'postgres') {
          expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/)
        } else if (Support.dialectIsMySQL()) {
          expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/)
        } else if (dialect === 'sqlite') {
          expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/)
        } else {
          throw new Error('Undefined dialect!')
        }

        done()
      })
    })

    it("emits an error event as the referenced table name is invalid", function(done) {
      var self    = this
        , Post    = this.sequelize.define('post', {
            title:    Sequelize.STRING,
            authorId: {
              type:          Sequelize.INTEGER,
              references:    '4uth0r5',
              referencesKey: "id"
            }
          })

      this.Author.hasMany(Post)
      Post.belongsTo(this.Author)

      // The posts table gets dropped in the before filter.
      Post.sync().success(function() {
        if (dialect === 'sqlite') {
          // sorry ... but sqlite is too stupid to understand whats going on ...
          expect(1).to.equal(1)
          done()
        } else {
          // the parser should not end up here ...
          expect(2).to.equal(1)
          done()
        }
      }).error(function(err) {
        if (Support.dialectIsMySQL(true)) {
          expect(err.message).to.match(/ER_CANNOT_ADD_FOREIGN|ER_CANT_CREATE_TABLE/)
        } else if (dialect === 'mariadb') {
          expect(err.message).to.match(/Can\'t create table/)
        } else if (dialect === 'sqlite') {
          // the parser should not end up here ... see above
          expect(1).to.equal(2)
        } else if (dialect === 'postgres') {
          expect(err.message).to.match(/relation "4uth0r5" does not exist/)
        } else {
          throw new Error('Undefined dialect!')
        }

        done()
      })
    })
  })

  describe("syntax sugar", function() {
    before(function(done) {
      this.User = this.sequelize.define("user", {
        username:  Sequelize.STRING,
        firstName: Sequelize.STRING,
        lastName:  Sequelize.STRING
      })

      this.User.sync({ force: true }).success(function() {
        done()
      })
    })

    describe("dataset", function() {
      it("returns a node-sql instance with the correct dialect", function() {
        var _dialect = dialect === 'mariadb' ? 'mysql' : dialect

        expect(this.User.dataset().sql.dialectName).to.equal(_dialect)
      })

      it("allows me to generate sql queries", function() {
        var query = this.User.dataset().select("username").toQuery()
        expect(Object.keys(query)).to.eql(['text', 'values'])
      })
    })

    describe("select", function() {
      it("sets .select() as an alias to .dataset().select()", function() {
        var query1 = this.User.select("username").toQuery()
          , query2 = this.User.dataset().select("username").toQuery()

        expect(query1.text).to.equal(query2.text)
      })
    })

    describe("toSql", function() {
      it("transforms the node-sql instance into a proper sql string", function() {
        var sql    = this.User.select("username").toSql()
        var sqlMap = {
          postgres: 'SELECT username FROM "' + this.User.tableName + '";',
          mariadb: 'SELECT username FROM `' + this.User.tableName + '`;',
          mysql:    'SELECT username FROM `' + this.User.tableName + '`;',
          sqlite:   'SELECT username FROM "' + this.User.tableName + '";'
        }
        expect(sql).to.equal(sqlMap[dialect])
      })

      it("transforms node-sql instances with chaining into a proper sql string", function() {
        var sql    = this.User.select("username").select("firstName").group("username").toSql()
        var sqlMap = {
          postgres: 'SELECT username, firstName FROM "' + this.User.tableName + '" GROUP BY username;',
          mariadb:    'SELECT username, firstName FROM `' + this.User.tableName + '` GROUP BY username;',
          mysql:    'SELECT username, firstName FROM `' + this.User.tableName + '` GROUP BY username;',
          sqlite:   'SELECT username, firstName FROM "' + this.User.tableName + '" GROUP BY username;'
        }
        expect(sql).to.equal(sqlMap[dialect])
      })
    })

    describe("exec", function() {
      beforeEach(function(done) {
        var self = this

        this
          .User
          .sync({ force: true })
          .then(function() { return self.User.create({ username: "foo" }) })
          .then(function() { return self.User.create({ username: "bar" }) })
          .then(function() { return self.User.create({ username: "baz" }) })
          .then(function() { done() })
      })

      it('supports transactions', function(done) {
        Support.prepareTransactionTest(this.sequelize, function(sequelize) {
          var User = sequelize.define('User', { username: Sequelize.STRING })

          User.sync({ force: true }).success(function() {
            sequelize.transaction(function(t) {
              User.create({ username: 'foo' }, { transaction: t }).success(function() {
                User.where({ username: "foo" }).exec().success(function(users1) {
                  User.where({ username: "foo" }).exec({ transaction: t }).success(function(users2) {
                    expect(users1).to.have.length(0)
                    expect(users2).to.have.length(1)
                    t.rollback().success(function() { done() })
                  })
                })
              })
            })
          })
        })
      })

      it("selects all users with name 'foo'", function(done) {
        this
          .User
          .where({ username: "foo" })
          .exec()
          .success(function(users) {
            expect(users).to.have.length(1)
            expect(users[0].username).to.equal("foo")
            done()
          })
      })

      it("returns an instanceof DAO", function(done) {
        var DAO = require(__dirname + "/../lib/dao")

        this.User.where({ username: "foo" }).exec().success(function(users) {
          expect(users[0]).to.be.instanceOf(DAO)
          done()
        })
      })

      it("returns all users in the db", function(done) {
        this.User.select().exec().success(function(users) {
          expect(users).to.have.length(3)
          done()
        })
      })

      it("can handle or queries", function(done) {
        this
          .User
          .where(this.User.dataset().username.equals("bar").or(this.User.dataset().username.equals("baz")))
          .exec()
          .success(function(users) {
            expect(users).to.have.length(2)
            done()
          })
      })
    })
  })

  describe("blob", function() {
    beforeEach(function(done) {
      this.BlobUser = this.sequelize.define("blobUser", {
        data: Sequelize.BLOB
      })

      this.BlobUser.sync({ force: true }).success(function() {
        done()
      })
    })

    describe("buffers", function () {
      it("should be able to take a buffer as parameter to a BLOB field", function (done) {
        this.BlobUser.create({
          data: new Buffer('Sequelize')
        }).success(function (user) {
          expect(user).to.be.ok
          done()
        })
      })

      it("should return a buffer when fetching a blob", function (done) {
        var self = this
        this.BlobUser.create({
          data: new Buffer('Sequelize')
        }).success(function (user) {
          self.BlobUser.find(user.id).success(function (user) {
            expect(user.data).to.be.an.instanceOf(Buffer)
            expect(user.data.toString()).to.have.string('Sequelize')
            done()
          })
        })
      })
    })

    describe("strings", function () {
      it("should be able to take a string as parameter to a BLOB field", function (done) {
        this.BlobUser.create({
          data: 'Sequelize'
        }).success(function (user) {
          expect(user).to.be.ok
          done()
        })
      })

      it("should return a buffer when fetching a BLOB, even when the BLOB was inserted as a string", function (done) {
        var self = this
        this.BlobUser.create({
          data: 'Sequelize'
        }).success(function (user) {
          self.BlobUser.find(user.id).success(function (user) {
            expect(user.data).to.be.an.instanceOf(Buffer)
            expect(user.data.toString()).to.have.string('Sequelize')
            done()
          })
        })
      })
    })
  })
})


get: Utils._.camelize('get_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language))),

set: Utils._.camelize('set_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language)))

keep keep keep keep replace replace keep keep keep keep

get: Utils._.camelize('get_' + this.options.as),

set: Utils._.camelize('set_' + this.options.as)

module.exports = {
  up: function(migration, DataTypes, done) {
    migration
        .createTable('trigger_test', {
          name: DataTypes.STRING,
          updated_at: {
            type: DataTypes.DATE,
            defaultValue: DataTypes.NOW,
            allowNull: false
          }
        })
        .complete(done)
  },
  down: function(migration, DataTypes, done) {
    migration.dropTable('trigger_test').complete(done)
  }
}


module.exports = {
  up: function(migration, DataTypes, done) {
    migration.removeColumn('User', 'shopId').complete(done)
  },

  down: function(migration, DataTypes, done) {
    migration.addColumn('User', 'shopId', { type: DataTypes.INTEGER, allowNull: true }).complete(done)
  }
}


Group.hasOne(User, { foreignKey: 'primaryGroupId', as: 'primaryUsers' })

Group.hasOne(User, { foreignKey: 'secondaryGroupId', as: 'secondaryUsers' })

it('supports transactions', function(done) {

keep keep keep keep replace keep keep keep keep keep

var Utils     = require("./../utils")
  , DataTypes = require('./../data-types')
  , Helpers   = require("./helpers")

module.exports = (function() {
  var HasOne = function(srcDAO, targetDAO, options) {
    this.associationType   = 'HasOne'
    this.source            = srcDAO
    this.target            = targetDAO
    this.options           = options
    this.isSelfAssociation = (this.source.tableName == this.target.tableName)

    if (this.isSelfAssociation && !this.options.foreignKey && !!this.options.as) {
      this.options.foreignKey = Utils._.underscoredIf(Utils.singularize(this.options.as, this.target.options.language) + "Id", this.options.underscored)
    }

    this.associationAccessor = this.isSelfAssociation
      ? Utils.combineTableNames(this.target.tableName, this.options.as || this.target.tableName)
      : this.options.as || this.target.tableName

    this.options.useHooks = options.useHooks

    this.accessors = {
      get: Utils._.camelize('get_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language))),
      set: Utils._.camelize('set_' + (this.options.as || Utils.singularize(this.target.tableName, this.target.options.language)))
    }
  }

  // the id is in the target table
  HasOne.prototype.injectAttributes = function() {
    var newAttributes = {}
      , sourceKeys    = Object.keys(this.source.primaryKeys)
      , keyType       = ((this.source.hasPrimaryKeys && sourceKeys.length === 1) ? this.source.rawAttributes[sourceKeys[0]].type : DataTypes.INTEGER)

    this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName, this.source.options.language) + "Id", this.options.underscored)
    newAttributes[this.identifier] = { type: this.options.keyType || keyType }
    Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, this.options)
    Utils._.defaults(this.target.rawAttributes, newAttributes)

    // Sync attributes and setters/getters to DAO prototype
    this.target.refreshAttributes()
    return this
  }

  HasOne.prototype.injectGetter = function(obj) {
    var self = this
      , smart

    obj[this.accessors.get] = function(params) {
      var primaryKeys = Object.keys(this.daoFactory.primaryKeys)
        , primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : 'id'
        , where = {}
        , id = this[primaryKey] || this.id

      where[self.identifier] = id

      if (!Utils._.isUndefined(params)) {
        if (!Utils._.isUndefined(params.attributes)) {
          params = Utils._.extend({where: where}, params)
        }
      } else {
        params = {where: where}
      }

      smart = Utils.smartWhere(params.where || [], self.target.daoFactoryManager.sequelize.options.dialect)
      smart = Utils.compileSmartWhere.call(self.target, smart, self.target.daoFactoryManager.sequelize.options.dialect)
      if (smart.length > 0) {
        params.where = smart
      }

      return self.target.find(params)
    }

    return this
  }

  HasOne.prototype.injectSetter = function(obj) {
    var self = this

    obj[this.accessors.set] = function(associatedObject, options) {
      var instance     = this
        , instanceKeys = Object.keys(instance.daoFactory.primaryKeys)
        , instanceKey  = instanceKeys.length === 1 ? instanceKeys[0] : 'id'

      return new Utils.CustomEventEmitter(function(emitter) {
        instance[self.accessors.get]().success(function(oldObj) {
          if (oldObj) {
            oldObj[self.identifier] = null
            oldObj
              .save(
                Utils._.extend({}, options, {
                  fields:    [self.identifier],
                  allowNull: [self.identifier]
                })
              )
              .success(function() {
                if (associatedObject) {
                  associatedObject[self.identifier] = instance[instanceKey]
                  associatedObject
                    .save(options)
                    .success(function() { emitter.emit('success', associatedObject) })
                    .error(function(err) { emitter.emit('error', err) })
                } else {
                  emitter.emit('success', null)
                }
              })
          } else {
            if (associatedObject) {
              associatedObject[self.identifier] = instance[instanceKey]
              associatedObject
                .save(options)
                .success(function() { emitter.emit('success', associatedObject) })
                .error(function(err) { emitter.emit('error', err) })
            } else {
              emitter.emit('success', null)
            }
          }
        })
      }).run()
    }

    return this
  }

  return HasOne
})()


var watchr = require('watchr')
  , spawn  = require('child_process').spawn

watchr.watch({
  paths: [__dirname + '/lib', __dirname + '/spec', __dirname + '/index.js'],
  listener: function(eventName) {
    if (['new', 'change'].indexOf(eventName) > -1) {
      // generate the documentation
      spawn('npm', ['run', 'docs'])

      // run the tests
      var buster = spawn('./node_modules/.bin/buster-test', ['--reporter', 'specification'], { env: process.ENV })
      buster.stderr.on('data', function(data) { console.log(data.toString()) })
      buster.stdout.on('data', function(data) { console.log(data.toString()) })
    }
  }
})


module.exports = {
  up: function(migration, DataTypes, done) {
    migration.changeColumn('User', 'signature', {
      type: DataTypes.STRING,
      allowNull: false,
      defaultValue: 'Signature'
    }).complete(done)
  },

  down: function(migration, DataTypes, done) { done() }
}


YUI().use(
    'yuidoc-meta',
    'api-list', 'history-hash', 'node-screen', 'node-style', 'pjax',
function (Y) {

var win          = Y.config.win,
    localStorage = win.localStorage,

    bdNode = Y.one('#bd'),

    pjax,
    defaultRoute,

    classTabView,
    selectedTab;

// Kill pjax functionality unless serving over HTTP.
if (!Y.getLocation().protocol.match(/^https?\:/)) {
    Y.Router.html5 = false;
}

// Create the default route with middleware which enables syntax highlighting
// on the loaded content.
defaultRoute = Y.Pjax.defaultRoute.concat(function (req, res, next) {
    prettyPrint();
    bdNode.removeClass('loading');

    next();
});

pjax = new Y.Pjax({
    container      : '#docs-main',
    contentSelector: '#docs-main > .content',
    linkSelector   : '#bd a',
    titleSelector  : '#xhr-title',

    navigateOnHash: true,
    root          : '/',
    routes        : [
        // -- / ----------------------------------------------------------------
        {
            path     : '/(index.html)?',
            callbacks: defaultRoute
        },

        // -- /classes/* -------------------------------------------------------
        {
            path     : '/classes/:class.html*',
            callbacks: [defaultRoute, 'handleClasses']
        },

        // -- /files/* ---------------------------------------------------------
        {
            path     : '/files/*file',
            callbacks: [defaultRoute, 'handleFiles']
        },

        // -- /modules/* -------------------------------------------------------
        {
            path     : '/modules/:module.html*',
            callbacks: defaultRoute
        }
    ]
});

// -- Utility Functions --------------------------------------------------------

pjax.checkVisibility = function (tab) {
    tab || (tab = selectedTab);

    if (!tab) { return; }

    var panelNode = tab.get('panelNode'),
        visibleItems;

    // If no items are visible in the tab panel due to the current visibility
    // settings, display a message to that effect.
    visibleItems = panelNode.all('.item,.index-item').some(function (itemNode) {
        if (itemNode.getComputedStyle('display') !== 'none') {
            return true;
        }
    });

    panelNode.all('.no-visible-items').remove();

    if (!visibleItems) {
        if (Y.one('#index .index-item')) {
            panelNode.append(
                '
' +
                    '
' +
                    'Some items are not shown due to the current visibility ' +
                    'settings. Use the checkboxes at the upper right of this ' +
                    'page to change the visibility settings.' +
                    '
' +
                '
'
            );
        } else {
            panelNode.append(
                '
' +
                    '
' +
                    'This class doesn\'t provide any methods, properties, ' +
                    'attributes, or events.' +
                    '
' +
                '
'
            );
        }
    }

    // Hide index sections without any visible items.
    Y.all('.index-section').each(function (section) {
        var items        = 0,
            visibleItems = 0;

        section.all('.index-item').each(function (itemNode) {
            items += 1;

            if (itemNode.getComputedStyle('display') !== 'none') {
                visibleItems += 1;
            }
        });

        section.toggleClass('hidden', !visibleItems);
        section.toggleClass('no-columns', visibleItems < 4);
    });
};

pjax.initClassTabView = function () {
    if (!Y.all('#classdocs .api-class-tab').size()) {
        return;
    }

    if (classTabView) {
        classTabView.destroy();
        selectedTab = null;
    }

    classTabView = new Y.TabView({
        srcNode: '#classdocs',

        on: {
            selectionChange: pjax.onTabSelectionChange
        }
    });

    pjax.updateTabState();
    classTabView.render();
};

pjax.initLineNumbers = function () {
    var hash      = win.location.hash.substring(1),
        container = pjax.get('container'),
        hasLines, node;

    // Add ids for each line number in the file source view.
    container.all('.linenums>li').each(function (lineNode, index) {
        lineNode.set('id', 'l' + (index + 1));
        lineNode.addClass('file-line');
        hasLines = true;
    });

    // Scroll to the desired line.
    if (hasLines && /^l\d+$/.test(hash)) {
        if ((node = container.getById(hash))) {
            win.scroll(0, node.getY());
        }
    }
};

pjax.initRoot = function () {
    var terminators = /^(?:classes|files|modules)$/,
        parts       = pjax._getPathRoot().split('/'),
        root        = [],
        i, len, part;

    for (i = 0, len = parts.length; i < len; i += 1) {
        part = parts[i];

        if (part.match(terminators)) {
            // Makes sure the path will end with a "/".
            root.push('');
            break;
        }

        root.push(part);
    }

    pjax.set('root', root.join('/'));
};

pjax.updateTabState = function (src) {
    var hash = win.location.hash.substring(1),
        defaultTab, node, tab, tabPanel;

    function scrollToNode() {
        if (node.hasClass('protected')) {
            Y.one('#api-show-protected').set('checked', true);
            pjax.updateVisibility();
        }

        if (node.hasClass('private')) {
            Y.one('#api-show-private').set('checked', true);
            pjax.updateVisibility();
        }

        setTimeout(function () {
            // For some reason, unless we re-get the node instance here,
            // getY() always returns 0.
            var node = Y.one('#classdocs').getById(hash);
            win.scrollTo(0, node.getY() - 70);
        }, 1);
    }

    if (!classTabView) {
        return;
    }

    if (src === 'hashchange' && !hash) {
        defaultTab = 'index';
    } else {
        if (localStorage) {
            defaultTab = localStorage.getItem('tab_' + pjax.getPath()) ||
                'index';
        } else {
            defaultTab = 'index';
        }
    }

    if (hash && (node = Y.one('#classdocs').getById(hash))) {
        if ((tabPanel = node.ancestor('.api-class-tabpanel', true))) {
            if ((tab = Y.one('#classdocs .api-class-tab.' + tabPanel.get('id')))) {
                if (classTabView.get('rendered')) {
                    Y.Widget.getByNode(tab).set('selected', 1);
                } else {
                    tab.addClass('yui3-tab-selected');
                }
            }
        }

        // Scroll to the desired element if this is a hash URL.
        if (node) {
            if (classTabView.get('rendered')) {
                scrollToNode();
            } else {
                classTabView.once('renderedChange', scrollToNode);
            }
        }
    } else {
        tab = Y.one('#classdocs .api-class-tab.' + defaultTab);

        // When the `defaultTab` node isn't found, `localStorage` is stale.
        if (!tab && defaultTab !== 'index') {
            tab = Y.one('#classdocs .api-class-tab.index');
        }

        if (classTabView.get('rendered')) {
            Y.Widget.getByNode(tab).set('selected', 1);
        } else {
            tab.addClass('yui3-tab-selected');
        }
    }
};

pjax.updateVisibility = function () {
    var container = pjax.get('container');

    container.toggleClass('hide-inherited',
            !Y.one('#api-show-inherited').get('checked'));

    container.toggleClass('show-deprecated',
            Y.one('#api-show-deprecated').get('checked'));

    container.toggleClass('show-protected',
            Y.one('#api-show-protected').get('checked'));

    container.toggleClass('show-private',
            Y.one('#api-show-private').get('checked'));

    pjax.checkVisibility();
};

// -- Route Handlers -----------------------------------------------------------

pjax.handleClasses = function (req, res, next) {
    var status = res.ioResponse.status;

    // Handles success and local filesystem XHRs.
    if (!status || (status >= 200 && status < 300)) {
        pjax.initClassTabView();
    }

    next();
};

pjax.handleFiles = function (req, res, next) {
    var status = res.ioResponse.status;

    // Handles success and local filesystem XHRs.
    if (!status || (status >= 200 && status < 300)) {
        pjax.initLineNumbers();
    }

    next();
};

// -- Event Handlers -----------------------------------------------------------

pjax.onNavigate = function (e) {
    var hash         = e.hash,
        originTarget = e.originEvent && e.originEvent.target,
        tab;

    if (hash) {
        tab = originTarget && originTarget.ancestor('.yui3-tab', true);

        if (hash === win.location.hash) {
            pjax.updateTabState('hashchange');
        } else if (!tab) {
            win.location.hash = hash;
        }

        e.preventDefault();
        return;
    }

    // Only scroll to the top of the page when the URL doesn't have a hash.
    this.set('scrollToTop', !e.url.match(/#.+$/));

    bdNode.addClass('loading');
};

pjax.onOptionClick = function (e) {
    pjax.updateVisibility();
};

pjax.onTabSelectionChange = function (e) {
    var tab   = e.newVal,
        tabId = tab.get('contentBox').getAttribute('href').substring(1);

    selectedTab = tab;

    // If switching from a previous tab (i.e., this is not the default tab),
    // replace the history entry with a hash URL that will cause this tab to
    // be selected if the user navigates away and then returns using the back
    // or forward buttons.
    if (e.prevVal && localStorage) {
        localStorage.setItem('tab_' + pjax.getPath(), tabId);
    }

    pjax.checkVisibility(tab);
};

// -- Init ---------------------------------------------------------------------

pjax.on('navigate', pjax.onNavigate);

pjax.initRoot();
pjax.upgrade();
pjax.initClassTabView();
pjax.initLineNumbers();
pjax.updateVisibility();

Y.APIList.rootPath = pjax.get('root');

Y.one('#api-options').delegate('click', pjax.onOptionClick, 'input');

Y.on('hashchange', function (e) {
    pjax.updateTabState('hashchange');
}, win);

});


/*
 * L.GridLayer is used as base class for grid-like layers like TileLayer.
 */

L.GridLayer = L.Layer.extend({

	options: {
		pane: 'tilePane',

		tileSize: 256,
		opacity: 1,

		unloadInvisibleTiles: L.Browser.mobile,
		updateWhenIdle: L.Browser.mobile,
		updateInterval: 150

		/*
		minZoom: <Number>,
		maxZoom: <Number>,
		attribution: <String>,
		zIndex: <Number>,
		bounds: <LatLngBounds>
		*/
	},

	initialize: function (options) {
		options = L.setOptions(this, options);
	},

	onAdd: function () {
		this._initContainer();

		if (!this.options.updateWhenIdle) {
			// update tiles on move, but not more often than once per given interval
			this._update = L.Util.throttle(this._update, this.options.updateInterval, this);
		}

		this._reset();
		this._update();
	},

	beforeAdd: function (map) {
		map._addZoomLimit(this);
	},

	onRemove: function (map) {
		this._clearBgBuffer();
		L.DomUtil.remove(this._container);

		map._removeZoomLimit(this);

		this._container = null;
	},

	bringToFront: function () {
		if (this._map) {
			L.DomUtil.toFront(this._container);
			this._setAutoZIndex(Math.max);
		}
		return this;
	},

	bringToBack: function () {
		if (this._map) {
			L.DomUtil.toBack(this._container);
			this._setAutoZIndex(Math.min);
		}
		return this;
	},

	getAttribution: function () {
		return this.options.attribution;
	},

	getContainer: function () {
		return this._container;
	},

	setOpacity: function (opacity) {
		this.options.opacity = opacity;

		if (this._map) {
			this._updateOpacity();
		}
		return this;
	},

	setZIndex: function (zIndex) {
		this.options.zIndex = zIndex;
		this._updateZIndex();

		return this;
	},

	redraw: function () {
		if (this._map) {
			this._reset({hard: true});
			this._update();
		}
		return this;
	},

	getEvents: function () {
		var events = {
			viewreset: this._reset,
			moveend: this._update
		};

		if (!this.options.updateWhenIdle) {
			events.move = this._update;
		}

		if (this._zoomAnimated) {
			events.zoomstart = this._startZoomAnim;
			events.zoomanim = this._animateZoom;
			events.zoomend = this._endZoomAnim;
		}

		return events;
	},

	_updateZIndex: function () {
		if (this._container && this.options.zIndex !== undefined) {
			this._container.style.zIndex = this.options.zIndex;
		}
	},

	_setAutoZIndex: function (compare) {
		// go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)

		var layers = this.getPane().children,
		    edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min

		for (var i = 0, len = layers.length, zIndex; i < len; i++) {

			zIndex = layers[i].style.zIndex;

			if (layers[i] !== this._container && zIndex) {
				edgeZIndex = compare(edgeZIndex, +zIndex);
			}
		}

		if (isFinite(edgeZIndex)) {
			this.options.zIndex = edgeZIndex + compare(-1, 1);
			this._updateZIndex();
		}
	},

	_updateOpacity: function () {
		var opacity = this.options.opacity;

		if (L.Browser.ielt9) {
			// IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
			for (var i in this._tiles) {
				L.DomUtil.setOpacity(this._tiles[i], opacity);
			}
		} else {
			L.DomUtil.setOpacity(this._container, opacity);
		}
	},

	_initContainer: function () {
		if (this._container) { return; }

		this._container = L.DomUtil.create('div', 'leaflet-layer');
		this._updateZIndex();

		if (this._zoomAnimated) {
			var className = 'leaflet-tile-container leaflet-zoom-animated';

			this._bgBuffer = L.DomUtil.create('div', className, this._container);
			this._tileContainer = L.DomUtil.create('div', className, this._container);

			L.DomUtil.setTransform(this._tileContainer);

		} else {
			this._tileContainer = this._container;
		}

		if (this.options.opacity < 1) {
			this._updateOpacity();
		}

		this.getPane().appendChild(this._container);
	},

	_reset: function (e) {
		for (var key in this._tiles) {
			this.fire('tileunload', {
				tile: this._tiles[key]
			});
		}

		if (this._abortLoading) {
			this._abortLoading();
		}

		this._tiles = {};
		this._tilesToLoad = 0;
		this._tilesTotal = 0;

		L.DomUtil.empty(this._tileContainer);

		if (this._zoomAnimated && e && e.hard) {
			this._clearBgBuffer();
		}

		var map = this._map,
		    zoom = map.getZoom(),
		    center = map.getCenter();

		this._tileSize = this._getTileSize();
		this._tileZoom = Math.round(zoom);
		this._tileRange = this._getTileRange();
		this._resetWrap();

		if (this._zoomAnimated) {
			this._pxOrigin = map._getNewTopLeftPoint(center, this._tileZoom);
			this._origScale = map.getZoomScale(zoom) / map.getZoomScale(this._tileZoom);
			this._origTranslate = map.getSize().multiplyBy((1 - this._origScale) / 2, true)
				.subtract(map._getMapPanePos()).round();

			L.DomUtil.setTransform(this._tileContainer, this._origTranslate, this._origScale);

		} else {
			this._pxOrigin = map.getPixelOrigin();
		}
	},

	_resetWrap: function () {
		var map = this._map,
		    crs = map.options.crs;

		if (crs.infinite) { return; }

		var tileSize = this._tileSize,
		    zoom = this._tileZoom;

		if (crs.wrapLng) {
			this._wrapLng = [
				Math.floor(map.project([0, crs.wrapLng[0]], zoom).x / tileSize),
				Math.ceil(map.project([0, crs.wrapLng[1]], zoom).x / tileSize)
			];
		}

		if (crs.wrapLat) {
			this._wrapLat = [
				Math.floor(map.project([crs.wrapLat[0], 0], zoom).y / tileSize),
				Math.ceil(map.project([crs.wrapLat[1], 0], zoom).y / tileSize)
			];
		}
	},

	_getTileSize: function () {
		return this.options.tileSize;
	},

	_update: function () {
		if (!this._map) { return; }

		// TODO move to reset
		var zoom = this._map.getZoom();

		if (zoom > this.options.maxZoom ||
		    zoom < this.options.minZoom) {
			this._clearBgBuffer();
			return;
		}

		var bounds = this._map.getBounds(),
		    pxBounds = new L.Bounds(
		        this._map.project(bounds.getNorthWest(), this._tileZoom),
		        this._map.project(bounds.getSouthEast(), this._tileZoom));

		// tile coordinates range for the current view
		var tileRange = this._boundsToTileRange(pxBounds);

		if (this.options.unloadInvisibleTiles) {
			this._removeOtherTiles(tileRange);
		}

		this._addTiles(tileRange);
	},

	_addTiles: function (tileRange) {
		var queue = [],
		    center = tileRange.getCenter(),
		    zoom = this._map.getZoom();

		var j, i, coords;

		// create a queue of coordinates to load tiles from
		for (j = tileRange.min.y; j <= tileRange.max.y; j++) {
			for (i = tileRange.min.x; i <= tileRange.max.x; i++) {

				coords = new L.Point(i, j);
				coords.z = zoom;

				// add tile to queue if it's not in cache or out of bounds
				if (!(this._tileCoordsToKey(coords) in this._tiles) && this._isValidTile(coords)) {
					queue.push(coords);
				}
			}
		}

		var tilesToLoad = queue.length;

		if (tilesToLoad === 0) { return; }

		// if its the first batch of tiles to load
		if (!this._tilesToLoad) {
			this.fire('loading');
		}

		this._tilesToLoad += tilesToLoad;
		this._tilesTotal += tilesToLoad;

		// sort tile queue to load tiles in order of their distance to center
		queue.sort(function (a, b) {
			return a.distanceTo(center) - b.distanceTo(center);
		});

		// create DOM fragment to append tiles in one batch
		var fragment = document.createDocumentFragment();

		for (i = 0; i < tilesToLoad; i++) {
			this._addTile(queue[i], fragment);
		}

		this._tileContainer.appendChild(fragment);
	},

	_isValidTile: function (coords) {
		var crs = this._map.options.crs;

		if (!crs.infinite) {
			// don't load tile if it's out of bounds and not wrapped
			var bounds = this._tileRange;
			if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) ||
			    (!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; }
		}

		if (!this.options.bounds) { return true; }

		// don't load tile if it doesn't intersect the bounds in options
		var tileBounds = this._tileCoordsToBounds(coords);
		return L.latLngBounds(this.options.bounds).intersects(tileBounds);
	},

	// converts tile coordinates to its geographical bounds
	_tileCoordsToBounds: function (coords) {

		var map = this._map,
		    tileSize = this._getTileSize(),

		    nwPoint = coords.multiplyBy(tileSize),
		    sePoint = nwPoint.add([tileSize, tileSize]),

		    nw = map.wrapLatLng(map.unproject(nwPoint, coords.z)),
		    se = map.wrapLatLng(map.unproject(sePoint, coords.z));

		return new L.LatLngBounds(nw, se);
	},

	// converts tile coordinates to key for the tile cache
	_tileCoordsToKey: function (coords) {
		return coords.x + ':' + coords.y;
	},

	// converts tile cache key to coordinates
	_keyToTileCoords: function (key) {
		var kArr = key.split(':'),
		    x = parseInt(kArr[0], 10),
		    y = parseInt(kArr[1], 10);

		return new L.Point(x, y);
	},

	// remove any present tiles that are off the specified bounds
	_removeOtherTiles: function (bounds) {
		for (var key in this._tiles) {
			if (!bounds.contains(this._keyToTileCoords(key))) {
				this._removeTile(key);
			}
		}
	},

	_removeTile: function (key) {
		var tile = this._tiles[key];

		L.DomUtil.remove(tile);

		delete this._tiles[key];

		this.fire('tileunload', {tile: tile});
	},

	_initTile: function (tile) {
		L.DomUtil.addClass(tile, 'leaflet-tile');

		tile.style.width = this._tileSize + 'px';
		tile.style.height = this._tileSize + 'px';

		tile.onselectstart = L.Util.falseFn;
		tile.onmousemove = L.Util.falseFn;

		// update opacity on tiles in IE7-8 because of filter inheritance problems
		if (L.Browser.ielt9 && this.options.opacity < 1) {
			L.DomUtil.setOpacity(tile, this.options.opacity);
		}

		// without this hack, tiles disappear after zoom on Chrome for Android
		// https://github.com/Leaflet/Leaflet/issues/2078
		if (L.Browser.android && !L.Browser.android23) {
			tile.style.WebkitBackfaceVisibility = 'hidden';
		}
	},

	_addTile: function (coords, container) {
		var tilePos = this._getTilePos(coords);

		// wrap tile coords if necessary (depending on CRS)
		this._wrapCoords(coords);

		var tile = this.createTile(coords, L.bind(this._tileReady, this));

		this._initTile(tile);

		// if createTile is defined with a second argument ("done" callback),
		// we know that tile is async and will be ready later; otherwise
		if (this.createTile.length < 2) {
			// mark tile as ready, but delay one frame for opacity animation to happen
			setTimeout(L.bind(this._tileReady, this, null, tile), 0);
		}

		// we prefer top/left over translate3d so that we don't create a HW-accelerated layer from each tile
		// which is slow, and it also fixes gaps between tiles in Safari
		L.DomUtil.setPosition(tile, tilePos, true);

		// save tile in cache
		this._tiles[this._tileCoordsToKey(coords)] = tile;

		container.appendChild(tile);
		this.fire('tileloadstart', {tile: tile});
	},

	_tileReady: function (err, tile) {
		if (err) {
			this.fire('tileerror', {
				error: err,
				tile: tile
			});
		}

		L.DomUtil.addClass(tile, 'leaflet-tile-loaded');

		this.fire('tileload', {tile: tile});

		this._tilesToLoad--;

		if (this._tilesToLoad === 0) {
			this._visibleTilesReady();
		}
	},

	_visibleTilesReady: function () {
		this.fire('load');

		if (this._zoomAnimated) {
			// clear scaled tiles after all new tiles are loaded (for performance)
			clearTimeout(this._clearBgBufferTimer);
			this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 300);
		}
	},

	_getTilePos: function (coords) {
		return coords.multiplyBy(this._tileSize).subtract(this._pxOrigin);
	},

	_wrapCoords: function (coords) {
		coords.x = this._wrapLng ? L.Util.wrapNum(coords.x, this._wrapLng) : coords.x;
		coords.y = this._wrapLat ? L.Util.wrapNum(coords.y, this._wrapLat) : coords.y;
	},

	// get the global tile coordinates range for the current zoom
	_getTileRange: function () {
		var bounds = this._map.getPixelWorldBounds(this._tileZoom);
		return bounds ? this._boundsToTileRange(bounds) : null;
	},

	_boundsToTileRange: function (bounds) {
		return new L.Bounds(
			bounds.min.divideBy(this._tileSize).floor(),
			bounds.max.divideBy(this._tileSize).ceil().subtract([1, 1]));
	},

	_startZoomAnim: function () {
		this._prepareBgBuffer();
		this._prevTranslate = this._translate || new L.Point(0, 0);
		this._prevScale = this._scale;
	},

	_animateZoom: function (e) {
		// avoid stacking transforms by calculating cumulating translate/scale sequence
		this._translate = this._prevTranslate.multiplyBy(e.scale).add(e.origin.multiplyBy(1 - e.scale)).round();
		this._scale = this._prevScale * e.scale;

		L.DomUtil.setTransform(this._bgBuffer, this._translate, this._scale);
	},

	_endZoomAnim: function () {
		var front = this._tileContainer;
		front.style.visibility = '';
		L.DomUtil.toFront(front); // bring to front
	},

	_clearBgBuffer: function () {
		var map = this._map,
			bg = this._bgBuffer;

		if (map && !map._animatingZoom && !map.touchZoom._zooming && bg) {
			L.DomUtil.empty(bg);
			L.DomUtil.setTransform(bg);
		}
	},

	_prepareBgBuffer: function () {

		var front = this._tileContainer,
		    bg = this._bgBuffer;

		if (this._abortLoading) {
			this._abortLoading();
		}

		if (this._tilesToLoad / this._tilesTotal > 0.5) {
			// if foreground layer doesn't have many tiles loaded,
			// keep the existing bg layer and just zoom it some more
			front.style.visibility = 'hidden';
			return;
		}

		// prepare the buffer to become the front tile pane
		bg.style.visibility = 'hidden';
		L.DomUtil.setTransform(bg);

		// switch out the current layer to be the new bg layer (and vice-versa)
		this._tileContainer = bg;
		this._bgBuffer = front;

		// reset bg layer transform info
		this._translate = this._origTranslate;
		this._scale = this._origScale;

		// prevent bg buffer from clearing right after zoom
		clearTimeout(this._clearBgBufferTimer);
	}
});

L.gridLayer = function (options) {
	return new L.GridLayer(options);
};


/*
 * L.Point represents a point with x and y coordinates.
 */

L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) {
	this.x = (round ? Math.round(x) : x);
	this.y = (round ? Math.round(y) : y);
};

L.Point.prototype = {

	clone: function () {
		return new L.Point(this.x, this.y);
	},

	// non-destructive, returns a new point
	add: function (point) {
		return this.clone()._add(L.point(point));
	},

	// destructive, used directly for performance in situations where it's safe to modify existing point
	_add: function (point) {
		this.x += point.x;
		this.y += point.y;
		return this;
	},

	subtract: function (point) {
		return this.clone()._subtract(L.point(point));
	},

	_subtract: function (point) {
		this.x -= point.x;
		this.y -= point.y;
		return this;
	},

	divideBy: function (num) {
		return this.clone()._divideBy(num);
	},

	_divideBy: function (num) {
		this.x /= num;
		this.y /= num;
		return this;
	},

	multiplyBy: function (num) {
		return this.clone()._multiplyBy(num);
	},

	_multiplyBy: function (num) {
		this.x *= num;
		this.y *= num;
		return this;
	},

	round: function () {
		return this.clone()._round();
	},

	_round: function () {
		this.x = Math.round(this.x);
		this.y = Math.round(this.y);
		return this;
	},

	floor: function () {
		return this.clone()._floor();
	},

	_floor: function () {
		this.x = Math.floor(this.x);
		this.y = Math.floor(this.y);
		return this;
	},

	ceil: function () {
		return this.clone()._ceil();
	},

	_ceil: function () {
		this.x = Math.ceil(this.x);
		this.y = Math.ceil(this.y);
		return this;
	},

	distanceTo: function (point) {
		point = L.point(point);

		var x = point.x - this.x,
		    y = point.y - this.y;

		return Math.sqrt(x * x + y * y);
	},

	equals: function (point) {
		point = L.point(point);

		return point.x === this.x &&
		       point.y === this.y;
	},

	contains: function (point) {
		point = L.point(point);

		return Math.abs(point.x) <= Math.abs(this.x) &&
		       Math.abs(point.y) <= Math.abs(this.y);
	},

	toString: function () {
		return 'Point(' +
		        L.Util.formatNum(this.x) + ', ' +
		        L.Util.formatNum(this.y) + ')';
	}
};

L.point = function (x, y, round) {
	if (x instanceof L.Point) {
		return x;
	}
	if (L.Util.isArray(x)) {
		return new L.Point(x[0], x[1]);
	}
	if (x === undefined || x === null) {
		return x;
	}
	return new L.Point(x, y, round);
};


// Karma configuration
module.exports = function (config) {

	var libSources = require(__dirname+'/../build/build.js').getFiles();

	var files = [
		"spec/sinon.js",
		"spec/expect.js"
	].concat(libSources, [
		"spec/after.js",
		"node_modules/happen/happen.js",
		"spec/suites/SpecHelper.js",
		"spec/suites/**/*.js",
		{pattern: "dist/images/*.png", included: false}
	]);

	config.set({
		// base path, that will be used to resolve files and exclude
		basePath: '../',

		plugins: [
			'karma-mocha',
			'karma-coverage',
			'karma-phantomjs-launcher',
			'karma-chrome-launcher'],

		// frameworks to use
		frameworks: ['mocha'],

		// list of files / patterns to load in the browser
		files: files,
		exclude: [],

		// test results reporter to use
		// possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
		reporters: ['dots'],

		// web server port
		port: 9876,

		// level of logging
		// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
		logLevel: config.LOG_WARN,

		// enable / disable colors in the output (reporters and logs)
		colors: true,

		// enable / disable watching file and executing tests whenever any file changes
		autoWatch: false,

		// Start these browsers, currently available:
		// - Chrome
		// - ChromeCanary
		// - Firefox
		// - Opera
		// - Safari (only Mac)
		// - PhantomJS
		// - IE (only Windows)
		browsers: ['PhantomJS'],

		// If browser does not capture in given timeout [ms], kill it
		captureTimeout: 5000,

		// Continuous Integration mode
		// if true, it capture browsers, run tests and exit
		singleRun: true
	});
};


/*
 * L.Util contains various utility functions used throughout Leaflet code.
 */

L.Util = {
	// extend an object with properties of one or more other objects
	extend: function (dest) {
		var i, j, len, src;

		for (j = 1, len = arguments.length; j < len; j++) {
			src = arguments[j];
			for (i in src) {
				dest[i] = src[i];
			}
		}
		return dest;
	},

	// create an object from a given prototype
	create: Object.create || (function () {
		function F() {}
		return function (proto) {
			F.prototype = proto;
			return new F();
		};
	})(),

	// bind a function to be called with a given context
	bind: function (fn, obj) {
		var slice = Array.prototype.slice;

		if (fn.bind) {
			return fn.bind.apply(fn, slice.call(arguments, 1));
		}

		var args = slice.call(arguments, 2);

		return function () {
			return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments);
		};
	},

	// return unique ID of an object
	stamp: function (obj) {
		// jshint camelcase: false
		obj._leaflet_id = obj._leaflet_id || ++L.Util.lastId;
		return obj._leaflet_id;
	},

	lastId: 0,

	// return a function that won't be called more often than the given interval
	throttle: function (fn, time, context) {
		var lock, args, wrapperFn, later;

		later = function () {
			// reset lock and call if queued
			lock = false;
			if (args) {
				wrapperFn.apply(context, args);
				args = false;
			}
		};

		wrapperFn = function () {
			if (lock) {
				// called too soon, queue to call later
				args = arguments;

			} else {
				// call and lock until later
				fn.apply(context, arguments);
				setTimeout(later, time);
				lock = true;
			}
		};

		return wrapperFn;
	},

	// wrap the given number to lie within a certain range (used for wrapping longitude)
	wrapNum: function (x, range, includeMax) {
		var max = range[1],
		    min = range[0],
		    d = max - min;
		return x === max && includeMax ? x : ((x - min) % d + d) % d + min;
	},

	// do nothing (used as a noop throughout the code)
	falseFn: function () { return false; },

	// round a given number to a given precision
	formatNum: function (num, digits) {
		var pow = Math.pow(10, digits || 5);
		return Math.round(num * pow) / pow;
	},

	// trim whitespace from both sides of a string
	trim: function (str) {
		return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
	},

	// split a string into words
	splitWords: function (str) {
		return L.Util.trim(str).split(/\s+/);
	},

	// set options to an object, inheriting parent's options as well
	setOptions: function (obj, options) {
		if (!obj.hasOwnProperty('options')) {
			obj.options = obj.options ? L.Util.create(obj.options) : {};
		}
		for (var i in options) {
			obj.options[i] = options[i];
		}
		return obj.options;
	},

	// make an URL with GET parameters out of a set of properties/values
	getParamString: function (obj, existingUrl, uppercase) {
		var params = [];
		for (var i in obj) {
			params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i]));
		}
		return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&');
	},

	// super-simple templating facility, used for TileLayer URLs
	template: function (str, data) {
		return str.replace(L.Util.templateRe, function (str, key) {
			var value = data[key];

			if (value === undefined) {
				throw new Error('No value provided for variable ' + str);

			} else if (typeof value === 'function') {
				value = value(data);
			}
			return value;
		});
	},

	templateRe: /\{ *([\w_]+) *\}/g,

	isArray: Array.isArray || function (obj) {
		return (Object.prototype.toString.call(obj) === '[object Array]');
	},

	// minimal image URI, set to an image when disposing to flush memory
	emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
};

(function () {
	// inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/

	function getPrefixed(name) {
		return window['webkit' + name] || window['moz' + name] || window['ms' + name];
	}

	var lastTime = 0;

	// fallback for IE 7-8
	function timeoutDefer(fn) {
		var time = +new Date(),
		    timeToCall = Math.max(0, 16 - (time - lastTime));

		lastTime = time + timeToCall;
		return window.setTimeout(fn, timeToCall);
	}

	var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer,
	    cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') ||
	               getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); };

	L.Util.requestAnimFrame = function (fn, context, immediate) {
		if (immediate && requestFn === timeoutDefer) {
			fn.call(context);
		} else {
			return requestFn.call(window, L.bind(fn, context));
		}
	};

	L.Util.cancelAnimFrame = function (id) {
		if (id) {
			cancelFn.call(window, id);
		}
	};
})();

// shortcuts for most used utility functions
L.extend = L.Util.extend;
L.bind = L.Util.bind;
L.stamp = L.Util.stamp;
L.setOptions = L.Util.setOptions;


var origin = e.origin.subtract(this._map._getCenterLayerPoint()),

offset = this._bounds.min.add(origin.multiplyBy(1 - e.scale));

offset = this._bounds.min.add(origin.multiplyBy(1 - e.scale)).add(e.offset).round();

/*
 * Extends L.Map to handle zoom animations.
 */

L.Map.mergeOptions({
	zoomAnimation: true,
	zoomAnimationThreshold: 4
});

var zoomAnimated = L.DomUtil.TRANSITION && L.Browser.any3d && !L.Browser.mobileOpera;

if (zoomAnimated) {

	L.Map.addInitHook(function () {
		// don't animate on browsers without hardware-accelerated transitions or old Android/Opera
		this._zoomAnimated = this.options.zoomAnimation;

		// zoom transitions run with the same duration for all layers, so if one of transitionend events
		// happens after starting zoom animation (propagating to the map pane), we know that it ended globally
		if (this._zoomAnimated) {
			L.DomEvent.on(this._mapPane, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
		}
	});
}

L.Map.include(!zoomAnimated ? {} : {

	_catchTransitionEnd: function (e) {
		if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) {
			this._onZoomTransitionEnd();
		}
	},

	_nothingToAnimate: function () {
		return !this._container.getElementsByClassName('leaflet-zoom-animated').length;
	},

	_tryAnimatedZoom: function (center, zoom, options) {

		if (this._animatingZoom) { return true; }

		options = options || {};

		// don't animate if disabled, not supported or zoom difference is too large
		if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() ||
		        Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; }

		// offset is the pixel coords of the zoom origin relative to the current center
		var scale = this.getZoomScale(zoom),
		    offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale);

		// don't animate if the zoom origin isn't within one screen from the current center, unless forced
		if (options.animate !== true && !this.getSize().contains(offset)) { return false; }

		L.Util.requestAnimFrame(function () {
			this
			    .fire('movestart')
			    .fire('zoomstart')
			    ._animateZoom(center, zoom, true);
		}, this);

		return true;
	},

	_animateZoom: function (center, zoom, startAnim) {
		if (startAnim) {
			this._animatingZoom = true;

			// remember what center/zoom to set after animation
			this._animateToCenter = center;
			this._animateToZoom = zoom;

			// disable any dragging during animation
			if (L.Draggable) {
				L.Draggable._disabled = true;
			}

			L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
		}

		var scale = this.getZoomScale(zoom),
			origin = this._getCenterLayerPoint().add(this._getCenterOffset(center)._divideBy(1 - 1 / scale));

		this.fire('zoomanim', {
			center: center,
			zoom: zoom,
			origin: origin,
			scale: scale
		});
	},

	_onZoomTransitionEnd: function () {

		this._animatingZoom = false;

		L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');

		this._resetView(this._animateToCenter, this._animateToZoom, true, true);

		if (L.Draggable) {
			L.Draggable._disabled = false;
		}
	}
});