import { run } from '@ember/runloop';
import Engine, { setEngineParent } from '@ember/engine';
import EngineInstance from '@ember/engine/instance';
import {
  moduleFor,
  ModuleBasedTestResolver,
  AbstractTestCase as TestCase,
} from 'internal-test-helpers';

let MyEngine, myEngine, myEngineInstance;

function buildEngineInstance(EngineClass) {
  let engineInstance = EngineClass.buildInstance();
  setEngineParent(engineInstance, {
    lookup() {
      return {};
    },
    resolveRegistration() {
      // Implement Factory
      return { isFake: true, create: () => {} };
    },
  });
  return engineInstance;
}

moduleFor(
  'Engine instance initializers',
  class extends TestCase {
    teardown() {
      super.teardown();
      run(() => {
        if (myEngineInstance) {
          myEngineInstance.destroy();
        }

        if (myEngine) {
          myEngine.destroy();
        }
      });
      MyEngine = myEngine = myEngineInstance = undefined;
    }

    ["@test initializers require proper 'name' and 'initialize' properties"]() {
      MyEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });

      expectAssertion(() => {
        run(() => {
          MyEngine.instanceInitializer({ name: 'initializer' });
        });
      });

      expectAssertion(() => {
        run(() => {
          MyEngine.instanceInitializer({ initialize() {} });
        });
      });
    }

    ['@test initializers are passed an engine instance'](assert) {
      MyEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });

      MyEngine.instanceInitializer({
        name: 'initializer',
        initialize(instance) {
          assert.ok(instance instanceof EngineInstance, 'initialize is passed an engine instance');
        },
      });

      myEngine = MyEngine.create();
      myEngineInstance = buildEngineInstance(myEngine);
      return myEngineInstance.boot();
    }

    ['@test initializers can be registered in a specified order'](assert) {
      let order = [];

      MyEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });

      MyEngine.instanceInitializer({
        name: 'fourth',
        after: 'third',
        initialize() {
          order.push('fourth');
        },
      });

      MyEngine.instanceInitializer({
        name: 'second',
        after: 'first',
        before: 'third',
        initialize() {
          order.push('second');
        },
      });

      MyEngine.instanceInitializer({
        name: 'fifth',
        after: 'fourth',
        before: 'sixth',
        initialize() {
          order.push('fifth');
        },
      });

      MyEngine.instanceInitializer({
        name: 'first',
        before: 'second',
        initialize() {
          order.push('first');
        },
      });

      MyEngine.instanceInitializer({
        name: 'third',
        initialize() {
          order.push('third');
        },
      });

      MyEngine.instanceInitializer({
        name: 'sixth',
        initialize() {
          order.push('sixth');
        },
      });

      myEngine = MyEngine.create();
      myEngineInstance = buildEngineInstance(myEngine);

      return myEngineInstance.boot().then(() => {
        assert.deepEqual(order, ['first', 'second', 'third', 'fourth', 'fifth', 'sixth']);
      });
    }

    ['@test initializers can be registered in a specified order as an array'](assert) {
      let order = [];
      MyEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });

      MyEngine.instanceInitializer({
        name: 'third',
        initialize() {
          order.push('third');
        },
      });

      MyEngine.instanceInitializer({
        name: 'second',
        after: 'first',
        before: ['third', 'fourth'],
        initialize() {
          order.push('second');
        },
      });

      MyEngine.instanceInitializer({
        name: 'fourth',
        after: ['second', 'third'],
        initialize() {
          order.push('fourth');
        },
      });

      MyEngine.instanceInitializer({
        name: 'fifth',
        after: 'fourth',
        before: 'sixth',
        initialize() {
          order.push('fifth');
        },
      });

      MyEngine.instanceInitializer({
        name: 'first',
        before: ['second'],
        initialize() {
          order.push('first');
        },
      });

      MyEngine.instanceInitializer({
        name: 'sixth',
        initialize() {
          order.push('sixth');
        },
      });

      myEngine = MyEngine.create();
      myEngineInstance = buildEngineInstance(myEngine);

      return myEngineInstance.boot().then(() => {
        assert.deepEqual(order, ['first', 'second', 'third', 'fourth', 'fifth', 'sixth']);
      });
    }

    ['@test initializers can have multiple dependencies'](assert) {
      let order = [];

      MyEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });

      let a = {
        name: 'a',
        before: 'b',
        initialize() {
          order.push('a');
        },
      };
      let b = {
        name: 'b',
        initialize() {
          order.push('b');
        },
      };
      let c = {
        name: 'c',
        after: 'b',
        initialize() {
          order.push('c');
        },
      };
      let afterB = {
        name: 'after b',
        after: 'b',
        initialize() {
          order.push('after b');
        },
      };
      let afterC = {
        name: 'after c',
        after: 'c',
        initialize() {
          order.push('after c');
        },
      };

      MyEngine.instanceInitializer(b);
      MyEngine.instanceInitializer(a);
      MyEngine.instanceInitializer(afterC);
      MyEngine.instanceInitializer(afterB);
      MyEngine.instanceInitializer(c);

      myEngine = MyEngine.create();
      myEngineInstance = buildEngineInstance(myEngine);

      return myEngineInstance.boot().then(() => {
        assert.ok(order.indexOf(a.name) < order.indexOf(b.name), 'a < b');
        assert.ok(order.indexOf(b.name) < order.indexOf(c.name), 'b < c');
        assert.ok(order.indexOf(b.name) < order.indexOf(afterB.name), 'b < afterB');
        assert.ok(order.indexOf(c.name) < order.indexOf(afterC.name), 'c < afterC');
      });
    }

    ['@test initializers set on Engine subclasses should not be shared between engines'](assert) {
      let firstInitializerRunCount = 0;
      let secondInitializerRunCount = 0;
      let FirstEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });
      let firstEngine, firstEngineInstance;

      FirstEngine.instanceInitializer({
        name: 'first',
        initialize() {
          firstInitializerRunCount++;
        },
      });

      let SecondEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });
      let secondEngine, secondEngineInstance;

      SecondEngine.instanceInitializer({
        name: 'second',
        initialize() {
          secondInitializerRunCount++;
        },
      });

      firstEngine = FirstEngine.create();
      firstEngineInstance = buildEngineInstance(firstEngine);

      return firstEngineInstance
        .boot()
        .then(() => {
          assert.equal(firstInitializerRunCount, 1, 'first initializer only was run');
          assert.equal(secondInitializerRunCount, 0, 'first initializer only was run');

          secondEngine = SecondEngine.create();
          secondEngineInstance = buildEngineInstance(secondEngine);
          return secondEngineInstance.boot();
        })
        .then(() => {
          assert.equal(firstInitializerRunCount, 1, 'second initializer only was run');
          assert.equal(secondInitializerRunCount, 1, 'second initializer only was run');

          run(() => {
            firstEngineInstance.destroy();
            secondEngineInstance.destroy();

            firstEngine.destroy();
            secondEngine.destroy();
          });
        });
    }

    ['@test initializers are concatenated'](assert) {
      let firstInitializerRunCount = 0;
      let secondInitializerRunCount = 0;
      let FirstEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });

      FirstEngine.instanceInitializer({
        name: 'first',
        initialize() {
          firstInitializerRunCount++;
        },
      });

      let SecondEngine = FirstEngine.extend();

      SecondEngine.instanceInitializer({
        name: 'second',
        initialize() {
          secondInitializerRunCount++;
        },
      });

      let firstEngine = FirstEngine.create();
      let firstEngineInstance = buildEngineInstance(firstEngine);

      let secondEngine, secondEngineInstance;

      return firstEngineInstance
        .boot()
        .then(() => {
          assert.equal(
            firstInitializerRunCount,
            1,
            'first initializer only was run when base class created'
          );
          assert.equal(
            secondInitializerRunCount,
            0,
            'second initializer was not run when first base class created'
          );
          firstInitializerRunCount = 0;

          secondEngine = SecondEngine.create();
          secondEngineInstance = buildEngineInstance(secondEngine);
          return secondEngineInstance.boot();
        })
        .then(() => {
          assert.equal(
            firstInitializerRunCount,
            1,
            'first initializer was run when subclass created'
          );
          assert.equal(
            secondInitializerRunCount,
            1,
            'second initializers was run when subclass created'
          );

          run(() => {
            firstEngineInstance.destroy();
            secondEngineInstance.destroy();

            firstEngine.destroy();
            secondEngine.destroy();
          });
        });
    }

    ['@test initializers are per-engine'](assert) {
      assert.expect(2);

      let FirstEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });

      FirstEngine.instanceInitializer({
        name: 'abc',
        initialize() {},
      });

      expectAssertion(() => {
        FirstEngine.instanceInitializer({
          name: 'abc',
          initialize() {},
        });
      });

      let SecondEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });
      SecondEngine.instanceInitializer({
        name: 'abc',
        initialize() {},
      });

      assert.ok(true, 'Two engines can have initializers named the same.');
    }

    ['@test initializers are executed in their own context'](assert) {
      assert.expect(1);

      let MyEngine = Engine.extend({
        Resolver: ModuleBasedTestResolver,
      });

      MyEngine.instanceInitializer({
        name: 'coolInitializer',
        myProperty: 'cool',
        initialize() {
          assert.equal(this.myProperty, 'cool', 'should have access to its own context');
        },
      });

      myEngine = MyEngine.create();
      myEngineInstance = buildEngineInstance(myEngine);

      return myEngineInstance.boot();
    }
  }
);
