'use strict';

const assert = require('assert');
const start = require('./common');
const BSON = require('mongodb/lib/bson');

const mongoose = start.mongoose;
const Schema = mongoose.Schema;


describe('Double', function() {
  beforeEach(() => mongoose.deleteModel(/Test/));

  it('is a valid schema type', function() {
    const schema = new Schema({
      myDouble: Schema.Types.Double
    });
    const Test = mongoose.model('Test', schema);

    const doc = new Test({
      myDouble: 13
    });
    assert.deepStrictEqual(doc.myDouble, new BSON.Double(13));
    assert.equal(typeof doc.myDouble, 'object');
  });

  describe('supports the required property', function() {
    it('when value is null', async function() {
      const schema = new Schema({
        Double: {
          type: Schema.Types.Double,
          required: true
        }
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        double: null
      });

      const err = await doc.validate().then(() => null, err => err);
      assert.ok(err);
      assert.ok(err.errors['Double']);
      assert.equal(err.errors['Double'].name, 'ValidatorError');
      assert.equal(
        err.errors['Double'].message,
        'Path `Double` is required.'
      );
    });
    it('when value is non-null', async function() {
      const schema = new Schema({
        Double: {
          type: Schema.Types.Double,
          required: true
        }
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        double: 3
      });

      const err = await doc.validate().then(() => null, err => err);
      assert.ok(err);
      assert.ok(err.errors['Double']);
      assert.equal(err.errors['Double'].name, 'ValidatorError');
      assert.equal(
        err.errors['Double'].message,
        'Path `Double` is required.'
      );
    });
  });

  describe('special inputs', function() {
    it('supports undefined as input', function() {
      const schema = new Schema({
        myDouble: {
          type: Schema.Types.Double
        }
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: undefined
      });
      assert.deepStrictEqual(doc.myDouble, undefined);
    });

    it('supports null as input', function() {
      const schema = new Schema({
        myDouble: {
          type: Schema.Types.Double
        }
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: null
      });
      assert.deepStrictEqual(doc.myDouble, null);
    });
  });

  describe('valid casts', function() {
    it('casts from decimal string', function() {
      const schema = new Schema({
        myDouble: {
          type: Schema.Types.Double
        }
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: '-42.008'
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(-42.008));
    });

    it('casts from exponential string', function() {
      const schema = new Schema({
        myDouble: {
          type: Schema.Types.Double
        }
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: '1.22008e45'
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(1.22008e45));
    });

    it('casts from infinite string', function() {
      const schema = new Schema({
        myDouble1: {
          type: Schema.Types.Double
        },
        myDouble2: {
          type: Schema.Types.Double
        }
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble1: 'Infinity',
        myDouble2: '-Infinity'
      });
      assert.deepStrictEqual(doc.myDouble1, new BSON.Double(Infinity));
      assert.deepStrictEqual(doc.myDouble2, new BSON.Double(-Infinity));
    });

    it('casts from NaN string', function() {
      const schema = new Schema({
        myDouble: {
          type: Schema.Types.Double
        }
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: 'NaN'
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double('NaN'));
    });

    it('casts from number', function() {
      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: 988
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(988));
    });

    it('casts from bigint', function() {
      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: -997n
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(-997));
    });

    it('casts from BSON.Long', function() {
      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: BSON.Long.fromNumber(-997987)
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(-997987));
    });

    it('casts from BSON.Double', function() {
      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: new BSON.Double(-997983.33)
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(-997983.33));
    });

    it('casts boolean true to 1', function() {
      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: true
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(1));
    });

    it('casts boolean false to 0', function() {
      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: false
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(0));
    });

    it('casts empty string to null', function() {
      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: ''
      });
      assert.deepStrictEqual(doc.myDouble, null);
    });

    it('supports valueOf() function ', function() {
      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      const Test = mongoose.model('Test', schema);

      const doc = new Test({
        myDouble: { a: 'random', b: { c: 'whatever' }, valueOf: () => 83.008 }
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(83.008));
    });
  });

  describe('cast errors', () => {
    let Test;

    beforeEach(function() {
      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      Test = mongoose.model('Test', schema);
    });

    describe('when a non-numeric string is provided to an Double field', () => {
      it('throws a CastError upon validation', async() => {
        const doc = new Test({
          myDouble: 'helloworld'
        });

        assert.deepStrictEqual(doc.myDouble, undefined);
        const err = await doc.validate().catch(e => e);
        assert.ok(err);
        assert.ok(err.errors['myDouble']);
        assert.equal(err.errors['myDouble'].name, 'CastError');
        assert.match(
          err.errors['myDouble'].message,
          /^Cast to Double failed for value "helloworld" \(type string\) at path "myDouble"/
        );
      });
    });

    describe('when an array is provided to a Double field', () => {
      it('throws a CastError upon validation, even for a single-element or empty array', async() => {
        for (const value of [[5], [], [5, 6]]) {
          const doc = new Test({ myDouble: value });

          assert.deepStrictEqual(doc.myDouble, undefined);
          const err = await doc.validate().catch(e => e);
          assert.ok(err);
          assert.ok(err.errors['myDouble']);
          assert.equal(err.errors['myDouble'].name, 'CastError');
        }
      });
    });
  });

  describe('custom casters', () => {
    const defaultCast = mongoose.Schema.Types.Double.cast();

    afterEach(() => {
      mongoose.Schema.Types.Double.cast(defaultCast);
    });

    it('supports cast disabled', async() => {
      mongoose.Schema.Types.Double.cast(false);
      const schema = new Schema({
        myDouble1: {
          type: Schema.Types.Double
        },
        myDouble2: {
          type: Schema.Types.Double
        }
      });
      const Test = mongoose.model('Test', schema);
      const doc = new Test({
        myDouble1: '52',
        myDouble2: new BSON.Double(52)
      });
      assert.deepStrictEqual(doc.myDouble1, undefined);
      assert.deepStrictEqual(doc.myDouble2, new BSON.Double(52));

      const err = await doc.validate().catch(e => e);
      assert.ok(err);
      assert.ok(err.errors['myDouble1']);
    });

    it('supports custom cast', () => {
      mongoose.Schema.Types.Double.cast(v => {
        if (isNaN(v)) {
          return new BSON.Double(2);
        }
        return defaultCast(v);
      });
      const schema = new Schema({
        myDouble: {
          type: Schema.Types.Double
        }
      });

      const Test = mongoose.model('Test', schema);
      const doc = new Test({
        myDouble: NaN
      });
      assert.deepStrictEqual(doc.myDouble, new BSON.Double(2));
    });
  });

  describe('mongoDB integration', function() {
    let db;
    let Test;

    before(async function() {
      db = await start();

      const schema = new Schema({
        myDouble: Schema.Types.Double
      });
      db.deleteModel(/Test/);
      Test = db.model('Test', schema);
    });

    after(async function() {
      await db.close();
    });

    beforeEach(async() => {
      await Test.deleteMany({});
    });

    describe('$type compatibility', function() {
      it('is queryable as a JS number in MongoDB', async function() {
        await Test.create({ myDouble: '42.04' });
        const doc = await Test.findOne({ myDouble: { $type: 'number' } });
        assert.ok(doc);
        assert.deepStrictEqual(doc.myDouble, new BSON.Double(42.04));
      });

      it('is NOT queryable as a BSON Integer in MongoDB if the value is NOT integer', async function() {
        await Test.create({ myDouble: '42.04' });
        const doc = await Test.findOne({ myDouble: { $type: 'int' } });
        assert.deepStrictEqual(doc, null);
      });

      it('is queryable as a BSON Double in MongoDB when a non-integer is provided', async function() {
        await Test.create({ myDouble: '42.04' });
        const doc = await Test.findOne({ myDouble: { $type: 'double' } });
        assert.deepStrictEqual(doc.myDouble, new BSON.Double(42.04));
      });

      it('is queryable as a BSON Double in MongoDB when an integer is provided', async function() {
        await Test.create({ myDouble: '42' });
        const doc = await Test.findOne({ myDouble: { $type: 'double' } });
        assert.deepStrictEqual(doc.myDouble, new BSON.Double(42));
      });
    });

    it('can query with comparison operators', async function() {
      await Test.create([
        { myDouble: 1.2 },
        { myDouble: 1.709 },
        { myDouble: 1.710 },
        { myDouble: 1.8 }
      ]);

      let docs = await Test.find({ myDouble: { $gte: 1.710 } }).sort({ myDouble: 1 });
      assert.equal(docs.length, 2);
      assert.deepStrictEqual(docs.map(doc => doc.myDouble), [new BSON.Double(1.710), new BSON.Double(1.8)]);

      docs = await Test.find({ myDouble: { $lt: 1.710 } }).sort({ myDouble: -1 });
      assert.equal(docs.length, 2);
      assert.deepStrictEqual(docs.map(doc => doc.myDouble), [new BSON.Double(1.709), new BSON.Double(1.2)]);
    });

    it('supports populate()', async function() {
      const parentSchema = new Schema({
        child: {
          type: Schema.Types.Double,
          ref: 'Child'
        }
      });
      const childSchema = new Schema({
        _id: Schema.Types.Double,
        name: String
      });
      const Parent = db.model('Parent', parentSchema);
      const Child = db.model('Child', childSchema);

      const { _id } = await Parent.create({ child: 42 });
      await Child.create({ _id: 42, name: 'test-Double-populate' });

      const doc = await Parent.findById(_id).populate('child');
      assert.ok(doc);
      assert.equal(doc.child.name, 'test-Double-populate');
      assert.equal(doc.child._id, 42);
    });
  });
});