import EmberObject, { computed, observer } from '@ember/object';
import {
  defineProperty,
  on,
  addObserver,
  removeObserver,
  addListener,
  removeListener,
  sendEvent,
} from '@ember/-internals/metal';
import Mixin from '@ember/object/mixin';
import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers';

moduleFor(
  'EmberObject ES Compatibility',
  class extends AbstractTestCase {
    ['@test extending an Ember.Object'](assert) {
      let calls = [];

      class MyObject extends EmberObject {
        constructor() {
          calls.push('constructor');
          super(...arguments);
          this.postInitProperty = 'post-init-property';
        }

        init() {
          calls.push('init');
          super.init(...arguments);
          this.initProperty = 'init-property';
        }
      }

      let myObject = MyObject.create({ passedProperty: 'passed-property' });

      assert.deepEqual(calls, ['constructor', 'init'], 'constructor then init called (create)');
      assert.equal(
        myObject.postInitProperty,
        'post-init-property',
        'constructor property available on instance (create)'
      );
      assert.equal(
        myObject.initProperty,
        'init-property',
        'init property available on instance (create)'
      );
      assert.equal(
        myObject.passedProperty,
        'passed-property',
        'passed property available on instance (create)'
      );
    }

    ['@test normal method super'](assert) {
      let calls = [];

      let Foo = EmberObject.extend({
        method() {
          calls.push('foo');
        },
      });

      let Bar = Foo.extend({
        method() {
          this._super();
          calls.push('bar');
        },
      });

      class Baz extends Bar {
        method() {
          super.method();
          calls.push('baz');
        }
      }

      let Qux = Baz.extend({
        method() {
          this._super();
          calls.push('qux');
        },
      });

      let Quux = Qux.extend({
        method() {
          this._super();
          calls.push('quux');
        },
      });

      class Corge extends Quux {
        method() {
          super.method();
          calls.push('corge');
        }
      }

      let callValues = ['foo', 'bar', 'baz', 'qux', 'quux', 'corge'];

      [Foo, Bar, Baz, Qux, Quux, Corge].forEach((Class, index) => {
        calls = [];
        Class.create().method();

        assert.deepEqual(
          calls,
          callValues.slice(0, index + 1),
          'chain of static methods called with super'
        );
      });
    }

    ['@test static method super'](assert) {
      let calls;

      let Foo = EmberObject.extend();
      Foo.reopenClass({
        method() {
          calls.push('foo');
        },
      });

      let Bar = Foo.extend();
      Bar.reopenClass({
        method() {
          this._super();
          calls.push('bar');
        },
      });

      class Baz extends Bar {
        static method() {
          super.method();
          calls.push('baz');
        }
      }

      let Qux = Baz.extend();
      Qux.reopenClass({
        method() {
          this._super();
          calls.push('qux');
        },
      });

      let Quux = Qux.extend();
      Quux.reopenClass({
        method() {
          this._super();
          calls.push('quux');
        },
      });

      class Corge extends Quux {
        static method() {
          super.method();
          calls.push('corge');
        }
      }

      let callValues = ['foo', 'bar', 'baz', 'qux', 'quux', 'corge'];

      [Foo, Bar, Baz, Qux, Quux, Corge].forEach((Class, index) => {
        calls = [];
        Class.method();

        assert.deepEqual(
          calls,
          callValues.slice(0, index + 1),
          'chain of static methods called with super'
        );
      });
    }

    ['@test using mixins'](assert) {
      let Mixin1 = Mixin.create({
        property1: 'data-1',
      });

      let Mixin2 = Mixin.create({
        property2: 'data-2',
      });

      class MyObject extends EmberObject.extend(Mixin1, Mixin2) {}

      let myObject = MyObject.create();
      assert.equal(myObject.property1, 'data-1', 'includes the first mixin');
      assert.equal(myObject.property2, 'data-2', 'includes the second mixin');
    }

    ['@test using instanceof'](assert) {
      class MyObject extends EmberObject {}

      let myObject = MyObject.create();

      assert.ok(myObject instanceof MyObject);
      assert.ok(myObject instanceof EmberObject);
    }

    ['@test using Ember.Object#detect'](assert) {
      let Parent = EmberObject.extend();
      class Child extends Parent {}
      let Grandchild = Child.extend();

      assert.ok(Parent.detect(Child), 'Parent.detect(Child)');
      assert.ok(Child.detect(Grandchild), 'Child.detect(Grandchild)');
    }

    ['@test extending an ES subclass of EmberObject'](assert) {
      let calls = [];

      class SubEmberObject extends EmberObject {
        constructor() {
          calls.push('constructor');
          super(...arguments);
        }

        init() {
          calls.push('init');
          super.init(...arguments);
        }
      }

      class MyObject extends SubEmberObject {}

      MyObject.create();
      assert.deepEqual(calls, ['constructor', 'init'], 'constructor then init called (create)');
    }

    ['@test calling extend on an ES subclass of EmberObject'](assert) {
      let calls = [];

      class SubEmberObject extends EmberObject {
        constructor() {
          calls.push('before constructor');
          super(...arguments);
          calls.push('after constructor');
          this.foo = 123;
        }

        init() {
          calls.push('init');
          super.init(...arguments);
        }
      }

      let MyObject = SubEmberObject.extend({});

      MyObject.create();
      assert.deepEqual(
        calls,
        ['before constructor', 'after constructor', 'init'],
        'constructor then init called (create)'
      );

      let obj = MyObject.create({
        foo: 456,
        bar: 789,
      });

      assert.equal(obj.foo, 456, 'sets class fields on instance correctly');
      assert.equal(obj.bar, 789, 'sets passed in properties on instance correctly');
    }

    ['@test calling metaForProperty on a native class works'](assert) {
      assert.expect(0);

      class SubEmberObject extends EmberObject {}

      defineProperty(
        SubEmberObject.prototype,
        'foo',
        computed('foo', {
          get() {
            return 'bar';
          },
        })
      );

      // able to get meta without throwing an error
      SubEmberObject.metaForProperty('foo');
    }

    '@test observes / removeObserver on / removeListener interop'(assert) {
      let fooDidChangeBase = 0;
      let fooDidChangeA = 0;
      let fooDidChangeB = 0;
      let someEventBase = 0;
      let someEventA = 0;
      let someEventB = 0;
      class A extends EmberObject.extend({
        fooDidChange: observer('foo', function () {
          fooDidChangeBase++;
        }),

        onSomeEvent: on('someEvent', function () {
          someEventBase++;
        }),
      }) {
        init() {
          super.init();
          this.foo = 'bar';
        }

        fooDidChange() {
          super.fooDidChange();
          fooDidChangeA++;
        }

        onSomeEvent() {
          super.onSomeEvent();
          someEventA++;
        }
      }

      class B extends A {
        fooDidChange() {
          super.fooDidChange();
          fooDidChangeB++;
        }

        onSomeEvent() {
          super.onSomeEvent();
          someEventB++;
        }
      }

      removeObserver(B.prototype, 'foo', null, 'fooDidChange');
      removeListener(B.prototype, 'someEvent', null, 'onSomeEvent');

      assert.equal(fooDidChangeBase, 0);
      assert.equal(fooDidChangeA, 0);
      assert.equal(fooDidChangeB, 0);

      assert.equal(someEventBase, 0);
      assert.equal(someEventA, 0);
      assert.equal(someEventB, 0);

      let a = A.create();
      a.set('foo', 'something');

      // TODO: Generator transpilation code doesn't play nice with class definitions/hoisting
      return runLoopSettled().then(async () => {
        assert.equal(fooDidChangeBase, 1);
        assert.equal(fooDidChangeA, 1);
        assert.equal(fooDidChangeB, 0);

        sendEvent(a, 'someEvent');
        assert.equal(someEventBase, 1);
        assert.equal(someEventA, 1);
        assert.equal(someEventB, 0);

        let b = B.create();
        b.set('foo', 'something');
        await runLoopSettled();

        assert.equal(fooDidChangeBase, 1);
        assert.equal(fooDidChangeA, 1);
        assert.equal(fooDidChangeB, 0);

        sendEvent(b, 'someEvent');
        assert.equal(someEventBase, 1);
        assert.equal(someEventA, 1);
        assert.equal(someEventB, 0);

        a.destroy();
        b.destroy();
      });
    }

    '@test super and _super interop between old and new methods'(assert) {
      let calls = [];
      let changes = [];
      let events = [];
      let lastProps;

      class A extends EmberObject {
        init(props) {
          calls.push('A init');
          lastProps = props;
        }
      }

      let Mixin1 = Mixin.create({
        init() {
          calls.push('Mixin1 init before _super');
          this._super(...arguments);
          calls.push('Mixin1 init after _super');
        },
      });

      let Mixin2 = Mixin.create({
        init() {
          calls.push('Mixin2 init before _super');
          this._super(...arguments);
          calls.push('Mixin2 init after _super');
        },
      });

      class B extends A.extend(Mixin1, Mixin2) {
        init() {
          calls.push('B init before super.init');
          super.init(...arguments);
          calls.push('B init after super.init');
        }

        onSomeEvent(evt) {
          events.push(`B onSomeEvent ${evt}`);
        }

        fullNameDidChange() {
          changes.push('B fullNameDidChange');
        }
      }

      // // define a CP
      defineProperty(
        B.prototype,
        'full',
        computed('first', 'last', {
          get() {
            return this.first + ' ' + this.last;
          },
        })
      );

      // Only string observers are allowed for prototypes
      addObserver(B.prototype, 'full', null, 'fullNameDidChange');

      // Only string listeners are allowed for prototypes
      addListener(B.prototype, 'someEvent', null, 'onSomeEvent');

      B.reopen({
        init() {
          calls.push('reopen init before _super');
          this._super(...arguments);
          calls.push('reopen init after _super');
        },
      });

      let C = B.extend({
        init() {
          calls.push('C init before _super');
          this._super(...arguments);
          calls.push('C init after _super');
        },

        onSomeEvent(evt) {
          calls.push('C onSomeEvent before _super');
          this._super(evt);
          calls.push('C onSomeEvent after _super');
        },

        fullNameDidChange() {
          calls.push('C fullNameDidChange before _super');
          this._super();
          calls.push('C fullNameDidChange after _super');
        },
      });

      class D extends C {
        init() {
          calls.push('D init before super.init');
          super.init(...arguments);
          calls.push('D init after super.init');
        }

        onSomeEvent(evt) {
          events.push('D onSomeEvent before super.onSomeEvent');
          super.onSomeEvent(evt);
          events.push('D onSomeEvent after super.onSomeEvent');
        }

        fullNameDidChange() {
          changes.push('D fullNameDidChange before super.fullNameDidChange');
          super.fullNameDidChange();
          changes.push('D fullNameDidChange after super.fullNameDidChange');
        }

        triggerSomeEvent(...args) {
          sendEvent(this, 'someEvent', args);
        }
      }

      assert.deepEqual(calls, [], 'nothing has been called');
      assert.deepEqual(changes, [], 'full has not changed');
      assert.deepEqual(events, [], 'onSomeEvent has not been triggered');

      let d = D.create({ first: 'Robert', last: 'Jackson' });

      assert.deepEqual(calls, [
        'D init before super.init',
        'C init before _super',
        'reopen init before _super',
        'B init before super.init',
        'Mixin2 init before _super',
        'Mixin1 init before _super',
        'A init',
        'Mixin1 init after _super',
        'Mixin2 init after _super',
        'B init after super.init',
        'reopen init after _super',
        'C init after _super',
        'D init after super.init',
      ]);
      assert.deepEqual(changes, [], 'full has not changed');
      assert.deepEqual(events, [], 'onSomeEvent has not been triggered');

      assert.deepEqual(lastProps, {
        first: 'Robert',
        last: 'Jackson',
      });

      assert.equal(d.full, 'Robert Jackson');

      d.setProperties({ first: 'Kris', last: 'Selden' });

      // TODO: Generator transpilation code doesn't play nice with class definitions/hoisting
      return runLoopSettled().then(() => {
        assert.deepEqual(changes, [
          'D fullNameDidChange before super.fullNameDidChange',
          'B fullNameDidChange',
          'D fullNameDidChange after super.fullNameDidChange',
        ]);

        assert.equal(d.full, 'Kris Selden');

        d.triggerSomeEvent('event arg');
        assert.deepEqual(events, [
          'D onSomeEvent before super.onSomeEvent',
          'B onSomeEvent event arg',
          'D onSomeEvent after super.onSomeEvent',
        ]);

        d.destroy();
      });
    }
  }
);
