import { DI, IContainer, inject, InterfaceSymbol, Registration, singleton } from '@aurelia/kernel';
import { assert, createSpy, ISpy } from '@aurelia/testing';

describe('1-kernel/di.integration.spec.ts', function () {
  describe('registerInRequester', function () {
    class Foo {
    }
    const fooSelfRegister = DI.singleton(Foo, { scoped: true });

    it('root', function () {
      const root = DI.createContainer();
      const foo1 = root.get(fooSelfRegister);
      const foo2 = root.get(fooSelfRegister);

      assert.strictEqual(foo1, foo2);
    });

    it('children', function () {
      const root = DI.createContainer();
      const child1 = root.createChild();
      const child2 = root.createChild();
      const foo1 = child1.get(fooSelfRegister);
      const foo2 = child2.get(fooSelfRegister);

      assert.notStrictEqual(foo1, foo2);
    });
  });

  // TODO: Enable those tests once the decorator metadata is emitted by TS.
  // The tests are disabled because TS with TC39 decorators (non-legacy), does not emit the decorator metadata as of now.
  // The following tests are dependent on that and hence cannot be successfully run.
  // Refer: https://github.com/microsoft/TypeScript/issues/55788
  describe.skip('DI.getDependencies', function () {
    it('string param', function () {
      @singleton
      class Foo {
        public constructor(public readonly test: string) { }
      }
      const actual = DI.getDependencies(Foo);
      assert.deepStrictEqual(actual, [String]);
    });

    it('class param', function () {
      class Bar { }
      @singleton
      class Foo {
        public constructor(public readonly test: Bar) { }
      }
      const actual = DI.getDependencies(Foo);
      assert.deepStrictEqual(actual, [Bar]);
    });
  });

  describe('DI.createInterface() -> container.get()', function () {
    let container: IContainer;

    interface ITransient { }
    class Transient implements ITransient { }
    let ITransient: InterfaceSymbol<ITransient>;

    interface ISingleton { }
    class Singleton implements ISingleton { }
    let ISingleton: InterfaceSymbol<ISingleton>;

    interface IInstance { }
    class Instance implements IInstance { }
    let IInstance: InterfaceSymbol<IInstance>;
    let instance: Instance;

    interface ICallback { }
    class Callback implements ICallback { }
    let ICallback: InterfaceSymbol<ICallback>;

    interface ICachedCallback { }
    class CachedCallback implements ICachedCallback { }
    let ICachedCallback: InterfaceSymbol<ICachedCallback>;
    const cachedCallback = 'cachedCallBack';
    let callbackCount = 0;
    function callbackToCache() {
      ++callbackCount;
      return new CachedCallback();
    }

    let callback: ISpy<() => ICallback>;
    let get: ISpy<IContainer['get']>;

    beforeEach(function () {
      callbackCount = 0;
      container = DI.createContainer();
      ITransient = DI.createInterface<ITransient>('ITransient', x => x.transient(Transient));
      ISingleton = DI.createInterface<ISingleton>('ISingleton', x => x.singleton(Singleton));
      instance = new Instance();
      IInstance = DI.createInterface<IInstance>('IInstance', x => x.instance(instance));
      callback = createSpy(() => new Callback());
      ICallback = DI.createInterface<ICallback>('ICallback', x => x.callback(callback));
      ICachedCallback = DI.createInterface<ICachedCallback>('ICachedCallback', x => x.cachedCallback(callbackToCache));
      get = createSpy(container, 'get', true);
    });

    afterEach(function () {
      get.restore();
    });

    describe('leaf', function () {
      it(`transient registration returns a new instance each time`, function () {
        const actual1 = container.get(ITransient);
        assert.instanceOf(actual1, Transient, `actual1`);

        const actual2 = container.get(ITransient);
        assert.instanceOf(actual2, Transient, `actual2`);

        assert.notStrictEqual(actual1, actual2, `actual1`);

        assert.deepStrictEqual(
          get.calls,
          [
            [ITransient],
            [ITransient],
          ],
          `get.calls`,
        );
      });

      it(`singleton registration returns the same instance each time`, function () {
        const actual1 = container.get(ISingleton);
        assert.instanceOf(actual1, Singleton, `actual1`);

        const actual2 = container.get(ISingleton);
        assert.instanceOf(actual2, Singleton, `actual2`);

        assert.strictEqual(actual1, actual2, `actual1`);

        assert.deepStrictEqual(
          get.calls,
          [
            [ISingleton],
            [ISingleton],
          ],
          `get.calls`,
        );
      });

      it(`instance registration returns the same instance each time`, function () {
        const actual1 = container.get(IInstance);
        assert.instanceOf(actual1, Instance, `actual1`);

        const actual2 = container.get(IInstance);
        assert.instanceOf(actual2, Instance, `actual2`);

        assert.strictEqual(actual1, instance, `actual1`);
        assert.strictEqual(actual2, instance, `actual2`);

        assert.deepStrictEqual(
          get.calls,
          [
            [IInstance],
            [IInstance],
          ],
          `get.calls`,
        );
      });

      it(`callback registration is invoked each time`, function () {
        const actual1 = container.get(ICallback);
        assert.instanceOf(actual1, Callback, `actual1`);

        const actual2 = container.get(ICallback);
        assert.instanceOf(actual2, Callback, `actual2`);

        assert.notStrictEqual(actual1, actual2, `actual1`);

        assert.deepStrictEqual(
          callback.calls,
          [
            [container, container, container.getResolver(ICallback)],
            [container, container, container.getResolver(ICallback)],
          ],
          `callback.calls`,
        );

        assert.deepStrictEqual(
          get.calls,
          [
            [ICallback],
            [ICallback],
          ],
          `get.calls`,
        );
      });

      it(`cachedCallback registration is invoked once`, function () {

        container.register(Registration.cachedCallback(cachedCallback, callbackToCache));
        const child = container.createChild();
        child.register(Registration.cachedCallback(cachedCallback, callbackToCache));
        const actual1 = container.get(cachedCallback);
        const actual2 = container.get(cachedCallback);

        assert.strictEqual(callbackCount, 1, `only called once`);
        assert.strictEqual(actual2, actual1, `getting from the same container`);

        const actual3 = child.get(cachedCallback);
        assert.notStrictEqual(actual3, actual1, `get from child that has new resolver`);
      });

      it(`cacheCallback multiple root containers`, function () {
        const container0 = DI.createContainer();
        const container1 = DI.createContainer();
        container0.register(Registration.cachedCallback(cachedCallback, callbackToCache));
        container1.register(Registration.cachedCallback(cachedCallback, callbackToCache));

        const actual11 = container0.get(cachedCallback);
        const actual12 = container0.get(cachedCallback);

        assert.strictEqual(callbackCount, 1, 'one callback');
        assert.strictEqual(actual11, actual12);

        const actual21 = container1.get(cachedCallback);
        const actual22 = container1.get(cachedCallback);

        assert.strictEqual(callbackCount, 2);
        assert.strictEqual(actual21, actual22);
      });

      it(`cacheCallback different containers should not create the same singleton GH #1064`, function () {
        const reg = Registration.cachedCallback(cachedCallback, callbackToCache);
        const container0 = DI.createContainer();
        const container1 = DI.createContainer();
        container0.register(reg);
        container1.register(reg);

        const actual11 = container0.get(cachedCallback);
        const actual12 = container0.get(cachedCallback);

        assert.strictEqual(actual11, actual12, "11 equals 12");
        assert.strictEqual(callbackCount, 1, "callback count 1");

        const actual21 = container1.get(cachedCallback);
        const actual22 = container1.get(cachedCallback);

        assert.strictEqual(actual21, actual22, "21 equals 22");
        assert.strictEqual(callbackCount, 2, "callback count 2");

        assert.notStrictEqual(actual11, actual21, "11 does not equal 21");
      });

      it(`cachedCallback registration on interface is invoked once`, function () {
        const actual1 = container.get(ICachedCallback);
        const actual2 = container.get(ICachedCallback);

        assert.strictEqual(callbackCount, 1, `only called once`);
        assert.strictEqual(actual2, actual1, `getting from the same container`);
      });

      it(`cacheCallback interface multiple root containers`, function () {
        const container0 = DI.createContainer();
        const container1 = DI.createContainer();
        const actual11 = container0.get(ICachedCallback);
        const actual12 = container0.get(ICachedCallback);

        assert.strictEqual(callbackCount, 1);
        assert.strictEqual(actual11, actual12);

        const actual21 = container1.get(ICachedCallback);
        const actual22 = container1.get(ICachedCallback);

        assert.strictEqual(callbackCount, 2);
        assert.strictEqual(actual21, actual22);
      });

      it(`InterfaceSymbol alias to transient registration returns a new instance each time`, function () {
        interface IAlias { }
        const IAlias = DI.createInterface<IAlias>('IAlias', x => x.aliasTo(ITransient));

        const actual1 = container.get(IAlias);
        assert.instanceOf(actual1, Transient, `actual1`);

        const actual2 = container.get(IAlias);
        assert.instanceOf(actual2, Transient, `actual2`);

        assert.notStrictEqual(actual1, actual2, `actual1`);

        assert.deepStrictEqual(
          get.calls,
          [
            [IAlias],
            [ITransient],
            [IAlias],
            [ITransient],
          ],
          `get.calls`,
        );
      });

      it(`InterfaceSymbol alias to singleton registration returns the same instance each time`, function () {
        interface IAlias { }
        const IAlias = DI.createInterface<IAlias>('IAlias', x => x.aliasTo(ISingleton));

        const actual1 = container.get(IAlias);
        assert.instanceOf(actual1, Singleton, `actual1`);

        const actual2 = container.get(IAlias);
        assert.instanceOf(actual2, Singleton, `actual2`);

        assert.strictEqual(actual1, actual2, `actual1`);

        assert.deepStrictEqual(
          get.calls,
          [
            [IAlias],
            [ISingleton],
            [IAlias],
            [ISingleton],
          ],
          `get.calls`,
        );
      });

      it(`InterfaceSymbol alias to instance registration returns the same instance each time`, function () {
        interface IAlias { }
        const IAlias = DI.createInterface<IAlias>('IAlias', x => x.aliasTo(IInstance));

        const actual1 = container.get(IAlias);
        assert.instanceOf(actual1, Instance, `actual1`);

        const actual2 = container.get(IAlias);
        assert.instanceOf(actual2, Instance, `actual2`);

        assert.strictEqual(actual1, instance, `actual1`);
        assert.strictEqual(actual2, instance, `actual2`);

        assert.deepStrictEqual(
          get.calls,
          [
            [IAlias],
            [IInstance],
            [IAlias],
            [IInstance],
          ],
          `get.calls`,
        );
      });

      // TODO: make test work
      it(`InterfaceSymbol alias to callback registration is invoked each time`, function () {
        interface IAlias { }
        const IAlias = DI.createInterface<IAlias>('IAlias', x => x.aliasTo(ICallback));

        const actual1 = container.get(IAlias);
        assert.instanceOf(actual1, Callback, `actual1`);

        const actual2 = container.get(IAlias);
        assert.instanceOf(actual2, Callback, `actual2`);

        assert.notStrictEqual(actual1, actual2, `actual1`);

        assert.deepStrictEqual(
          callback.calls,
          [
            [container, container, container.getResolver(ICallback)],
            [container, container, container.getResolver(ICallback)],
          ],
          `callback.calls`,
        );

        assert.deepStrictEqual(
          get.calls,
          [
            [IAlias],
            [ICallback],
            [IAlias],
            [ICallback],
          ],
          `get.calls`,
        );
      });

      it(`string alias to transient registration returns a new instance each time`, function () {
        container.register(Registration.aliasTo(ITransient, 'alias'));

        const actual1 = container.get('alias');
        assert.instanceOf(actual1, Transient, `actual1`);

        const actual2 = container.get('alias');
        assert.instanceOf(actual2, Transient, `actual2`);

        assert.notStrictEqual(actual1, actual2, `actual1`);

        assert.deepStrictEqual(
          get.calls,
          [
            ['alias'],
            [ITransient],
            ['alias'],
            [ITransient],
          ],
          `get.calls`,
        );
      });

      it(`string alias to singleton registration returns the same instance each time`, function () {
        container.register(Registration.aliasTo(ISingleton, 'alias'));

        const actual1 = container.get('alias');
        assert.instanceOf(actual1, Singleton, `actual1`);

        const actual2 = container.get('alias');
        assert.instanceOf(actual2, Singleton, `actual2`);

        assert.strictEqual(actual1, actual2, `actual1`);

        assert.deepStrictEqual(
          get.calls,
          [
            ['alias'],
            [ISingleton],
            ['alias'],
            [ISingleton],
          ],
          `get.calls`,
        );
      });

      it(`string alias to instance registration returns the same instance each time`, function () {
        container.register(Registration.aliasTo(IInstance, 'alias'));

        const actual1 = container.get('alias');
        assert.instanceOf(actual1, Instance, `actual1`);

        const actual2 = container.get('alias');
        assert.instanceOf(actual2, Instance, `actual2`);

        assert.strictEqual(actual1, instance, `actual1`);
        assert.strictEqual(actual2, instance, `actual2`);

        assert.deepStrictEqual(
          get.calls,
          [
            ['alias'],
            [IInstance],
            ['alias'],
            [IInstance],
          ],
          `get.calls`,
        );
      });

      it(`string alias to callback registration is invoked each time`, function () {
        container.register(Registration.aliasTo(ICallback, 'alias'));

        const actual1 = container.get('alias');
        assert.instanceOf(actual1, Callback, `actual1`);

        const actual2 = container.get('alias');
        assert.instanceOf(actual2, Callback, `actual2`);

        assert.notStrictEqual(actual1, actual2, `actual1`);

        assert.deepStrictEqual(
          callback.calls,
          [
            [container, container, container.getResolver(ICallback)],
            [container, container, container.getResolver(ICallback)],
          ],
          `callback.calls`,
        );

        assert.deepStrictEqual(
          get.calls,
          [
            ['alias'],
            [ICallback],
            ['alias'],
            [ICallback],
          ],
          `get.calls`,
        );
      });
    });

    // describe('parent without inject decorator', function () {
    //   function decorator(): ClassDecorator { return (target: any) => target; }
    //   interface IParent { dep: any; }
    //   let IParent: InterfaceSymbol<IParent>;

    //   function register(cls: any) {
    //     IParent = DI.createInterface<IParent>('IParent', x => x.transient(cls));
    //   }

    //   it(`transient child registration throws`, function () {
    //     @decorator()
    //     class Parent implements IParent { constructor(public dep: ITransient) {} }
    //     register(Parent);

    //     assert.throws(() => container.get(IParent), /5/, `() => container.get(IParent)`);
    //   });

    //   it(`singleton child registration throws`, function () {
    //     @decorator()
    //     class Parent implements IParent { constructor(public dep: ISingleton) {} }
    //     register(Parent);

    //     assert.throws(() => container.get(IParent), /5/, `() => container.get(IParent)`);
    //   });

    //   it(`instance child registration throws`, function () {
    //     @decorator()
    //     class Parent implements IParent { constructor(public dep: IInstance) {} }
    //     register(Parent);

    //     assert.throws(() => container.get(IParent), /5/, `() => container.get(IParent)`);
    //   });

    //   it(`callback child registration throws`, function () {
    //     @decorator()
    //     class Parent implements IParent { constructor(public dep: ICallback) {} }
    //     register(Parent);

    //     assert.throws(() => container.get(IParent), /5/, `() => container.get(IParent)`);
    //   });
    // });

    describe('transient parent', function () {
      interface ITransientParent { dep: any }
      let ITransientParent: InterfaceSymbol<ITransientParent>;

      function register(cls: any) {
        ITransientParent = DI.createInterface<ITransientParent>('ITransientParent', x => x.transient(cls));
      }

      it(`transient child registration returns a new instance each time`, function () {
        @inject(ITransient)
        class TransientParent implements ITransientParent { public constructor(public dep: ITransient) { } }
        register(TransientParent);

        const actual1 = container.get(ITransientParent);
        assert.instanceOf(actual1, TransientParent, `actual1`);
        assert.instanceOf(actual1.dep, Transient, `actual1.dep`);

        const actual2 = container.get(ITransientParent);
        assert.instanceOf(actual2, TransientParent, `actual2`);
        assert.instanceOf(actual2.dep, Transient, `actual2.dep`);

        assert.notStrictEqual(actual1, actual2, `actual1`);

        assert.notStrictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          get.calls,
          [
            [ITransientParent],
            [ITransient],
            [ITransientParent],
            [ITransient],
          ],
          `get.calls`,
        );
      });

      it(`singleton child registration returns the same instance each time`, function () {
        @inject(ISingleton)
        class TransientParent implements ITransientParent { public constructor(public dep: ISingleton) { } }
        register(TransientParent);

        const actual1 = container.get(ITransientParent);
        assert.instanceOf(actual1, TransientParent, `actual1`);
        assert.instanceOf(actual1.dep, Singleton, `actual1.dep`);

        const actual2 = container.get(ITransientParent);
        assert.instanceOf(actual2, TransientParent, `actual2`);
        assert.instanceOf(actual2.dep, Singleton, `actual2.dep`);

        assert.notStrictEqual(actual1, actual2, `actual1`);

        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          get.calls,
          [
            [ITransientParent],
            [ISingleton],
            [ITransientParent],
            [ISingleton],
          ],
          `get.calls`,
        );
      });

      it(`instance child registration returns the same instance each time`, function () {
        @inject(IInstance)
        class TransientParent implements ITransientParent { public constructor(public dep: IInstance) { } }
        register(TransientParent);

        const actual1 = container.get(ITransientParent);
        assert.instanceOf(actual1, TransientParent, `actual1`);
        assert.instanceOf(actual1.dep, Instance, `actual1.dep`);

        const actual2 = container.get(ITransientParent);
        assert.instanceOf(actual2, TransientParent, `actual2`);
        assert.instanceOf(actual2.dep, Instance, `actual2.dep`);

        assert.notStrictEqual(actual1, actual2, `actual1`);

        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          get.calls,
          [
            [ITransientParent],
            [IInstance],
            [ITransientParent],
            [IInstance],
          ],
          `get.calls`,
        );
      });

      it(`callback child registration is invoked each time`, function () {
        @inject(ICallback)
        class TransientParent implements ITransientParent { public constructor(public dep: ICallback) { } }
        register(TransientParent);

        const actual1 = container.get(ITransientParent);
        assert.instanceOf(actual1, TransientParent, `actual1`);
        assert.instanceOf(actual1.dep, Callback, `actual1.dep`);

        const actual2 = container.get(ITransientParent);
        assert.instanceOf(actual2, TransientParent, `actual2`);
        assert.instanceOf(actual2.dep, Callback, `actual2.dep`);

        assert.notStrictEqual(actual1, actual2, `actual1`);
        assert.notStrictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          callback.calls,
          [
            [container, container, container.getResolver(ICallback)],
            [container, container, container.getResolver(ICallback)],
          ],
          `callback.calls`,
        );

        assert.deepStrictEqual(
          get.calls,
          [
            [ITransientParent],
            [ICallback],
            [ITransientParent],
            [ICallback],
          ],
          `get.calls`,
        );
      });
    });

    describe('singleton parent', function () {
      interface ISingletonParent { dep: any }
      let ISingletonParent: InterfaceSymbol<ISingletonParent>;

      function register(cls: any) {
        ISingletonParent = DI.createInterface<ISingletonParent>('ISingletonParent', x => x.singleton(cls));
      }

      it(`transient child registration is reused by the singleton parent`, function () {
        @inject(ITransient)
        class SingletonParent implements ISingletonParent { public constructor(public dep: ITransient) { } }
        register(SingletonParent);

        const actual1 = container.get(ISingletonParent);
        assert.instanceOf(actual1, SingletonParent, `actual1`);
        assert.instanceOf(actual1.dep, Transient, `actual1.dep`);

        const actual2 = container.get(ISingletonParent);
        assert.instanceOf(actual2, SingletonParent, `actual2`);
        assert.instanceOf(actual2.dep, Transient, `actual2.dep`);

        assert.strictEqual(actual1, actual2, `actual1`);

        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          get.calls,
          [
            [ISingletonParent],
            [ITransient],
            [ISingletonParent],
          ],
          `get.calls`,
        );
      });

      it(`singleton registration is reused by the singleton parent`, function () {
        @inject(ISingleton)
        class SingletonParent implements ISingletonParent { public constructor(public dep: ISingleton) { } }
        register(SingletonParent);

        const actual1 = container.get(ISingletonParent);
        assert.instanceOf(actual1, SingletonParent, `actual1`);
        assert.instanceOf(actual1.dep, Singleton, `actual1.dep`);

        const actual2 = container.get(ISingletonParent);
        assert.instanceOf(actual2, SingletonParent, `actual2`);
        assert.instanceOf(actual2.dep, Singleton, `actual2.dep`);

        assert.strictEqual(actual1, actual2, `actual1`);

        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          get.calls,
          [
            [ISingletonParent],
            [ISingleton],
            [ISingletonParent],
          ],
          `get.calls`,
        );
      });

      it(`instance registration is reused by the singleton parent`, function () {
        @inject(IInstance)
        class SingletonParent implements ISingletonParent { public constructor(public dep: IInstance) { } }
        register(SingletonParent);

        const actual1 = container.get(ISingletonParent);
        assert.instanceOf(actual1, SingletonParent, `actual1`);
        assert.instanceOf(actual1.dep, Instance, `actual1.dep`);

        const actual2 = container.get(ISingletonParent);
        assert.instanceOf(actual2, SingletonParent, `actual2`);
        assert.instanceOf(actual2.dep, Instance, `actual2.dep`);

        assert.strictEqual(actual1, actual2, `actual1`);

        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          get.calls,
          [
            [ISingletonParent],
            [IInstance],
            [ISingletonParent],
          ],
          `get.calls`,
        );
      });

      it(`callback registration is reused by the singleton parent`, function () {
        @inject(ICallback)
        class SingletonParent implements ISingletonParent { public constructor(public dep: ICallback) { } }
        register(SingletonParent);

        const actual1 = container.get(ISingletonParent);
        assert.instanceOf(actual1, SingletonParent, `actual1`);
        assert.instanceOf(actual1.dep, Callback, `actual1.dep`);

        const actual2 = container.get(ISingletonParent);
        assert.instanceOf(actual2, SingletonParent, `actual2`);
        assert.instanceOf(actual2.dep, Callback, `actual2.dep`);

        assert.strictEqual(actual1, actual2, `actual1`);
        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          callback.calls,
          [
            [container, container, container.getResolver(ICallback)],
          ],
          `callback.calls`,
        );

        assert.deepStrictEqual(
          get.calls,
          [
            [ISingletonParent],
            [ICallback],
            [ISingletonParent],
          ],
          `get.calls`,
        );
      });
    });

    describe('instance parent', function () {
      interface IInstanceParent { dep: any }
      let IInstanceParent: InterfaceSymbol<IInstanceParent>;
      let instanceParent: IInstanceParent;

      function register(cls: any) {
        instanceParent = container.get(cls);
        get.reset();
        IInstanceParent = DI.createInterface<IInstanceParent>('IInstanceParent', x => x.instance(instanceParent));
      }

      it(`transient registration is reused by the instance parent`, function () {
        @inject(ITransient)
        class InstanceParent implements IInstanceParent { public constructor(public dep: ITransient) { } }
        register(InstanceParent);

        const actual1 = container.get(IInstanceParent);
        assert.instanceOf(actual1, InstanceParent, `actual1`);
        assert.instanceOf(actual1.dep, Transient, `actual1.dep`);

        const actual2 = container.get(IInstanceParent);
        assert.instanceOf(actual2, InstanceParent, `actual2`);
        assert.instanceOf(actual2.dep, Transient, `actual2.dep`);

        assert.strictEqual(actual1, actual2, `actual1`);

        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          get.calls,
          [
            [IInstanceParent],
            [IInstanceParent],
          ],
          `get.calls`,
        );
      });

      it(`singleton registration is reused by the instance parent`, function () {
        @inject(ISingleton)
        class InstanceParent implements IInstanceParent { public constructor(public dep: ISingleton) { } }
        register(InstanceParent);

        const actual1 = container.get(IInstanceParent);
        assert.instanceOf(actual1, InstanceParent, `actual1`);
        assert.instanceOf(actual1.dep, Singleton, `actual1.dep`);

        const actual2 = container.get(IInstanceParent);
        assert.instanceOf(actual2, InstanceParent, `actual2`);
        assert.instanceOf(actual2.dep, Singleton, `actual2.dep`);

        assert.strictEqual(actual1, actual2, `actual1`);

        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          get.calls,
          [
            [IInstanceParent],
            [IInstanceParent],
          ],
          `get.calls`,
        );
      });

      it(`instance registration is reused by the instance parent`, function () {
        @inject(IInstance)
        class InstanceParent implements IInstanceParent { public constructor(public dep: IInstance) { } }
        register(InstanceParent);

        const actual1 = container.get(IInstanceParent);
        assert.instanceOf(actual1, InstanceParent, `actual1`);
        assert.instanceOf(actual1.dep, Instance, `actual1.dep`);

        const actual2 = container.get(IInstanceParent);
        assert.instanceOf(actual2, InstanceParent, `actual2`);
        assert.instanceOf(actual2.dep, Instance, `actual2.dep`);

        assert.strictEqual(actual1, actual2, `actual1`);

        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          get.calls,
          [
            [IInstanceParent],
            [IInstanceParent],
          ],
          `get.calls`,
        );
      });

      it(`callback registration is reused by the instance parent`, function () {
        @inject(ICallback)
        class InstanceParent implements IInstanceParent { public constructor(public dep: ICallback) { } }
        register(InstanceParent);

        const actual1 = container.get(IInstanceParent);
        assert.instanceOf(actual1, InstanceParent, `actual1`);
        assert.instanceOf(actual1.dep, Callback, `actual1.dep`);

        const actual2 = container.get(IInstanceParent);
        assert.instanceOf(actual2, InstanceParent, `actual2`);
        assert.instanceOf(actual2.dep, Callback, `actual2.dep`);

        assert.strictEqual(actual1, actual2, `actual1`);
        assert.strictEqual(actual1.dep, actual2.dep, `actual1.dep`);

        assert.deepStrictEqual(
          callback.calls,
          [
            [container, container, container.getResolver(ICallback)],
          ],
          `callback.calls`,
        );

        assert.deepStrictEqual(
          get.calls,
          [
            [IInstanceParent],
            [IInstanceParent],
          ],
          `get.calls`,
        );
      });
    });
  });

  describe('defer registration', function () {
    class FakeCSSService {
      public constructor(public data: any) { }
    }

    class FakeCSSHandler {
      public register(container: IContainer, data) {
        container.register(
          Registration.instance(
            FakeCSSService,
            new FakeCSSService(data)
          )
        );
      }
    }

    it(`enables the handler class to provide registrations for data`, function () {
      const container = DI.createContainer();
      const data = {};

      container.register(
        Registration.singleton('.css', FakeCSSHandler)
      );

      container.register(
        Registration.defer('.css', data)
      );

      const service = container.get(FakeCSSService);

      assert.strictEqual(service.data, data);
    });

    it(`passes the params to the container's register method if no handler is found`, function () {
      const container = DI.createContainer();
      const data = {
        wasCalled: false,
        register() {
          this.wasCalled = true;
        }
      };

      container.register(
        Registration.defer('.css', data)
      );

      assert.strictEqual(data.wasCalled, true);
    });

    [
      {
        name: 'string',
        value: 'some string value'
      },
      {
        name: 'boolean',
        value: true
      },
      {
        name: 'number',
        value: 42
      }
    ].forEach(x => {
      it(`does not pass ${x.name} params to the container's register when no handler is found`, function () {
        const container = DI.createContainer();
        container.register(
          Registration.defer('.css', x.value)
        );
      });
    });

    // TODO: fix test setup for emitDecoratorMetadata
    // it('can inject dependencies based on TS metadata', function () {
    //   const deco: ClassDecorator = function (target) { return target; };

    //   class Foo {}

    //   @deco
    //   class Bar {
    //     public constructor(
    //       public readonly foo: Foo
    //     ) {}
    //   }

    //   const bar = DI.createContainer().get(Bar);

    //   assert.instanceOf(bar.foo, Foo);
    // });
  });
});

