'use strict';

describe('injector.modules', function() {
    it('should expose the loaded module info on the instance injector', function() {
      var test1 = angular.module('test1', ['test2']).info({ version: '1.1' });
      var test2 = angular.module('test2', []).info({ version: '1.2' });
      module('test1');
      inject(['$injector', function($injector) {
        expect(Object.keys($injector.modules)).toEqual(['ng', 'ngLocale', 'ngMock', 'test1', 'test2']);
        expect($injector.modules['test1'].info()).toEqual({ version: '1.1' });
        expect($injector.modules['test2'].info()).toEqual({ version: '1.2' });
      }]);
    });

    it('should expose the loaded module info on the provider injector', function() {
      var providerInjector;
      var test1 = angular.module('test1', ['test2']).info({ version: '1.1' });
      var test2 = angular.module('test2', [])
        .info({ version: '1.2' })
        .provider('test', ['$injector', function($injector) {
          providerInjector = $injector;
          return {$get: function() {}};
        }]);
      module('test1');
      // needed to ensure that the provider blocks are executed
      inject();

      expect(Object.keys(providerInjector.modules)).toEqual(['ng', 'ngLocale', 'ngMock', 'test1', 'test2']);
      expect(providerInjector.modules['test1'].info()).toEqual({ version: '1.1' });
      expect(providerInjector.modules['test2'].info()).toEqual({ version: '1.2' });
    });
});

