const assert = require('assert');

describe('moog', function() {
  it('should exist', function() {
    const moog = require('../lib/moog.js')({ });
    assert(moog);
  });

  it('should be initialized without arguments', function() {
    const moog = require('../lib/moog.js')();
    assert(moog);
  });

  describe('methods', function() {
    it('should have a `define` method', function() {
      const moog = require('../lib/moog.js')({});
      assert(moog.define);
    });

    it('should have a `redefine` method', function() {
      const moog = require('../lib/moog.js')({});
      assert(moog.redefine);
    });

    it('should have a `create` method', function() {
      const moog = require('../lib/moog.js')({});
      assert(moog.create);
    });

    it('should have an `isDefined` method', function() {
      const moog = require('../lib/moog.js')({});
      assert(moog.isDefined);
    });
  });

  describe('defining and creating', function() {
    it('should be able to `define` a class', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('myObject', {
        methods(self) {
          return {};
        }
      });
    });

    it('should be able to `define` and then `create` an instance', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('myObject', {
        options: {
          color: 'blue'
        },
        init(self) {
          self.color = self.options.color;
        }
      });

      const myObject = await moog.create('myObject', {});
      assert(myObject);
      assert(myObject.color === 'blue');
    });
  });

  describe('`create` syntax', function() {
    it('should `create` without options', async function() {
      const moog = require('../lib/moog.js')();

      await moog.define('myClass', {});

      const myObj = await moog.create('myClass');
      assert(myObj);
      assert(myObj.__meta.name === 'myClass');
    });

  });

  describe('explicit subclassing behavior', function() {

    it('should be able to create a subclass with expected default option behavior (async)', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('baseClass', {
        options: {
          color: 'blue'
        }
      });

      await moog.define('subClass', {
        options: {
          color: 'red'
        },
        extend: 'baseClass'
      });

      const myObject = await moog.create('subClass', {});
      assert(myObject);
      assert(myObject.options.color === 'red');
    });

    it('should report an error gracefully if subclass to be extended does not exist (async)', async function() {
      const moog = require('../lib/moog.js')({});

      // base class does not actually exist
      await moog.define('subClass', {
        options: {
          color: 'red'
        },
        extend: 'baseClass'
      });

      try {
        await moog.create('subClass', {});
        assert(false);
      } catch (e) {
        assert(e);
        assert(e.toString().match(/baseClass/));
      }
    });

    it('should be able to `extend` a subclass into yet another subclass', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('baseClass', {
        options: {
          color: 'blue'
        }
      });

      await moog.define('subClassOne', {
        options: {
          color: 'red'
        },
        extend: 'baseClass'
      });

      await moog.define('subClassTwo', {
        options: {
          color: 'green'
        },
        extend: 'subClassOne'
      });

      const myObject = await moog.create('subClassTwo', {});
      assert(myObject);
      assert(myObject.options.color === 'green');
    });

    it('default base class should take effect if configured', async function() {
      const moog = require('../lib/moog.js')({ defaultBaseClass: 'baseClass' });

      await moog.define('baseClass', {
        init(self) {
          assert(self.__meta);
          assert(self.__meta.chain);
          assert(self.__meta.chain[0]);
          assert(self.__meta.chain[0].name === 'baseClass');
          assert(self.__meta.chain[1].name === 'subClass');
          assert(self.__meta.name === 'subClass');
          self.color = self.options.color;
        }
      });

      await moog.define('subClass', {
        options: {
          color: 'red'
        }
      });

      const myObject = await moog.create('subClass', {});
      assert(myObject);
      // This verifies that init() for base class actually ran
      assert(myObject.color === 'red');
    });

    it('default base class should not take effect if extend is explicitly set to false', async function() {
      const moog = require('../lib/moog.js')({ defaultBaseClass: 'baseClass' });

      await moog.define('baseClass', {
        construct: function(self) {
          self.based = true;
        }
      });

      await moog.define('subClass', {
        options: {
          color: 'red'
        },
        extend: false
      });

      const myObject = await moog.create('subClass', {});
      assert(myObject);
      assert(!myObject.based);
    });

    it('should define methods and allow overriding and extending them through implicit subclassing', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('myObject', {
        methods(self) {
          return {
            basic() {
              return true;
            },
            overridden() {
              return false;
            },
            extended(times) {
              return 2 * times;
            }
          };
        }
      });

      await moog.define('myObject', {
        methods(self) {
          return {
            overridden() {
              return true;
            }
          };
        },
        extendMethods(self) {
          return {
            extended(_super, times) {
              return _super(times) * 2;
            }
          };
        }
      });

      const myObject = await moog.create('myObject', {});
      assert(myObject);
      assert(myObject.basic());
      assert(myObject.overridden());
      assert(myObject.extended(5) === 20);
    });

    it('should support inheriting field group fields rather than requiring all fields to be restated', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('myObject', {
        cascades: [ 'fields' ],
        fields: {
          add: {
            one: { type: 'string' },
            two: { type: 'string' },
            three: { type: 'string' }
          },
          group: {
            basics: {
              fields: [ 'one', 'two', 'three' ]
            }
          }
        }
      });

      await moog.define('myObject', {
        fields: {
          add: {
            four: { type: 'string' },
            five: { type: 'string' }
          },
          group: {
            basics: {
              fields: [ 'four', 'five' ]
            },
            other: {
              fields: [ 'one' ]
            }
          }
        }
      });

      const myObject = await moog.create('myObject', {});
      assert(myObject);
      assert(myObject.fieldsGroups);
      assert(!myObject.fieldsGroups.basics.fields.includes('one'));
      assert(myObject.fieldsGroups.other.fields.includes('one'));
      assert(myObject.fieldsGroups.basics.fields.includes('two'));
      assert(myObject.fieldsGroups.basics.fields.includes('three'));
      assert(myObject.fieldsGroups.basics.fields.includes('four'));
      assert(myObject.fieldsGroups.basics.fields.includes('five'));
    });

    it('should support operations alias of group fields', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('myObject', {
        cascades: [ 'batchOperations' ],
        batchOperations: {
          add: {
            one: { label: 'One' },
            two: { label: 'Two' },
            three: { label: 'Three' }
          },
          group: {
            more: {
              operations: [ 'one', 'two', 'three' ]
            }
          }
        }
      });

      await moog.define('myObject', {
        batchOperations: {
          add: {
            four: { label: 'Four' },
            five: { label: 'Five' }
          },
          group: {
            more: {
              operations: [ 'four', 'five' ]
            },
            other: {
              operations: [ 'one' ]
            }
          }
        }
      });

      const myObject = await moog.create('myObject', {});
      assert(myObject);
      assert(myObject.batchOperationsGroups);
      assert(!myObject.batchOperationsGroups.more.operations.includes('one'));
      assert(myObject.batchOperationsGroups.other.operations.includes('one'));
      assert(myObject.batchOperationsGroups.more.operations.includes('two'));
      assert(myObject.batchOperationsGroups.more.operations.includes('three'));
      assert(myObject.batchOperationsGroups.more.operations.includes('four'));
      assert(myObject.batchOperationsGroups.more.operations.includes('five'));
    });

    it('should order fields with the last option unless the order array overrides', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('unorderedObject', {
        cascades: [ 'fields' ],
        fields: {
          add: {
            first: { type: 'string' },
            last: {
              type: 'string',
              last: true
            },
            second: { type: 'string' },
            third: { type: 'string' }
          }
        }
      });

      await moog.define('orderedObject', {
        cascades: [ 'fields' ],
        fields: {
          add: {
            first: { type: 'string' },
            last: {
              type: 'string',
              last: true
            },
            second: { type: 'string' },
            third: { type: 'string' }
          },
          order: [ 'last', 'third', 'second', 'first' ]
        }
      });
      const unordered = await moog.create('unorderedObject', {});
      assert(unordered);
      assert(Object.keys(unordered.fields)[0] === 'first');
      assert(Object.keys(unordered.fields)[1] === 'second');
      assert(Object.keys(unordered.fields)[2] === 'third');
      assert(Object.keys(unordered.fields)[3] === 'last');

      const ordered = await moog.create('orderedObject', {});
      assert(ordered);
      assert(Object.keys(ordered.fields)[0] === 'last');
      assert(Object.keys(ordered.fields)[1] === 'third');
      assert(Object.keys(ordered.fields)[2] === 'second');
      assert(Object.keys(ordered.fields)[3] === 'first');
    });

    // ==================================================
    // `redefine` AND `isDefined`
    // ==================================================

    it('should allow a module to be redefined', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('myObject', {
        methods(self) {
          return {
            oldMethod() {}
          };
        }
      });

      await moog.redefine('myObject', {
        methods(self) {
          return {
            newMethod() {}
          };
        }
      });

      const myObject = await moog.create('myObject', {});
      assert(myObject);
      assert(!myObject.oldMethod);
      assert(myObject.newMethod);
    });

    it('should find a module definition using `isDefined`', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('myObject', {});

      assert.equal(await moog.isDefined('myObject'), true);
    });

    it('should NOT find a non-existant module definition using `isDefined`', async function() {
      const moog = require('../lib/moog.js')({});

      assert.equal(await moog.isDefined('myObject'), false);
    });

  });

  describe('implicit subclassing behavior', function() {
    it('should allow a class defined twice to be implicitly subclassed', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('myObject', {
        init(self) {
          self.order = (self.order || []).concat('first');
        }
      });

      await moog.define('myObject', {
        init(self) {
          self.order = (self.order || []).concat('second');
        }
      });

      const myObject = await moog.create('myObject', {});
      assert(myObject);
      assert(myObject.order[0] === 'first');
      assert(myObject.order[1] === 'second');
    });
  });

  describe('order of operations', function() {

    // ==================================================
    // ORDERING
    // ==================================================

    it('should call `init` methods baseClass-first', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('baseClass', {
        init(self) {
          self.order = (self.order || []).concat('first');
        }
      });

      await moog.define('subClassOne', {
        extend: 'baseClass',
        init(self) {
          self.order = (self.order || []).concat('second');
        }
      });

      await moog.define('subClassTwo', {
        extend: 'subClassOne',
        init(self) {
          self.order = (self.order || []).concat('third');
        }
      });

      const subClassTwo = await moog.create('subClassTwo', {});
      assert(subClassTwo.order[0] === 'first');
      assert(subClassTwo.order[1] === 'second');
      assert(subClassTwo.order[2] === 'third');
    });

    it('should call `beforeSuperClass` methods subClass-first', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('baseClass', {
        beforeSuperClass(self) {
          self.options.order = (self.options.order || []).concat('third');
        }
      });

      await moog.define('subClassOne', {
        extend: 'baseClass',
        beforeSuperClass(self) {
          self.options.order = (self.options.order || []).concat('second');
        }
      });

      await moog.define('subClassTwo', {
        extend: 'subClassOne',
        beforeSuperClass(self) {
          self.options.order = (self.options.order || []).concat('first');
        }
      });

      const subClassTwo = await moog.create('subClassTwo', {});
      assert(subClassTwo.options.order[0] === 'first');
      assert(subClassTwo.options.order[1] === 'second');
      assert(subClassTwo.options.order[2] === 'third');
    });

    // "sync and async playing nicely" and exception-catching
    // tests eliminated because the built-in language functionality
    // of async/await now handles those jobs and is independently tested. -Tom
  });

  describe('odds and ends', function() {

    it('should report an error on a cyclical reference (extend in a loop)', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('classOne', {
        extend: 'classTwo'
      });

      await moog.define('classTwo', {
        extend: 'classOne'
      });

      let e;
      let classOne;
      try {
        classOne = await moog.create('classOne', {});
      } catch (_e) {
        e = _e;
      }
      assert(e);
      assert(!classOne);
    });

    it('should report an error asynchronously when creating a nonexistent type asynchronously', async function() {
      const moog = require('../lib/moog.js')({});
      try {
        await moog.create('nonesuch');
        assert(false);
      } catch (e) {
        assert(true);
      }
    });

    it('instanceOf should yield correct results', async function() {
      const moog = require('../lib/moog.js')({});

      await moog.define('classOne', {});

      await moog.define('classTwo', {
        extend: 'classOne'
      });

      await moog.define('classThree', {});

      await moog.define('classFour', {
        extend: 'classTwo'
      });

      const one = await moog.create('classOne');
      const two = await moog.create('classTwo');
      const three = await moog.create('classThree');
      const four = await moog.create('classFour');
      const rando = { strange: 'object' };

      assert(moog.instanceOf(one, 'classOne'));
      assert(moog.instanceOf(two, 'classOne'));
      assert(!moog.instanceOf(three, 'classOne'));
      assert(moog.instanceOf(four, 'classOne'));
      assert(!moog.instanceOf(rando, 'classOne'));
    });

    it('sanity check of await behavior', async function() {
      const moog = require('../lib/moog.js')({});
      await moog.define('classOne', {
        async init(self) {
          await delay(100);
          self.size = 1;
        }
      });
      await moog.define('classTwo', {
        extend: 'classOne',
        async init(self) {
          await delay(1);
          self.size = 2;
        }
      });
      assert((await moog.create('classTwo', {})).size === 2);
    });

    it('isMy behaves sensibly', function() {
      const moog = require('../lib/moog.js')({});
      assert(moog.isMy('my-foo'));
      assert(!moog.isMy('foo'));
      assert(moog.isMy('@namespace/my-foo'));
      assert(!moog.isMy('@namespace/foo'));
    });

    it('originalToMy behaves sensibly', function() {
      const moog = require('../lib/moog.js')({});
      assert(moog.originalToMy('foo') === 'my-foo');
      assert(moog.originalToMy('@namespace/foo') === '@namespace/my-foo');
      // originalToMy is not guaranteed to do anything specific with
      // names that already have my-
    });

    it('myToOriginal behaves sensibly', function() {
      const moog = require('../lib/moog.js')({});
      assert(moog.myToOriginal('my-foo') === 'foo');
      assert(moog.myToOriginal('foo') === 'foo');
      assert(moog.myToOriginal('@namespace/my-foo') === '@namespace/foo');
      assert(moog.myToOriginal('@namespace/foo') === '@namespace/foo');
    });
  });

  describe('sections should work', function() {
    it('routes should work, including extending routes', async function() {
      const moog = require('../lib/moog.js')({ sections: [ 'routes' ] });
      await moog.define('baseclass', {
        routes(self) {
          return {
            post: {
              async insert(req) {
                return 'inserted';
              },
              async list(req) {
                return 'listed';
              }
            }
          };
        }
      });
      await moog.define('subclass', {
        extend: 'baseclass',
        routes(self) {
          return {
            post: {
              async remove(req) {
                return 'removed';
              },
              floss: [
                (req, res, next) => {
                  req.seen = true;
                  next();
                },
                (req) => 'flossed'
              ]
            }
          };
        },
        extendRoutes(self) {
          return {
            post: {
              async list(_super, req) {
                return (await _super(req)) + '-suffix';
              },
              async floss(_super, req) {
                return (await _super(req)) + '-daily';
              }
            }
          };
        }
      });
      const instance = await moog.create('subclass', {});
      assert(instance);
      assert((await instance.routes.post.insert({})) === 'inserted');
      assert((await instance.routes.post.list({})) === 'listed-suffix');
      assert((await instance.routes.post.remove({})) === 'removed');
      const req = {};
      const floss = await instance.routes.post.floss;
      // We are not here to reimplement middleware, just to verify
      // the chain is there and would work
      assert((typeof floss[0]) === 'function');
      floss[0](req, {}, function() {});
      assert(req.seen);
      assert((await floss[1](req)) === 'flossed-daily');
    });
  });

});

function delay(ms) {
  return new Promise(function(resolve, reject) {
    setTimeout(() => resolve(true), ms);
  });
}