describe('injector', function() {
  var providers;
  var injector;
  var providerInjector;
  var controllerProvider;

  beforeEach(module(function($provide, $injector, $controllerProvider) {
    providers = function(name, factory, annotations) {
      $provide.factory(name, extend(factory, annotations || {}));
    };
    providerInjector = $injector;
    controllerProvider = $controllerProvider;
  }));
  beforeEach(inject(function($injector) {
    injector = $injector;
  }));


  it('should return same instance from calling provider', function() {
    var instance = {},
        original = instance;
    providers('instance', function() { return instance; });
    expect(injector.get('instance')).toEqual(instance);
    instance = 'deleted';
    expect(injector.get('instance')).toEqual(original);
  });


  it('should inject providers', function() {
    providers('a', function() {return 'Mi';});
    providers('b', function(mi) {return mi + 'sko';}, {$inject:['a']});
    expect(injector.get('b')).toEqual('Misko');
  });


  it('should check its modulesToLoad argument', function() {
    expect(function() { angular.injector('test'); })
        .toThrowMinErr('ng', 'areq');
  });


  it('should resolve dependency graph and instantiate all services just once', function() {
    var log = [];

    //          s1
    //        /  | \
    //       /  s2  \
    //      /  / | \ \
    //     /s3 < s4 > s5
    //    //
    //   s6

    providers('s1', function() { log.push('s1'); return {}; }, {$inject: ['s2', 's5', 's6']});
    providers('s2', function() { log.push('s2'); return {}; }, {$inject: ['s3', 's4', 's5']});
    providers('s3', function() { log.push('s3'); return {}; }, {$inject: ['s6']});
    providers('s4', function() { log.push('s4'); return {}; }, {$inject: ['s3', 's5']});
    providers('s5', function() { log.push('s5'); return {}; });
    providers('s6', function() { log.push('s6'); return {}; });

    injector.get('s1');

    expect(log).toEqual(['s6', 's3', 's5', 's4', 's2', 's1']);
  });


  it('should allow query names', function() {
    providers('abc', function() { return ''; });

    expect(injector.has('abc')).toBe(true);
    expect(injector.has('xyz')).toBe(false);
    expect(injector.has('$injector')).toBe(true);
  });


  it('should provide useful message if no provider', function() {
    expect(function() {
      injector.get('idontexist');
    }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist');
  });


  it('should provide the caller name if given', function() {
    expect(function() {
      injector.get('idontexist', 'callerName');
    }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist <- callerName');
  });


  it('should provide the caller name for controllers', function() {
    controllerProvider.register('myCtrl', function(idontexist) {});
    var $controller = injector.get('$controller');
    expect(function() {
      $controller('myCtrl', {$scope: {}});
    }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist <- myCtrl');
  });


  it('should not corrupt the cache when an object fails to get instantiated', function() {
    expect(function() {
      injector.get('idontexist');
    }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist');

    expect(function() {
      injector.get('idontexist');
    }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist');
  });


  it('should provide path to the missing provider', function() {
    providers('a', function(idontexist) {return 1;});
    providers('b', function(a) {return 2;});
    expect(function() {
      injector.get('b');
    }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: idontexistProvider <- idontexist <- a <- b');
  });


  it('should create a new $injector for the run phase', inject(function($injector) {
    expect($injector).not.toBe(providerInjector);
  }));


  describe('loadNewModules', function() {
    it('should be defined on $injector', function() {
      var injector = createInjector([]);
      expect(injector.loadNewModules).toEqual(jasmine.any(Function));
    });

    it('should allow new modules to be added after injector creation', function() {
      angular.module('initial', []);
      var injector = createInjector(['initial']);
      expect(injector.modules['initial']).toBeDefined();
      expect(injector.modules['lazy']).toBeUndefined();
      angular.module('lazy', []);
      injector.loadNewModules(['lazy']);
      expect(injector.modules['lazy']).toBeDefined();
    });

    it('should execute runBlocks of new modules', function() {
      var log = [];
      angular.module('initial', []).run(function() { log.push('initial'); });
      var injector = createInjector(['initial']);
      log.push('created');

      angular.module('a', []).run(function() { log.push('a'); });
      injector.loadNewModules(['a']);
      expect(log).toEqual(['initial', 'created', 'a']);
    });

    it('should execute configBlocks of new modules', function() {
      var log = [];
      angular.module('initial', []).config(function() { log.push('initial'); });
      var injector = createInjector(['initial']);
      log.push('created');

      angular.module('a', [], function() { log.push('config1'); }).config(function() { log.push('config2'); });
      injector.loadNewModules(['a']);
      expect(log).toEqual(['initial', 'created', 'config1', 'config2']);
    });

    it('should execute runBlocks and configBlocks in the correct order', function() {
      var log = [];
      angular.module('initial', [], function() { log.push(1); })
        .config(function() { log.push(2); })
        .run(function() { log.push(3); });
      var injector = createInjector(['initial']);
      log.push('created');

      angular.module('a', [], function() { log.push(4); })
        .config(function() { log.push(5); })
        .run(function() { log.push(6); });
      injector.loadNewModules(['a']);
      expect(log).toEqual([1, 2, 3, 'created', 4, 5, 6]);
    });

    it('should load dependent modules', function() {
      angular.module('initial', []);
      var injector = createInjector(['initial']);
      expect(injector.modules['initial']).toBeDefined();
      expect(injector.modules['lazy1']).toBeUndefined();
      expect(injector.modules['lazy2']).toBeUndefined();
      angular.module('lazy1', ['lazy2']);
      angular.module('lazy2', []);
      injector.loadNewModules(['lazy1']);
      expect(injector.modules['lazy1']).toBeDefined();
      expect(injector.modules['lazy2']).toBeDefined();
    });

    it('should execute blocks of new modules in the correct order', function() {
      var log = [];
      angular.module('initial', []);
      var injector = createInjector(['initial']);

      angular.module('lazy1', ['lazy2'], function() { log.push('lazy1-1'); })
        .config(function() { log.push('lazy1-2'); })
        .run(function() { log.push('lazy1-3'); });
      angular.module('lazy2', [], function() { log.push('lazy2-1'); })
        .config(function() { log.push('lazy2-2'); })
        .run(function() { log.push('lazy2-3'); });

      injector.loadNewModules(['lazy1']);
      expect(log).toEqual(['lazy2-1', 'lazy2-2', 'lazy1-1', 'lazy1-2', 'lazy2-3', 'lazy1-3']);
    });

    it('should not reload a module that is already loaded', function() {
      var log = [];
      angular.module('initial', []).run(function() { log.push('initial'); });
      var injector = createInjector(['initial']);
      expect(log).toEqual(['initial']);

      injector.loadNewModules(['initial']);
      expect(log).toEqual(['initial']);

      angular.module('a', []).run(function() { log.push('a'); });
      injector.loadNewModules(['a']);
      expect(log).toEqual(['initial', 'a']);
      injector.loadNewModules(['a']);
      expect(log).toEqual(['initial', 'a']);

      angular.module('b', ['a']).run(function() { log.push('b'); });
      angular.module('c', []).run(function() { log.push('c'); });
      angular.module('d', ['b', 'c']).run(function() { log.push('d'); });
      injector.loadNewModules(['d']);
      expect(log).toEqual(['initial', 'a', 'b', 'c', 'd']);
    });

    it('should be able to register a service from a new module', function() {
      var injector = createInjector([]);
      angular.module('a', []).factory('aService', function() {
        return {sayHello: function() { return 'Hello'; }};
      });
      injector.loadNewModules(['a']);
      injector.invoke(function(aService) {
        expect(aService.sayHello()).toEqual('Hello');
      });
    });


    it('should be able to register a controller from a new module', function() {
      var injector = createInjector(['ng']);
      angular.module('a', []).controller('aController', function($scope) {
        $scope.test = 'b';
      });
      injector.loadNewModules(['a']);
      injector.invoke(function($controller) {
        var scope = {};
        $controller('aController', {$scope: scope});
        expect(scope.test).toEqual('b');
      });
    });


    it('should be able to register a filter from a new module', function() {
      var injector = createInjector(['ng']);
      angular.module('a', []).filter('aFilter', function() {
        return function(input) { return input + ' filtered'; };
      });
      injector.loadNewModules(['a']);
      injector.invoke(function(aFilterFilter) {
        expect(aFilterFilter('test')).toEqual('test filtered');
      });
    });


    it('should be able to register a directive from a new module', function() {
      var injector = createInjector(['ng']);
      angular.module('a', []).directive('aDirective', function() {
        return {template: 'test directive'};
      });
      injector.loadNewModules(['a']);
      injector.invoke(function($compile, $rootScope) {
        var elem = $compile('<div a-directive></div>')($rootScope);  // compile and link
        $rootScope.$digest();
        expect(elem.text()).toEqual('test directive');
        elem.remove();
      });
    });
  });

  it('should have a false strictDi property', inject(function($injector) {
    expect($injector.strictDi).toBe(false);
  }));


  describe('invoke', function() {
    var args;

    beforeEach(function() {
      args = null;
      providers('a', function() {return 1;});
      providers('b', function() {return 2;});
    });


    function Fn(a, b, c, d) {
      args = [this, a, b, c, d];
      return a + b + c + d;
    }


    it('should call function', function() {
      Fn.$inject = ['a', 'b', 'c', 'd'];
      injector.invoke(Fn, {name:'this'},  {c:3, d:4});
      expect(args).toEqual([{name:'this'}, 1, 2, 3, 4]);
    });


    it('should treat array as annotations', function() {
      injector.invoke(['a', 'b', 'c', 'd', Fn], {name:'this'}, {c:3, d:4});
      expect(args).toEqual([{name:'this'}, 1, 2, 3, 4]);
    });


    it('should invoke the passed-in fn with all of the dependencies as arguments', function() {
      providers('c', function() {return 3;});
      providers('d', function() {return 4;});
      expect(injector.invoke(['a', 'b', 'c', 'd', Fn])).toEqual(10);
    });


    it('should fail with errors if not function or array', function() {
      expect(function() {
        injector.invoke({});
      }).toThrowMinErr('ng', 'areq', 'Argument \'fn\' is not a function, got Object');
      expect(function() {
        injector.invoke(['a', 123], {});
      }).toThrowMinErr('ng', 'areq', 'Argument \'fn\' is not a function, got number');
    });
  });


  describe('annotation', function() {
    /* global annotate: false */
    it('should return $inject', function() {
      function fn() {}
      fn.$inject = ['a'];
      expect(annotate(fn)).toBe(fn.$inject);
      expect(annotate(function() {})).toEqual([]);
      expect(annotate(function() {})).toEqual([]);
      /* eslint-disable space-before-function-paren, no-multi-spaces */
      expect(annotate(function  () {})).toEqual([]);
      expect(annotate(function /* */ () {})).toEqual([]);
      /* eslint-enable */
    });


    it('should create $inject', function() {
      var extraParams = angular.noop;
      /* eslint-disable space-before-function-paren */
      // keep the multi-line to make sure we can handle it
      function $f_n0 /*
          */(
          $a, // x, <-- looks like an arg but it is a comment
          b_, /* z, <-- looks like an arg but it is a
                 multi-line comment
                 function(a, b) {}
                 */
          _c,
          /* {some type} */ d) { extraParams(); }
      /* eslint-enable */
      expect(annotate($f_n0)).toEqual(['$a', 'b_', '_c',  'd']);
      expect($f_n0.$inject).toEqual(['$a', 'b_', '_c',  'd']);
    });


    it('should strip leading and trailing underscores from arg name during inference', function() {
      function beforeEachFn(_foo_) { /* foo = _foo_ */ }
      expect(annotate(beforeEachFn)).toEqual(['foo']);
    });

    it('should not strip service names with a single underscore', function() {
      function beforeEachFn(_) { /* _ = _ */ }
      expect(annotate(beforeEachFn)).toEqual(['_']);
    });

    it('should handle no arg functions', function() {
      function $f_n0() {}
      expect(annotate($f_n0)).toEqual([]);
      expect($f_n0.$inject).toEqual([]);
    });


    it('should handle no arg functions with spaces in the arguments list', function() {
      function fn() {}
      expect(annotate(fn)).toEqual([]);
      expect(fn.$inject).toEqual([]);
    });


    it('should handle args with both $ and _', function() {
      function $f_n0($a_) {}
      expect(annotate($f_n0)).toEqual(['$a_']);
      expect($f_n0.$inject).toEqual(['$a_']);
    });

    it('should handle functions with overridden toString', function() {
      function fn(a) {}
      fn.toString = function() { return 'fn'; };
      expect(annotate(fn)).toEqual(['a']);
      expect(fn.$inject).toEqual(['a']);
    });

    it('should throw on non function arg', function() {
      expect(function() {
        annotate({});
      }).toThrow();
    });


    describe('es6', function() {
      if (support.shorthandMethods) {
        // The functions are generated using `eval` as just having the ES6 syntax can break some browsers.
        it('should be possible to annotate shorthand methods', function() {
          // eslint-disable-next-line no-eval
          expect(annotate(eval('({ fn(x) { return; } })').fn)).toEqual(['x']);
        });
      }


      if (support.fatArrows) {
        it('should create $inject for arrow functions', function() {
          // eslint-disable-next-line no-eval
          expect(annotate(eval('(a, b) => a'))).toEqual(['a', 'b']);
        });
      }


      if (support.fatArrows) {
        it('should create $inject for arrow functions with no parenthesis', function() {
          // eslint-disable-next-line no-eval
          expect(annotate(eval('a => a'))).toEqual(['a']);
        });
      }


      if (support.fatArrows) {
        it('should take args before first arrow', function() {
          // eslint-disable-next-line no-eval
          expect(annotate(eval('a => b => b'))).toEqual(['a']);
        });
      }

      if (support.classes) {
        it('should be possible to instantiate ES6 classes', function() {
          providers('a', function() { return 'a-value'; });
          // eslint-disable-next-line no-eval
          var Clazz = eval('(class { constructor(a) { this.a = a; } aVal() { return this.a; } })');
          var instance = injector.instantiate(Clazz);
          expect(instance).toEqual(new Clazz('a-value'));
          expect(instance.aVal()).toEqual('a-value');
        });

        they('should detect ES6 classes regardless of whitespace/comments ($prop)', [
          'class Test {}',
          'class Test{}',
          'class //<--ES6 stuff\nTest {}',
          'class//<--ES6 stuff\nTest {}',
          'class {}',
          'class{}',
          'class //<--ES6 stuff\n {}',
          'class//<--ES6 stuff\n {}',
          'class/* Test */{}',
          'class /* Test */ {}'
        ], function(classDefinition) {
          // eslint-disable-next-line no-eval
          var Clazz = eval('(' + classDefinition + ')');
          var instance = injector.invoke(Clazz);

          expect(instance).toEqual(jasmine.any(Clazz));
        });
      }
    });


    it('should publish annotate API', function() {
      expect(angular.mock.$$annotate).toBe(annotate);
      spyOn(angular.mock, '$$annotate').and.callThrough();
      function fn() {}
      injector.annotate(fn);
      expect(angular.mock.$$annotate).toHaveBeenCalledWith(fn);
    });
  });


  it('should have $injector', function() {
    var $injector = createInjector();
    expect($injector.get('$injector')).toBe($injector);
  });


  it('should define module', function() {
    var log = '';
    var injector = createInjector([function($provide) {
      $provide.value('value', 'value;');
      $provide.factory('fn', valueFn('function;'));
      $provide.provider('service', function Provider() {
        this.$get = valueFn('service;');
      });
    }, function(valueProvider, fnProvider, serviceProvider) {
      log += valueProvider.$get() + fnProvider.$get() + serviceProvider.$get();
    }]).invoke(function(value, fn, service) {
      log += '->' + value + fn + service;
    });
    expect(log).toEqual('value;function;service;->value;function;service;');
  });


  describe('module', function() {
    it('should provide $injector even when no module is requested', function() {
      var $provide,
        $injector = createInjector([
          angular.extend(function(p) { $provide = p; }, {$inject: ['$provide']})
        ]);
      expect($injector.get('$injector')).toBe($injector);
    });


    it('should load multiple function modules and infer inject them', function() {
      var a = 'junk';
      var $injector = createInjector([
        function() {
          a = 'A'; // reset to prove we ran
        },
        function($provide) {
          $provide.value('a', a);
        },
        angular.extend(function(p, serviceA) {
          p.value('b', serviceA.$get() + 'B');
        }, {$inject:['$provide', 'aProvider']}),
        ['$provide', 'bProvider', function(p, serviceB) {
          p.value('c', serviceB.$get() + 'C');
        }]
      ]);
      expect($injector.get('a')).toEqual('A');
      expect($injector.get('b')).toEqual('AB');
      expect($injector.get('c')).toEqual('ABC');
    });


    it('should run symbolic modules', function() {
      angularModule('myModule', []).value('a', 'abc');
      var $injector = createInjector(['myModule']);
      expect($injector.get('a')).toEqual('abc');
    });


    it('should error on invalid module name', function() {
      expect(function() {
        createInjector(['IDontExist'], {});
      }).toThrowMinErr('$injector', 'modulerr',
        /\[\$injector:nomod] Module 'IDontExist' is not available! You either misspelled the module name or forgot to load it/);
    });


    it('should load dependant modules only once', function() {
      var log = '';
      angular.module('a', [], function() { log += 'a'; });
      angular.module('b', ['a'], function() { log += 'b'; });
      angular.module('c', ['a', 'b'], function() { log += 'c'; });
      createInjector(['c', 'c']);
      expect(log).toEqual('abc');
    });

    it('should load different instances of dependent functions', function() {
      function  generateValueModule(name, value) {
        return function($provide) {
          $provide.value(name, value);
        };
      }
      var injector = createInjector([generateValueModule('name1', 'value1'),
                                     generateValueModule('name2', 'value2')]);
      expect(injector.get('name2')).toBe('value2');
    });

    it('should load same instance of dependent function only once', function() {
      var count = 0;
      function valueModule($provide) {
        count++;
        $provide.value('name', 'value');
      }

      var injector = createInjector([valueModule, valueModule]);
      expect(injector.get('name')).toBe('value');
      expect(count).toBe(1);
    });

    it('should execute runBlocks after injector creation', function() {
      var log = '';
      angular.module('a', [], function() { log += 'a'; }).run(function() { log += 'A'; });
      angular.module('b', ['a'], function() { log += 'b'; }).run(function() { log += 'B'; });
      createInjector([
        'b',
        valueFn(function() { log += 'C'; }),
        [valueFn(function() { log += 'D'; })]
      ]);
      expect(log).toEqual('abABCD');
    });

    it('should execute own config blocks after all own providers are invoked', function() {
      var log = '';
      angular.module('a', ['b'])
      .config(function($aProvider) {
        log += 'aConfig;';
      })
      .provider('$a', function Provider$a() {
        log += '$aProvider;';
        this.$get = function() {};
      });
      angular.module('b', [])
      .config(function($bProvider) {
        log += 'bConfig;';
      })
      .provider('$b', function Provider$b() {
        log += '$bProvider;';
        this.$get = function() {};
      });

      createInjector(['a']);
      expect(log).toBe('$bProvider;bConfig;$aProvider;aConfig;');
    });

    describe('$provide', function() {

      it('should throw an exception if we try to register a service called "hasOwnProperty"', function() {
        createInjector([function($provide) {
          expect(function() {
            $provide.provider('hasOwnProperty', function() {  });
          }).toThrowMinErr('ng', 'badname');
        }]);
      });

      it('should throw an exception if we try to register a constant called "hasOwnProperty"', function() {
        createInjector([function($provide) {
          expect(function() {
            $provide.constant('hasOwnProperty', {});
          }).toThrowMinErr('ng', 'badname');
        }]);
      });


      describe('constant', function() {
        it('should create configuration injectable constants', function() {
          var log = [];
          createInjector([
            function($provide) {
              $provide.constant('abc', 123);
              $provide.constant({a: 'A', b:'B'});
              return function(a) {
                log.push(a);
              };
            },
            function(abc) {
              log.push(abc);
              return function(b) {
                log.push(b);
              };
            }
          ]).get('abc');
          expect(log).toEqual([123, 'A', 'B']);
        });
      });


      describe('value', function() {
        it('should configure $provide values', function() {
          expect(createInjector([function($provide) {
            $provide.value('value', 'abc');
          }]).get('value')).toEqual('abc');
        });


        it('should configure a set of values', function() {
          expect(createInjector([function($provide) {
            $provide.value({value: Array});
          }]).get('value')).toEqual(Array);
        });
      });


      describe('factory', function() {
        it('should configure $provide factory function', function() {
          expect(createInjector([function($provide) {
            $provide.factory('value', valueFn('abc'));
          }]).get('value')).toEqual('abc');
        });


        it('should configure a set of factories', function() {
          expect(createInjector([function($provide) {
            $provide.factory({value: Array});
          }]).get('value')).toEqual([]);
        });
      });


      describe('service', function() {
        it('should register a class', function() {
          function Type(value) {
            this.value = value;
          }

          var instance = createInjector([function($provide) {
            $provide.value('value', 123);
            $provide.service('foo', Type);
          }]).get('foo');

          expect(instance instanceof Type).toBe(true);
          expect(instance.value).toBe(123);
        });


        it('should register a set of classes', function() {
          var Type = function() {};

          var injector = createInjector([function($provide) {
            $provide.service({
              foo: Type,
              bar: Type
            });
          }]);

          expect(injector.get('foo') instanceof Type).toBe(true);
          expect(injector.get('bar') instanceof Type).toBe(true);
        });
      });


      describe('provider', function() {
        it('should configure $provide provider object', function() {
          expect(createInjector([function($provide) {
            $provide.provider('value', {
              $get: valueFn('abc')
            });
          }]).get('value')).toEqual('abc');
        });


        it('should configure $provide provider type', function() {
          function Type() {}
          Type.prototype.$get = function() {
            expect(this instanceof Type).toBe(true);
            return 'abc';
          };
          expect(createInjector([function($provide) {
            $provide.provider('value', Type);
          }]).get('value')).toEqual('abc');
        });


        it('should configure $provide using an array', function() {
          function Type(PREFIX) {
            this.prefix = PREFIX;
          }
          Type.prototype.$get = function() {
            return this.prefix + 'def';
          };
          expect(createInjector([function($provide) {
            $provide.constant('PREFIX', 'abc');
            $provide.provider('value', ['PREFIX', Type]);
          }]).get('value')).toEqual('abcdef');
        });


        it('should configure a set of providers', function() {
          expect(createInjector([function($provide) {
            $provide.provider({value: valueFn({$get:Array})});
          }]).get('value')).toEqual([]);
        });
      });


      describe('decorator', function() {
        var log, injector;

        beforeEach(function() {
          log = [];
        });


        it('should be called with the original instance', function() {
          injector = createInjector([function($provide) {
            $provide.value('myService', function(val) {
              log.push('myService:' + val);
              return 'origReturn';
            });

            $provide.decorator('myService', function($delegate) {
              return function(val) {
                log.push('myDecoratedService:' + val);
                var origVal = $delegate('decInput');
                return 'dec+' + origVal;
              };
            });
          }]);

          var out = injector.get('myService')('input');
          log.push(out);
          expect(log.join('; ')).
            toBe('myDecoratedService:input; myService:decInput; dec+origReturn');
        });


        it('should allow multiple decorators to be applied to a service', function() {
          injector = createInjector([function($provide) {
            $provide.value('myService', function(val) {
              log.push('myService:' + val);
              return 'origReturn';
            });

            $provide.decorator('myService', function($delegate) {
              return function(val) {
                log.push('myDecoratedService1:' + val);
                var origVal = $delegate('decInput1');
                return 'dec1+' + origVal;
              };
            });

            $provide.decorator('myService', function($delegate) {
              return function(val) {
                log.push('myDecoratedService2:' + val);
                var origVal = $delegate('decInput2');
                return 'dec2+' + origVal;
              };
            });
          }]);

          var out = injector.get('myService')('input');
          log.push(out);
          expect(log).toEqual(['myDecoratedService2:input',
                               'myDecoratedService1:decInput2',
                               'myService:decInput1',
                               'dec2+dec1+origReturn']);
        });


        it('should decorate services with dependencies', function() {
          injector = createInjector([function($provide) {
            $provide.value('dep1', 'dependency1');

            $provide.factory('myService', ['dep1', function(dep1) {
              return function(val) {
                log.push('myService:' + val + ',' + dep1);
                return 'origReturn';
              };
            }]);

            $provide.decorator('myService', function($delegate) {
              return function(val) {
                log.push('myDecoratedService:' + val);
                var origVal = $delegate('decInput');
                return 'dec+' + origVal;
              };
            });
          }]);

          var out = injector.get('myService')('input');
          log.push(out);
          expect(log.join('; ')).
            toBe('myDecoratedService:input; myService:decInput,dependency1; dec+origReturn');
        });


        it('should allow for decorators to be injectable', function() {
          injector = createInjector([function($provide) {
            $provide.value('dep1', 'dependency1');

            $provide.factory('myService', function() {
              return function(val) {
                log.push('myService:' + val);
                return 'origReturn';
              };
            });

            $provide.decorator('myService', function($delegate, dep1) {
              return function(val) {
                log.push('myDecoratedService:' + val + ',' + dep1);
                var origVal = $delegate('decInput');
                return 'dec+' + origVal;
              };
            });
          }]);

          var out = injector.get('myService')('input');
          log.push(out);
          expect(log.join('; ')).
            toBe('myDecoratedService:input,dependency1; myService:decInput; dec+origReturn');
        });


        it('should allow for decorators to $injector', function() {
          injector = createInjector(['ng', function($provide) {
            $provide.decorator('$injector', function($delegate) {
              return extend({}, $delegate, {get: function(val) {
                if (val === 'key') {
                  return 'value';
                }
                return $delegate.get(val);
              }});
            });
          }]);

          expect(injector.get('key')).toBe('value');
          expect(injector.get('$http')).not.toBeUndefined();
        });
      });
    });


    describe('error handling', function() {
      it('should handle wrong argument type', function() {
        expect(function() {
          createInjector([
            {}
          ], {});
        }).toThrowMinErr('$injector', 'modulerr', /Failed to instantiate module \{\} due to:\n.*\[ng:areq] Argument 'module' is not a function, got Object/);
      });


      it('should handle exceptions', function() {
        expect(function() {
          createInjector([function() {
            throw new Error('MyError');
          }], {});
        }).toThrowMinErr('$injector', 'modulerr', /Failed to instantiate module .+ due to:\n.*MyError/);
      });


      it('should decorate the missing service error with module name', function() {
        angular.module('TestModule', [], function(xyzzy) {});
        expect(function() {
          createInjector(['TestModule']);
        }).toThrowMinErr(
          '$injector', 'modulerr', /Failed to instantiate module TestModule due to:\n.*\[\$injector:unpr] Unknown provider: xyzzy/
        );
      });


      it('should decorate the missing service error with module function', function() {
        function myModule(xyzzy) {}
        expect(function() {
          createInjector([myModule]);
        }).toThrowMinErr(
          '$injector', 'modulerr', /Failed to instantiate module function myModule\(xyzzy\) due to:\n.*\[\$injector:unpr] Unknown provider: xyzzy/
        );
      });


      it('should decorate the missing service error with module array function', function() {
        function myModule(xyzzy) {}
        expect(function() {
          createInjector([['xyzzy', myModule]]);
        }).toThrowMinErr(
          '$injector', 'modulerr', /Failed to instantiate module function myModule\(xyzzy\) due to:\n.*\[\$injector:unpr] Unknown provider: xyzzy/
        );
      });


      it('should throw error when trying to inject oneself', function() {
        expect(function() {
          createInjector([function($provide) {
            $provide.factory('service', function(service) {});
            return function(service) {};
          }]);
        }).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: service <- service');
      });


      it('should throw error when trying to inject circular dependency', function() {
        expect(function() {
          createInjector([function($provide) {
            $provide.factory('a', function(b) {});
            $provide.factory('b', function(a) {});
            return function(a) {};
          }]);
        }).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: a <- b <- a');
      });

    });
  });


  describe('retrieval', function() {
    var instance = {name:'angular'};
    function Instance() { this.name = 'angular'; }

    function createInjectorWithValue(instanceName, instance) {
      return createInjector([['$provide', function(provide) {
        provide.value(instanceName, instance);
      }]]);
    }
    function createInjectorWithFactory(serviceName, serviceDef) {
      return createInjector([['$provide', function(provide) {
        provide.factory(serviceName, serviceDef);
      }]]);
    }


    it('should retrieve by name', function() {
      var $injector = createInjectorWithValue('instance', instance);
      var retrievedInstance = $injector.get('instance');
      expect(retrievedInstance).toBe(instance);
    });


    it('should cache instance', function() {
      var $injector = createInjectorWithFactory('instance', function() { return new Instance(); });
      var instance = $injector.get('instance');
      expect($injector.get('instance')).toBe(instance);
      expect($injector.get('instance')).toBe(instance);
    });


    it('should call functions and infer arguments', function() {
      var $injector = createInjectorWithValue('instance', instance);
      expect($injector.invoke(function(instance) { return instance; })).toBe(instance);
    });

  });


  describe('method invoking', function() {
    var $injector;

    beforeEach(function() {
      $injector = createInjector([function($provide) {
        $provide.value('book', 'moby');
        $provide.value('author', 'melville');
      }]);
    });


    it('should invoke method', function() {
      expect($injector.invoke(function(book, author) {
        return author + ':' + book;
      })).toEqual('melville:moby');
      expect($injector.invoke(function(book, author) {
        expect(this).toEqual($injector);
        return author + ':' + book;
      }, $injector)).toEqual('melville:moby');
    });


    it('should invoke method with locals', function() {
      expect($injector.invoke(function(book, author) {
        return author + ':' + book;
      })).toEqual('melville:moby');
      expect($injector.invoke(
        function(book, author, chapter) {
          expect(this).toEqual($injector);
          return author + ':' + book + '-' + chapter;
        }, $injector, {author:'m', chapter:'ch1'})).toEqual('m:moby-ch1');
    });


    it('should invoke method which is annotated', function() {
      expect($injector.invoke(extend(function(b, a) {
        return a + ':' + b;
      }, {$inject:['book', 'author']}))).toEqual('melville:moby');
      expect($injector.invoke(extend(function(b, a) {
        expect(this).toEqual($injector);
        return a + ':' + b;
      }, {$inject:['book', 'author']}), $injector)).toEqual('melville:moby');
    });


    it('should invoke method which is an array of annotation', function() {
      expect($injector.invoke(function(book, author) {
        return author + ':' + book;
      })).toEqual('melville:moby');
      expect($injector.invoke(function(book, author) {
        expect(this).toEqual($injector);
        return author + ':' + book;
      }, $injector)).toEqual('melville:moby');
    });


    it('should throw useful error on wrong argument type]', function() {
      expect(function() {
        $injector.invoke({});
      }).toThrowMinErr('ng', 'areq', 'Argument \'fn\' is not a function, got Object');
    });
  });


  describe('service instantiation', function() {
    var $injector;

    beforeEach(function() {
      $injector = createInjector([function($provide) {
        $provide.value('book', 'moby');
        $provide.value('author', 'melville');
      }]);
    });


    function Type(book, author) {
      this.book = book;
      this.author = author;
    }
    Type.prototype.title = function() {
      return this.author + ': ' + this.book;
    };


    it('should instantiate object and preserve constructor property and be instanceof', function() {
      var t = $injector.instantiate(Type);
      expect(t.book).toEqual('moby');
      expect(t.author).toEqual('melville');
      expect(t.title()).toEqual('melville: moby');
      expect(t instanceof Type).toBe(true);
    });


    it('should instantiate object and preserve constructor property and be instanceof ' +
        'with the array annotated type', function() {
      var t = $injector.instantiate(['book', 'author', Type]);
      expect(t.book).toEqual('moby');
      expect(t.author).toEqual('melville');
      expect(t.title()).toEqual('melville: moby');
      expect(t instanceof Type).toBe(true);
    });


    it('should allow constructor to return different object', function() {
      var obj = {};
      var Class = function() {
        return obj;
      };

      expect($injector.instantiate(Class)).toBe(obj);
    });


    it('should allow constructor to return a function', function() {
      var fn = function() {};
      var Class = function() {
        return fn;
      };

      expect($injector.instantiate(Class)).toBe(fn);
    });


    it('should handle constructor exception', function() {
      expect(function() {
        $injector.instantiate(function() { throw 'MyError'; });
      }).toThrow('MyError');
    });


    it('should return instance if constructor returns non-object value', function() {
      var A = function() {
        return 10;
      };

      var B = function() {
        return 'some-string';
      };

      var C = function() {
        return undefined;
      };

      expect($injector.instantiate(A) instanceof A).toBe(true);
      expect($injector.instantiate(B) instanceof B).toBe(true);
      expect($injector.instantiate(C) instanceof C).toBe(true);
    });
  });

  describe('protection modes', function() {
    it('should prevent provider lookup in app', function() {
      var  $injector = createInjector([function($provide) {
        $provide.value('name', 'angular');
      }]);
      expect(function() {
        $injector.get('nameProvider');
      }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: nameProviderProvider <- nameProvider');
    });


    it('should prevent provider configuration in app', function() {
      var  $injector = createInjector([]);
      expect(function() {
        $injector.get('$provide').value('a', 'b');
      }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: $provideProvider <- $provide');
    });


    it('should prevent instance lookup in module', function() {
      function instanceLookupInModule(name) { throw new Error('FAIL'); }
      expect(function() {
        createInjector([function($provide) {
          $provide.value('name', 'angular');
        }, instanceLookupInModule]);
      }).toThrowMinErr('$injector', 'modulerr', '[$injector:unpr] Unknown provider: name');
    });
  });
});

describe('strict-di injector', function() {
  beforeEach(inject.strictDi(true));

  describe('with ngMock', function() {
    it('should not throw when calling mock.module() with "magic" annotations', function() {
      expect(function() {
        module(function($provide, $httpProvider, $compileProvider) {
          // Don't throw!
        });
      }).not.toThrow();
    });


    it('should not throw when calling mock.inject() with "magic" annotations', function() {
      expect(function() {
        inject(function($rootScope, $compile, $http) {
          // Don't throw!
        });
      }).not.toThrow();
    });
  });


  it('should throw if magic annotation is used by service', function() {
    module(function($provide) {
      $provide.service({
        '$test': function() { return this; },
        '$test2': function($test) { return this; }
      });
    });
    inject(function($injector) {
      expect(function() {
        $injector.invoke(function($test2) {});
      }).toThrowMinErr('$injector', 'strictdi');
    });
  });


  it('should throw if magic annotation is used by provider', function() {
    module(function($provide) {
      $provide.provider({
        '$test': function() { this.$get = function($rootScope) { return $rootScope; }; }
      });
    });
    inject(function($injector) {
      expect(function() {
        $injector.invoke(['$test', function($test) {}]);
      }).toThrowMinErr('$injector', 'strictdi');
    });
  });


  it('should throw if magic annotation is used by factory', function() {
    module(function($provide) {
      $provide.factory({
        '$test': function($rootScope) { return function() {}; }
      });
    });
    inject(function($injector) {
      expect(function() {
        $injector.invoke(['$test', function(test) {}]);
      }).toThrowMinErr('$injector', 'strictdi');
    });
  });


  it('should throw if factory does not return a value', function() {
    module(function($provide) {
      $provide.factory('$test', function() {});
    });
    expect(function() {
      inject(function($test) {});
    }).toThrowMinErr('$injector', 'undef');
  });


  it('should always use provider as `this` when invoking a factory', function() {
    var called = false;

    function factoryFn() {
      called = true;
      expect(typeof this.$get).toBe('function');
      return this;
    }
    module(function($provide) {
      $provide.factory('$test', factoryFn);
    });
    inject(function($test) {});
    expect(called).toBe(true);
  });

  it('should set strictDi property to true on the injector instance', inject(function($injector) {
    expect($injector.strictDi).toBe(true);
  }));
});
