/* global FormController: false */
'use strict';

describe('form', function() {
  var doc, control, scope, $compile, changeInputValue;

  beforeEach(module(function($compileProvider) {
    $compileProvider.directive('storeModelCtrl', function() {
      return {
        require: 'ngModel',
        link: function(scope, elm, attr, ctrl) {
          control = ctrl;
        }
      };
    });
  }));

  beforeEach(inject(function($injector, $sniffer) {
    $compile = $injector.get('$compile');
    scope = $injector.get('$rootScope');

    changeInputValue = function(elm, value) {
      elm.val(value);
      browserTrigger(elm, $sniffer.hasEvent('input') ? 'input' : 'change');
    };
  }));

  afterEach(function() {
    dealoc(doc);
  });


  it('should instantiate form and attach it to DOM', function() {
    doc = $compile('<form>')(scope);
    expect(doc.data('$formController')).toBeTruthy();
    expect(doc.data('$formController') instanceof FormController).toBe(true);
  });


  it('should remove form control references from the form when nested control is removed from the DOM', function() {
    doc = $compile(
      '<form name="myForm">' +
        '<input ng-if="inputPresent" name="alias" ng-model="value" store-model-ctrl/>' +
      '</form>')(scope);
    scope.inputPresent = true;
    scope.$digest();

    var form = scope.myForm;
    control.$setValidity('required', false);
    expect(form.alias).toBe(control);
    expect(form.$error.required).toEqual([control]);

    // remove nested control
    scope.inputPresent = false;
    scope.$apply();

    expect(form.$error.required).toBeFalsy();
    expect(form.alias).toBeUndefined();
  });


  it('should ignore changes in manually removed controls', function() {
    doc = $compile(
      '<form name="myForm">' +
        '<input name="control" ng-maxlength="1" ng-model="value" store-model-ctrl/>' +
      '</form>')(scope);

    var form = scope.myForm;

    var input = doc.find('input').eq(0);
    var inputController = input.controller('ngModel');

    changeInputValue(input, 'ab');
    scope.$apply();

    expect(form.$error.maxlength).toBeTruthy();
    expect(form.$dirty).toBe(true);
    expect(form.$error.maxlength[0].$name).toBe('control');

    // remove control
    form.$removeControl(form.control);
    expect(form.control).toBeUndefined();
    expect(form.$error.maxlength).toBeFalsy();

    inputController.$setPristine();
    expect(form.$dirty).toBe(true);

    form.$setPristine();

    changeInputValue(input, 'abc');
    scope.$apply();

    expect(form.$error.maxlength).toBeFalsy();
    expect(form.$dirty).toBe(false);
  });


  it('should react to validation changes in manually added controls', function() {
    doc = $compile(
      '<form name="myForm">' +
        '<input name="control" ng-maxlength="1" ng-model="value" store-model-ctrl/>' +
      '</form>')(scope);

      scope.$digest();

    var form = scope.myForm;

    var input = doc.find('input').eq(0);

    // remove control and invalidate it
    form.$removeControl(control);
    expect(form.control).toBeUndefined();

    changeInputValue(input, 'abc');
    expect(control.$error.maxlength).toBe(true);
    expect(control.$dirty).toBe(true);
    expect(form.$error.maxlength).toBeFalsy();
    expect(form.$dirty).toBe(false);

    // re-add the control; its current validation state is not propagated
    form.$addControl(control);
    expect(form.control).toBe(control);
    expect(form.$error.maxlength).toBeFalsy();
    expect(form.$dirty).toBe(false);

    // Only when the input changes again its validation state is propagated
    changeInputValue(input, 'abcd');
    expect(form.$error.maxlength[0]).toBe(control);
    expect(form.$dirty).toBe(false);
  });


  it('should use the correct parent when renaming and removing dynamically added controls', function() {
    scope.controlName = 'childControl';
    scope.hasChildControl = true;

    doc = $compile(
      '<form name="myForm">' +
        '<div ng-if="hasChildControl">' +
          '<input name="{{controlName}}" ng-maxlength="1" ng-model="value"/>' +
        '</div>' +
      '</form>' +
      '<form name="otherForm"></form>')(scope);

    scope.$digest();

    var form = scope.myForm;
    var otherForm = scope.otherForm;
    var childControl = form.childControl;

    // remove child form and add it to another form
    form.$removeControl(childControl);
    otherForm.$addControl(childControl);

    expect(form.childControl).toBeUndefined();
    expect(otherForm.childControl).toBe(childControl);

    // rename the childControl
    scope.controlName = 'childControlMoved';
    scope.$digest();

    expect(form.childControlMoved).toBeUndefined();
    expect(otherForm.childControl).toBeUndefined();
    expect(otherForm.childControlMoved).toBe(childControl);

    scope.hasChildControl = false;
    scope.$digest();

    expect(form.childControlMoved).toBeUndefined();
    expect(otherForm.childControlMoved).toBeUndefined();
  });


  it('should remove scope reference when form with no parent form is removed from the DOM', function() {
    var formController;
    scope.ctrl = {};
    doc = $compile(
      '<div><form name="ctrl.myForm" ng-if="formPresent">' +
        '<input name="alias" ng-model="value" />' +
      '</form></div>')(scope);

    scope.$digest();
    expect(scope.ctrl.myForm).toBeUndefined();

    scope.$apply('formPresent = true');
    expect(scope.ctrl.myForm).toBeDefined();

    formController = doc.find('form').controller('form');
    expect(scope.ctrl.myForm).toBe(formController);

    scope.$apply('formPresent = false');
    expect(scope.ctrl.myForm).toBeUndefined();
  });

  it('should use ngForm value as form name', function() {
    doc = $compile(
      '<div ng-form="myForm">' +
        '<input type="text" name="alias" ng-model="value"/>' +
      '</div>')(scope);

    expect(scope.myForm).toBeDefined();
    expect(scope.myForm.alias).toBeDefined();
  });

  it('should use ngForm value as form name when nested inside form', function() {
    doc = $compile(
      '<form name="myForm">' +
        '<div ng-form="nestedForm"><input type="text" name="alias" ng-model="value"/></div>' +
      '</form>')(scope);

    expect(scope.myForm).toBeDefined();
    expect(scope.myForm.nestedForm).toBeDefined();
    expect(scope.myForm.nestedForm.alias).toBeDefined();
  });


  it('should publish form to scope when name attr is defined', function() {
    doc = $compile('<form name="myForm"></form>')(scope);
    expect(scope.myForm).toBeTruthy();
    expect(doc.data('$formController')).toBeTruthy();
    expect(doc.data('$formController')).toEqual(scope.myForm);
  });


  it('should support expression in form name', function() {
    doc = $compile('<form name="obj.myForm"></form>')(scope);

    expect(scope.obj).toBeDefined();
    expect(scope.obj.myForm).toBeTruthy();
  });


  it('should support two forms on a single scope', function() {
    doc = $compile(
      '<div>' +
        '<form name="formA">' +
          '<input name="firstName" ng-model="firstName" required>' +
        '</form>' +
        '<form name="formB">' +
          '<input name="lastName" ng-model="lastName" required>' +
        '</form>' +
      '</div>'
    )(scope);

    scope.$apply();

    expect(scope.formA.$error.required.length).toBe(1);
    expect(scope.formA.$error.required).toEqual([scope.formA.firstName]);
    expect(scope.formB.$error.required.length).toBe(1);
    expect(scope.formB.$error.required).toEqual([scope.formB.lastName]);

    var inputA = doc.find('input').eq(0),
        inputB = doc.find('input').eq(1);

    changeInputValue(inputA, 'val1');
    changeInputValue(inputB, 'val2');

    expect(scope.firstName).toBe('val1');
    expect(scope.lastName).toBe('val2');

    expect(scope.formA.$error.required).toBeFalsy();
    expect(scope.formB.$error.required).toBeFalsy();
  });


  it('should publish widgets', function() {
    doc = jqLite('<form name="form"><input type="text" name="w1" ng-model="some" /></form>');
    $compile(doc)(scope);

    var widget = scope.form.w1;
    expect(widget).toBeDefined();
    expect(widget.$pristine).toBe(true);
    expect(widget.$dirty).toBe(false);
    expect(widget.$valid).toBe(true);
    expect(widget.$invalid).toBe(false);
  });


  it('should throw an exception if an input has name="hasOwnProperty"', function() {
    doc = jqLite(
      '<form name="form">' +
        '<input name="hasOwnProperty" ng-model="some" />' +
        '<input name="other" ng-model="someOther" />' +
      '</form>');
    expect(function() {
      $compile(doc)(scope);
    }).toThrowMinErr('ng', 'badname');
  });

  describe('triggering commit value on submit', function() {
    it('should trigger update on form submit', function() {
      var form = $compile(
          '<form name="test" ng-model-options="{ updateOn: \'submit\' }" >' +
            '<input type="text" ng-model="name" />' +
          '</form>')(scope);
      scope.$digest();

      var inputElm = form.find('input').eq(0);
      changeInputValue(inputElm, 'a');
      expect(scope.name).toEqual(undefined);
      browserTrigger(form, 'submit');
      expect(scope.name).toEqual('a');
      dealoc(form);
    });

    it('should trigger update on form submit with nested forms', function() {
      var form = $compile(
          '<form name="test" ng-model-options="{ updateOn: \'submit\' }" >' +
            '<div class="ng-form" name="child">' +
              '<input type="text" ng-model="name" />' +
            '</div>' +
          '</form>')(scope);
      scope.$digest();

      var inputElm = form.find('input').eq(0);
      changeInputValue(inputElm, 'a');
      expect(scope.name).toEqual(undefined);
      browserTrigger(form, 'submit');
      expect(scope.name).toEqual('a');
      dealoc(form);
    });

    it('should trigger update before ng-submit is invoked', function() {
      var form = $compile(
          '<form name="test" ng-submit="submit()" ' +
              'ng-model-options="{ updateOn: \'submit\' }" >' +
            '<input type="text" ng-model="name" />' +
          '</form>')(scope);
      scope.$digest();

      var inputElm = form.find('input').eq(0);
      changeInputValue(inputElm, 'a');
      scope.submit = jasmine.createSpy('submit').and.callFake(function() {
        expect(scope.name).toEqual('a');
      });
      browserTrigger(form, 'submit');
      expect(scope.submit).toHaveBeenCalled();
      dealoc(form);
    });
  });

  describe('rollback view value', function() {
    it('should trigger rollback on form controls', function() {
      var form = $compile(
          '<form name="test" ng-model-options="{ updateOn: \'submit\' }" >' +
            '<input type="text" ng-model="name" />' +
            '<button ng-click="test.$rollbackViewValue()" />' +
          '</form>')(scope);
      scope.$digest();

      var inputElm = form.find('input').eq(0);
      changeInputValue(inputElm, 'a');
      expect(inputElm.val()).toBe('a');
      browserTrigger(form.find('button'), 'click');
      expect(inputElm.val()).toBe('');
      dealoc(form);
    });

    it('should trigger rollback on form controls with nested forms', function() {
      var form = $compile(
          '<form name="test" ng-model-options="{ updateOn: \'submit\' }" >' +
            '<div class="ng-form" name="child">' +
              '<input type="text" ng-model="name" />' +
            '</div>' +
            '<button ng-click="test.$rollbackViewValue()" />' +
          '</form>')(scope);
      scope.$digest();

      var inputElm = form.find('input').eq(0);
      changeInputValue(inputElm, 'a');
      expect(inputElm.val()).toBe('a');
      browserTrigger(form.find('button'), 'click');
      expect(inputElm.val()).toBe('');
      dealoc(form);
    });
  });

  describe('preventing default submission', function() {

    it('should prevent form submission', function(done) {
      var job = createAsync(done);
      var nextTurn = false,
          submitted = false,
          reloadPrevented;

      doc = jqLite('<form ng-submit="submitMe()">' +
                     '<input type="submit" value="submit">' +
                   '</form>');
      // Support: Chrome 60+ (on Windows)
      // We need to add the form to the DOM in order for `submit` events to be properly fired.
      window.document.body.appendChild(doc[0]);

      var assertPreventDefaultListener = function(e) {
        reloadPrevented = e.defaultPrevented || (e.returnValue === false);
      };

      $compile(doc)(scope);

      scope.submitMe = function() {
        submitted = true;
      };

      doc[0].addEventListener('submit', assertPreventDefaultListener);

      browserTrigger(doc.find('input'));

      // let the browser process all events (and potentially reload the page)
      window.setTimeout(function() { nextTurn = true;});
      job.waitsFor(function() { return nextTurn; })
      .runs(function() {
        expect(reloadPrevented).toBe(true);
        expect(submitted).toBe(true);

        // prevent mem leak in test
        doc[0].removeEventListener('submit', assertPreventDefaultListener);
      })
      .done();
      job.start();
    });


    it('should prevent the default when the form is destroyed by a submission via a click event', function(done) {
      inject(function($timeout) {
        doc = jqLite('<div>' +
                        '<form ng-submit="submitMe()">' +
                          '<button type="submit" ng-click="destroy()"></button>' +
                        '</form>' +
                      '</div>');
        // Support: Chrome 60+ (on Windows)
        // We need to add the form to the DOM in order for `submit` events to be properly fired.
        window.document.body.appendChild(doc[0]);

        var form = doc.find('form'),
            destroyed = false,
            nextTurn = false,
            submitted = false,
            reloadPrevented = 'never called';

        scope.destroy = function() {
          // yes, I know, scope methods should not do direct DOM manipulation, but I wanted to keep
          // this test small. Imagine that the destroy action will cause a model change (e.g.
          // $location change) that will cause some directive to destroy the dom (e.g. ngView+$route)
          doc.empty();
          destroyed = true;
        };

        scope.submitMe = function() {
          submitted = true;
        };

        var assertPreventDefaultListener = function(e) {
          reloadPrevented = e.defaultPrevented || (e.returnValue === false);
        };

        $compile(doc)(scope);

        form[0].addEventListener('submit', assertPreventDefaultListener);

        browserTrigger(doc.find('button'), 'click');

        // let the browser process all events (and potentially reload the page)
        window.setTimeout(function() { nextTurn = true;}, 100);

        var job = createAsync(done);
        job.waitsFor(function() { return nextTurn; })
        .runs(function() {
          expect(doc.html()).toBe('');
          expect(destroyed).toBe(true);
          expect(submitted).toBe(false); // this is known corner-case that is not currently handled
                                         // the issue is that the submit listener is destroyed before
                                         // the event propagates there. we can fix this if we see
                                         // the issue in the wild, I'm not going to bother to do it
                                         // now. (i)

          // Support: Chrome 60+ (on Windows)
          // Chrome 60+ on Windows does not fire `submit` events when the form is not attached to
          // the DOM. Verify that the `submit` listener was either never fired or (if fired) the
          // reload was prevented.
          expect(reloadPrevented).not.toBe(false);

          // prevent mem leak in test
          form[0].removeEventListener('submit', assertPreventDefaultListener);
        })
        .done();
        job.start();
      });
    });


    it('should NOT prevent form submission if action attribute present', function() {
      var callback = jasmine.createSpy('submit').and.callFake(function(event) {
        expect(event.isDefaultPrevented()).toBe(false);
        event.preventDefault();
      });

      doc = $compile('<form action="some.py"></form>')(scope);
      doc.on('submit', callback);

      browserTrigger(doc, 'submit');
      expect(callback).toHaveBeenCalledOnce();
    });
  });


  describe('nested forms', function() {

    it('should chain nested forms', function() {
      doc = jqLite(
          '<ng:form name="parent">' +
            '<ng:form name="child">' +
              '<input ng:model="modelA" name="inputA">' +
              '<input ng:model="modelB" name="inputB">' +
            '</ng:form>' +
          '</ng:form>');
      $compile(doc)(scope);

      var parent = scope.parent,
          child = scope.child,
          inputA = child.inputA,
          inputB = child.inputB;

      inputA.$setValidity('MyError', false);
      inputB.$setValidity('MyError', false);
      expect(parent.$error.MyError).toEqual([child]);
      expect(child.$error.MyError).toEqual([inputA, inputB]);

      inputA.$setValidity('MyError', true);
      expect(parent.$error.MyError).toEqual([child]);
      expect(child.$error.MyError).toEqual([inputB]);

      inputB.$setValidity('MyError', true);
      expect(parent.$error.MyError).toBeFalsy();
      expect(child.$error.MyError).toBeFalsy();

      child.$setDirty();
      expect(parent.$dirty).toBeTruthy();

      child.$setSubmitted();
      expect(parent.$submitted).toBeTruthy();
    });

    it('should set $submitted to true on child forms when parent is submitted', function() {
      doc = jqLite(
          '<ng-form name="parent">' +
            '<ng-form name="child">' +
              '<input ng:model="modelA" name="inputA">' +
              '<input ng:model="modelB" name="inputB">' +
            '</ng-form>' +
          '</ng-form>');
      $compile(doc)(scope);

      var parent = scope.parent,
          child = scope.child;

      parent.$setSubmitted();
      expect(parent.$submitted).toBeTruthy();
      expect(child.$submitted).toBeTruthy();
    });


    it('should not propagate $submitted state on removed child forms when parent is submitted', function() {
      doc = jqLite(
          '<ng-form name="parent">' +
            '<ng-form name="child">' +
              '<ng-form name="grandchild">' +
                '<input ng:model="modelA" name="inputA">' +
              '</ng-form>' +
            '</ng-form>' +
          '</ng-form>');
      $compile(doc)(scope);

      var parent = scope.parent,
          child = scope.child,
          grandchild = scope.grandchild,
          ggchild = scope.greatgrandchild;

      parent.$removeControl(child);

      parent.$setSubmitted();
      expect(parent.$submitted).toBeTruthy();
      expect(child.$submitted).not.toBeTruthy();
      expect(grandchild.$submitted).not.toBeTruthy();

      parent.$addControl(child);

      expect(parent.$submitted).toBeTruthy();
      expect(child.$submitted).not.toBeTruthy();
      expect(grandchild.$submitted).not.toBeTruthy();

      parent.$setSubmitted();
      expect(parent.$submitted).toBeTruthy();
      expect(child.$submitted).toBeTruthy();
      expect(grandchild.$submitted).toBeTruthy();

      parent.$removeControl(child);

      expect(parent.$submitted).toBeTruthy();
      expect(child.$submitted).toBeTruthy();
      expect(grandchild.$submitted).toBeTruthy();

      parent.$setPristine(); // sets $submitted to false
      expect(parent.$submitted).not.toBeTruthy();
      expect(child.$submitted).toBeTruthy();
      expect(grandchild.$submitted).toBeTruthy();

      grandchild.$setPristine();
      expect(grandchild.$submitted).not.toBeTruthy();

      child.$setSubmitted();
      expect(parent.$submitted).not.toBeTruthy();
      expect(child.$submitted).toBeTruthy();
      expect(grandchild.$submitted).toBeTruthy();

      child.$setPristine();
      expect(parent.$submitted).not.toBeTruthy();
      expect(child.$submitted).not.toBeTruthy();
      expect(grandchild.$submitted).not.toBeTruthy();

      // Test upwards submission setting
      grandchild.$setSubmitted();
      expect(parent.$submitted).not.toBeTruthy();
      expect(child.$submitted).toBeTruthy();
      expect(grandchild.$submitted).toBeTruthy();
    });


    it('should set $submitted to true on child and parent forms when form is submitted', function() {
      doc = jqLite(
          '<ng-form name="parent">' +
            '<ng-form name="child">' +
              '<ng-form name="grandchild">' +
                '<input ng:model="modelA" name="inputA">' +
                '<input ng:model="modelB" name="inputB">' +
              '</ng-form>' +
            '</ng-form>' +
          '</ng-form>');
      $compile(doc)(scope);

      var parent = scope.parent,
          child = scope.child,
          grandchild = scope.grandchild;

      child.$setSubmitted();

      expect(parent.$submitted).toBeTruthy();
      expect(child.$submitted).toBeTruthy();
      expect(grandchild.$submitted).toBeTruthy();
    });

    it('should deregister a child form when its DOM is removed', function() {
      doc = jqLite(
        '<form name="parent">' +
          '<div class="ng-form" name="child">' +
          '<input ng:model="modelA" name="inputA" required>' +
          '</div>' +
          '</form>');
      $compile(doc)(scope);
      scope.$apply();

      var parent = scope.parent,
        child = scope.child;

      expect(parent).toBeDefined();
      expect(child).toBeDefined();
      expect(parent.$error.required).toEqual([child]);
      doc.children().remove(); //remove child

      expect(parent.child).toBeUndefined();
      expect(scope.child).toBeUndefined();
      expect(parent.$error.required).toBeFalsy();
    });


    it('should deregister a child form whose name is an expression when its DOM is removed', function() {
      doc = jqLite(
        '<form name="parent">' +
          '<div class="ng-form" name="child.form">' +
          '<input ng:model="modelA" name="inputA" required>' +
          '</div>' +
          '</form>');
      $compile(doc)(scope);
      scope.$apply();

      var parent = scope.parent,
        child = scope.child.form;

      expect(parent).toBeDefined();
      expect(child).toBeDefined();
      expect(parent.$error.required).toEqual([child]);
      doc.children().remove(); //remove child

      expect(parent.child).toBeUndefined();
      expect(scope.child.form).toBeUndefined();
      expect(parent.$error.required).toBeFalsy();
    });


    it('should deregister a input when it is removed from DOM', function() {
      doc = jqLite(
        '<form name="parent">' +
          '<div class="ng-form" name="child">' +
            '<input ng-if="inputPresent" ng-model="modelA" name="inputA" required maxlength="10">' +
          '</div>' +
        '</form>');
      $compile(doc)(scope);
      scope.inputPresent = true;
      scope.$apply();

      var parent = scope.parent,
          child = scope.child,
          input = child.inputA;

      expect(parent).toBeDefined();
      expect(child).toBeDefined();

      expect(parent.$error.required).toEqual([child]);
      expect(parent.$$success.maxlength).toEqual([child]);

      expect(child.$error.required).toEqual([input]);
      expect(child.$$success.maxlength).toEqual([input]);

      expect(doc.hasClass('ng-invalid')).toBe(true);
      expect(doc.hasClass('ng-invalid-required')).toBe(true);
      expect(doc.hasClass('ng-valid-maxlength')).toBe(true);
      expect(doc.find('div').hasClass('ng-invalid')).toBe(true);
      expect(doc.find('div').hasClass('ng-invalid-required')).toBe(true);
      expect(doc.find('div').hasClass('ng-valid-maxlength')).toBe(true);

      //remove child input
      scope.$apply('inputPresent = false');

      expect(parent.$error.required).toBeFalsy();
      expect(parent.$$success.maxlength).toBeFalsy();

      expect(child.$error.required).toBeFalsy();
      expect(child.$$success.maxlength).toBeFalsy();

      expect(doc.hasClass('ng-valid')).toBe(true);
      expect(doc.hasClass('ng-valid-required')).toBe(false);
      expect(doc.hasClass('ng-invalid-required')).toBe(false);
      expect(doc.hasClass('ng-valid-maxlength')).toBe(false);
      expect(doc.hasClass('ng-invalid-maxlength')).toBe(false);

      expect(doc.find('div').hasClass('ng-valid')).toBe(true);
      expect(doc.find('div').hasClass('ng-valid-required')).toBe(false);
      expect(doc.find('div').hasClass('ng-invalid-required')).toBe(false);
      expect(doc.find('div').hasClass('ng-valid-maxlength')).toBe(false);
      expect(doc.find('div').hasClass('ng-invalid-maxlength')).toBe(false);
    });

    it('should deregister a input that is $pending when it is removed from DOM', function() {
      doc = jqLite(
        '<form name="parent">' +
          '<div class="ng-form" name="child">' +
            '<input ng-if="inputPresent" ng-model="modelA" name="inputA">' +
          '</div>' +
        '</form>');
      $compile(doc)(scope);
      scope.$apply('inputPresent = true');

      var parent = scope.parent;
      var child = scope.child;
      var input = child.inputA;

      scope.$apply(child.inputA.$setValidity('fake', undefined));

      expect(parent).toBeDefined();
      expect(child).toBeDefined();

      expect(parent.$pending.fake).toEqual([child]);
      expect(child.$pending.fake).toEqual([input]);

      expect(doc.hasClass('ng-pending')).toBe(true);
      expect(doc.find('div').hasClass('ng-pending')).toBe(true);

      //remove child input
      scope.$apply('inputPresent = false');

      expect(parent.$pending).toBeUndefined();
      expect(child.$pending).toBeUndefined();

      expect(doc.hasClass('ng-pending')).toBe(false);
      expect(doc.find('div').hasClass('ng-pending')).toBe(false);
    });


    it('should leave the parent form invalid when deregister a removed input', function() {
      doc = jqLite(
        '<form name="parent">' +
          '<div class="ng-form" name="child">' +
            '<input ng-if="inputPresent" ng-model="modelA" name="inputA" required>' +
            '<input ng-model="modelB" name="inputB" required>' +
          '</div>' +
        '</form>');
      $compile(doc)(scope);
      scope.inputPresent = true;
      scope.$apply();

      var parent = scope.parent,
          child = scope.child,
          inputA = child.inputA,
          inputB = child.inputB;

      expect(parent).toBeDefined();
      expect(child).toBeDefined();
      expect(parent.$error.required).toEqual([child]);
      expect(child.$error.required).toEqual([inputB, inputA]);

      //remove child input
      scope.inputPresent = false;
      scope.$apply();

      expect(parent.$error.required).toEqual([child]);
      expect(child.$error.required).toEqual([inputB]);
    });


    it('should ignore changes in manually removed child forms', function() {
      doc = $compile(
        '<form name="myForm">' +
          '<ng-form name="childform">' +
            '<input name="childformcontrol" ng-maxlength="1" ng-model="value"/>' +
          '</ng-form>' +
        '</form>')(scope);

      var form = scope.myForm;
      var childformController = doc.find('ng-form').eq(0).controller('form');

      var input = doc.find('input').eq(0);
      var inputController = input.controller('ngModel');

      changeInputValue(input, 'ab');
      scope.$apply();

      expect(form.$dirty).toBe(true);
      expect(form.$error.maxlength).toBeTruthy();
      expect(form.$error.maxlength[0].$name).toBe('childform');

      inputController.$setPristine();
      expect(form.$dirty).toBe(true);

      form.$setPristine();

      // remove child form
      form.$removeControl(childformController);
      expect(form.childform).toBeUndefined();
      expect(form.$error.maxlength).toBeFalsy();

      changeInputValue(input, 'abc');
      scope.$apply();

      expect(form.$error.maxlength).toBeFalsy();
      expect(form.$dirty).toBe(false);
    });


    it('should react to changes in manually added child forms', function() {
      doc = $compile(
        '<form name="myForm">' +
          '<ng-form name="childForm">' +
            '<input name="childformcontrol" ng-maxlength="1" ng-model="value" />' +
          '</ng-form>' +
        '</form>')(scope);

      var form = scope.myForm;
      var childFormController = doc.find('ng-form').eq(0).controller('form');

      var input = doc.find('input').eq(0);

      // remove child form so we can add it manually
      form.$removeControl(childFormController);
      changeInputValue(input, 'ab');

      expect(form.childForm).toBeUndefined();
      expect(form.$dirty).toBe(false);
      expect(form.$error.maxlength).toBeFalsy();

      // re-add the child form; its current validation state is not propagated
      form.$addControl(childFormController);
      expect(form.childForm).toBe(childFormController);
      expect(form.$error.maxlength).toBeFalsy();
      expect(form.$dirty).toBe(false);

      // Only when the input inside the child form changes, the validation state is propagated
      changeInputValue(input, 'abc');
      expect(form.$error.maxlength[0]).toBe(childFormController);
      expect(form.$dirty).toBe(false);
    });


    it('should use the correct parent when renaming and removing dynamically added forms', function() {
      scope.formName = 'childForm';
      scope.hasChildForm = true;

      doc = $compile(
        '<form name="myForm">' +
          '<div ng-if="hasChildForm">' +
            '<ng-form name="{{formName}}">' +
              '<input name="childformcontrol" ng-maxlength="1" ng-model="value"/>' +
            '</ng-form>' +
          '</div>' +
        '</form>' +
        '<form name="otherForm"></form>')(scope);

      scope.$digest();

      var form = scope.myForm;
      var otherForm = scope.otherForm;
      var childForm = form.childForm;

      // remove child form and add it to another form
      form.$removeControl(childForm);
      otherForm.$addControl(childForm);

      expect(form.childForm).toBeUndefined();
      expect(otherForm.childForm).toBe(childForm);

      // rename the childForm
      scope.formName = 'childFormMoved';
      scope.$digest();

      expect(form.childFormMoved).toBeUndefined();
      expect(otherForm.childForm).toBeUndefined();
      expect(otherForm.childFormMoved).toBe(childForm);

      scope.hasChildForm = false;
      scope.$digest();

      expect(form.childFormMoved).toBeUndefined();
      expect(otherForm.childFormMoved).toBeUndefined();
    });


    it('should chain nested forms in repeater', function() {
      doc = jqLite(
         '<ng:form name=parent>' +
          '<ng:form ng:repeat="f in forms" name=child>' +
            '<input type=text ng:model=text name=text>' +
           '</ng:form>' +
         '</ng:form>');
      $compile(doc)(scope);

      scope.$apply(function() {
        scope.forms = [1];
      });

      var parent = scope.parent;
      var child = doc.find('input').scope().child;
      var input = child.text;

      expect(parent).toBeDefined();
      expect(child).toBeDefined();
      expect(input).toBeDefined();

      input.$setValidity('myRule', false);
      expect(input.$error.myRule).toEqual(true);
      expect(child.$error.myRule).toEqual([input]);
      expect(parent.$error.myRule).toEqual([child]);

      input.$setValidity('myRule', true);
      expect(parent.$error.myRule).toBeFalsy();
      expect(child.$error.myRule).toBeFalsy();
    });
  });


  describe('validation', function() {

    beforeEach(function() {
      doc = $compile(
          '<form name="form">' +
            '<input ng-model="name" name="name" store-model-ctrl/>' +
          '</form>')(scope);

      scope.$digest();
    });


    it('should have ng-valid/ng-invalid css class', function() {
      expect(doc).toBeValid();

      control.$setValidity('error', false);
      scope.$digest();
      expect(doc).toBeInvalid();
      expect(doc.hasClass('ng-valid-error')).toBe(false);
      expect(doc.hasClass('ng-invalid-error')).toBe(true);

      control.$setValidity('another', false);
      scope.$digest();
      expect(doc.hasClass('ng-valid-error')).toBe(false);
      expect(doc.hasClass('ng-invalid-error')).toBe(true);
      expect(doc.hasClass('ng-valid-another')).toBe(false);
      expect(doc.hasClass('ng-invalid-another')).toBe(true);

      control.$setValidity('error', true);
      scope.$digest();
      expect(doc).toBeInvalid();
      expect(doc.hasClass('ng-valid-error')).toBe(true);
      expect(doc.hasClass('ng-invalid-error')).toBe(false);
      expect(doc.hasClass('ng-valid-another')).toBe(false);
      expect(doc.hasClass('ng-invalid-another')).toBe(true);

      control.$setValidity('another', true);
      scope.$digest();
      expect(doc).toBeValid();
      expect(doc.hasClass('ng-valid-error')).toBe(true);
      expect(doc.hasClass('ng-invalid-error')).toBe(false);
      expect(doc.hasClass('ng-valid-another')).toBe(true);
      expect(doc.hasClass('ng-invalid-another')).toBe(false);

      // validators are skipped, e.g. because of a parser error
      control.$setValidity('error', null);
      control.$setValidity('another', null);
      scope.$digest();
      expect(doc.hasClass('ng-valid-error')).toBe(false);
      expect(doc.hasClass('ng-invalid-error')).toBe(false);
      expect(doc.hasClass('ng-valid-another')).toBe(false);
      expect(doc.hasClass('ng-invalid-another')).toBe(false);
    });

    it('should have ng-pristine/ng-dirty css class', function() {
      expect(doc).toBePristine();

      control.$setViewValue('');
      scope.$apply();
      expect(doc).toBeDirty();
    });
  });

  describe('$pending', function() {
    beforeEach(function() {
      doc = $compile('<form name="form"></form>')(scope);
      scope.$digest();
    });

    it('should set valid and invalid to undefined when a validation error state is set as pending', inject(function($q, $rootScope) {
      var defer, form = doc.data('$formController');

      var ctrl = {};
      form.$setValidity('matias', undefined, ctrl);

      expect(form.$valid).toBeUndefined();
      expect(form.$invalid).toBeUndefined();
      expect(form.$pending.matias).toEqual([ctrl]);

      form.$setValidity('matias', true, ctrl);

      expect(form.$valid).toBe(true);
      expect(form.$invalid).toBe(false);
      expect(form.$pending).toBeUndefined();

      form.$setValidity('matias', false, ctrl);

      expect(form.$valid).toBe(false);
      expect(form.$invalid).toBe(true);
      expect(form.$pending).toBeUndefined();
    }));
  });

  describe('$setPristine', function() {

    it('should reset pristine state of form and controls', function() {

      doc = $compile(
          '<form name="testForm">' +
            '<input ng-model="named1" name="foo">' +
            '<input ng-model="named2" name="bar">' +
          '</form>')(scope);

      scope.$digest();

      var form = doc,
          formCtrl = scope.testForm,
          input1 = form.find('input').eq(0),
          input1Ctrl = input1.controller('ngModel'),
          input2 = form.find('input').eq(1),
          input2Ctrl = input2.controller('ngModel');

      input1Ctrl.$setViewValue('xx');
      input2Ctrl.$setViewValue('yy');
      scope.$apply();
      expect(form).toBeDirty();
      expect(input1).toBeDirty();
      expect(input2).toBeDirty();


      formCtrl.$setPristine();
      scope.$digest();
      expect(form).toBePristine();
      expect(formCtrl.$pristine).toBe(true);
      expect(formCtrl.$dirty).toBe(false);
      expect(input1).toBePristine();
      expect(input1Ctrl.$pristine).toBe(true);
      expect(input1Ctrl.$dirty).toBe(false);
      expect(input2).toBePristine();
      expect(input2Ctrl.$pristine).toBe(true);
      expect(input2Ctrl.$dirty).toBe(false);
    });


    it('should reset pristine state of anonymous form controls', function() {

      doc = $compile(
          '<form name="testForm">' +
            '<input ng-model="anonymous">' +
          '</form>')(scope);

      scope.$digest();

      var form = doc,
          formCtrl = scope.testForm,
          input = form.find('input').eq(0),
          inputCtrl = input.controller('ngModel');

      inputCtrl.$setViewValue('xx');
      scope.$apply();
      expect(form).toBeDirty();
      expect(input).toBeDirty();

      formCtrl.$setPristine();
      scope.$digest();
      expect(form).toBePristine();
      expect(formCtrl.$pristine).toBe(true);
      expect(formCtrl.$dirty).toBe(false);
      expect(input).toBePristine();
      expect(inputCtrl.$pristine).toBe(true);
      expect(inputCtrl.$dirty).toBe(false);
    });


    it('should reset pristine state of nested forms', function() {

      doc = $compile(
          '<form name="testForm">' +
            '<div ng-form>' +
              '<input ng-model="named" name="foo">' +
            '</div>' +
          '</form>')(scope);

      scope.$digest();

      var form = doc,
          formCtrl = scope.testForm,
          nestedForm = form.find('div'),
          nestedFormCtrl = nestedForm.controller('form'),
          nestedInput = form.find('input').eq(0),
          nestedInputCtrl = nestedInput.controller('ngModel');

      nestedInputCtrl.$setViewValue('xx');
      scope.$apply();
      expect(form).toBeDirty();
      expect(nestedForm).toBeDirty();
      expect(nestedInput).toBeDirty();

      formCtrl.$setPristine();
      scope.$digest();
      expect(form).toBePristine();
      scope.$digest();

      expect(formCtrl.$pristine).toBe(true);
      expect(formCtrl.$dirty).toBe(false);
      expect(nestedForm).toBePristine();
      expect(nestedFormCtrl.$pristine).toBe(true);
      expect(nestedFormCtrl.$dirty).toBe(false);
      expect(nestedInput).toBePristine();
      expect(nestedInputCtrl.$pristine).toBe(true);
      expect(nestedInputCtrl.$dirty).toBe(false);
    });
  });

  describe('$setUntouched', function() {
    it('should trigger setUntouched on form controls', function() {
      var form = $compile(
          '<form name="myForm">' +
            '<input name="alias" type="text" ng-model="name" />' +
          '</form>')(scope);
      scope.$digest();

      scope.myForm.alias.$setTouched();
      expect(scope.myForm.alias.$touched).toBe(true);
      scope.myForm.$setUntouched();
      expect(scope.myForm.alias.$touched).toBe(false);
      dealoc(form);
    });

    it('should trigger setUntouched on form controls with nested forms', function() {
      var form = $compile(
          '<form name="myForm">' +
            '<div class="ng-form" name="childForm">' +
              '<input name="alias" type="text" ng-model="name" />' +
            '</div>' +
          '</form>')(scope);
      scope.$digest();

      scope.myForm.childForm.alias.$setTouched();
      expect(scope.myForm.childForm.alias.$touched).toBe(true);
      scope.myForm.$setUntouched();
      expect(scope.myForm.childForm.alias.$touched).toBe(false);
      dealoc(form);
    });
  });

  describe('$getControls', function() {
    it('should return an empty array if the controller has no controls', function() {
      doc = $compile('<form name="testForm"></form>')(scope);

      scope.$digest();

      var formCtrl = scope.testForm;

      expect(formCtrl.$getControls()).toEqual([]);
    });

    it('should return a shallow copy of the form controls', function() {
      doc = $compile(
          '<form name="testForm">' +
            '<input ng-model="named" name="foo">' +
            '<div ng-form>' +
              '<input ng-model="named" name="foo">' +
            '</div>' +
          '</form>')(scope);

      scope.$digest();

      var form = doc,
          formCtrl = scope.testForm,
          formInput = form.children('input').eq(0),
          formInputCtrl = formInput.controller('ngModel'),
          nestedForm = form.find('div'),
          nestedFormCtrl = nestedForm.controller('form'),
          nestedInput = nestedForm.children('input').eq(0),
          nestedInputCtrl = nestedInput.controller('ngModel');

      var controls = formCtrl.$getControls();

      expect(controls).not.toBe(formCtrl.$$controls);

      controls.push('something');
      expect(formCtrl.$$controls).not.toContain('something');

      expect(controls[0]).toBe(formInputCtrl);
      expect(controls[1]).toBe(nestedFormCtrl);

      var nestedControls = controls[1].$getControls();

      expect(nestedControls[0]).toBe(nestedInputCtrl);
    });
  });

  it('should rename nested form controls when interpolated name changes', function() {
    scope.idA = 'A';
    scope.idB = 'X';

    doc = $compile(
      '<form name="form">' +
        '<div ng-form="nested{{idA}}">' +
          '<div ng-form name="nested{{idB}}"' +
          '</div>' +
        '</div>' +
      '</form>'
    )(scope);

    scope.$digest();
    var formA = scope.form.nestedA;
    expect(formA).toBeDefined();
    expect(formA.$name).toBe('nestedA');

    var formX = formA.nestedX;
    expect(formX).toBeDefined();
    expect(formX.$name).toBe('nestedX');

    scope.idA = 'B';
    scope.idB = 'Y';
    scope.$digest();

    expect(scope.form.nestedA).toBeUndefined();
    expect(scope.form.nestedB).toBe(formA);
    expect(formA.nestedX).toBeUndefined();
    expect(formA.nestedY).toBe(formX);
  });


  it('should rename forms with no parent when interpolated name changes', function() {
    var element = $compile('<form name="name{{nameID}}"></form>')(scope);
    var element2 = $compile('<div ng-form="ngform{{nameID}}"></div>')(scope);
    scope.nameID = 'A';
    scope.$digest();
    var form = element.controller('form');
    var form2 = element2.controller('form');
    expect(scope.nameA).toBe(form);
    expect(scope.ngformA).toBe(form2);
    expect(form.$name).toBe('nameA');
    expect(form2.$name).toBe('ngformA');

    scope.nameID = 'B';
    scope.$digest();
    expect(scope.nameA).toBeUndefined();
    expect(scope.ngformA).toBeUndefined();
    expect(scope.nameB).toBe(form);
    expect(scope.ngformB).toBe(form2);
    expect(form.$name).toBe('nameB');
    expect(form2.$name).toBe('ngformB');
  });

  it('should rename forms with an initially blank name', function() {
    var element = $compile('<form name="{{name}}"></form>')(scope);
    scope.$digest();
    var form = element.controller('form');
    expect(scope['']).toBe(form);
    expect(form.$name).toBe('');
    scope.name = 'foo';
    scope.$digest();
    expect(scope.foo).toBe(form);
    expect(form.$name).toBe('foo');
    expect(scope.foo).toBe(form);
  });

  describe('$setSubmitted', function() {
    beforeEach(function() {
      doc = $compile(
          '<form name="form" ng-submit="submitted = true">' +
            '<input type="text" ng-model="name" required />' +
            '<input type="submit" />' +
          '</form>')(scope);

      scope.$digest();
    });

    it('should not init in submitted state', function() {
      expect(scope.form.$submitted).toBe(false);
    });

    it('should be in submitted state when submitted', function() {
      browserTrigger(doc, 'submit');
      expect(scope.form.$submitted).toBe(true);
    });

    it('should revert submitted back to false when $setPristine is called on the form', function() {
      scope.form.$submitted = true;
      scope.form.$setPristine();
      expect(scope.form.$submitted).toBe(false);
    });
  });
});

describe('form animations', function() {
  beforeEach(module('ngAnimateMock'));

  function assertValidAnimation(animation, event, classNameAdded, classNameRemoved) {
    expect(animation.event).toBe(event);
    expect(animation.args[1]).toBe(classNameAdded);
    expect(animation.args[2]).toBe(classNameRemoved);
  }

  var doc, scope, form;
  beforeEach(inject(function($rootScope, $compile, $rootElement, $animate) {
    scope = $rootScope.$new();
    doc = jqLite('<form name="myForm"></form>');
    $rootElement.append(doc);
    $compile(doc)(scope);
    $animate.queue = [];
    form = scope.myForm;
  }));

  afterEach(function() {
    dealoc(doc);
  });

  it('should trigger an animation when invalid', inject(function($animate) {
    form.$setValidity('required', false);

    assertValidAnimation($animate.queue[0], 'removeClass', 'ng-valid');
    assertValidAnimation($animate.queue[1], 'addClass', 'ng-invalid');
    assertValidAnimation($animate.queue[2], 'addClass', 'ng-invalid-required');
  }));

  it('should trigger an animation when valid', inject(function($animate) {
    form.$setValidity('required', false);

    $animate.queue = [];

    form.$setValidity('required', true);

    assertValidAnimation($animate.queue[0], 'addClass', 'ng-valid');
    assertValidAnimation($animate.queue[1], 'removeClass', 'ng-invalid');
    assertValidAnimation($animate.queue[2], 'addClass', 'ng-valid-required');
  }));

  it('should trigger an animation when dirty', inject(function($animate) {
    form.$setDirty();

    assertValidAnimation($animate.queue[0], 'removeClass', 'ng-pristine');
    assertValidAnimation($animate.queue[1], 'addClass', 'ng-dirty');
  }));

  it('should trigger an animation when pristine', inject(function($animate) {
    form.$setDirty();

    $animate.queue = [];

    form.$setPristine();

    assertValidAnimation($animate.queue[0], 'setClass', 'ng-pristine', 'ng-dirty ng-submitted');
  }));

  it('should trigger custom errors as addClass/removeClass when invalid/valid', inject(function($animate) {
    form.$setValidity('custom-error', false);

    assertValidAnimation($animate.queue[0], 'removeClass', 'ng-valid');
    assertValidAnimation($animate.queue[1], 'addClass', 'ng-invalid');
    assertValidAnimation($animate.queue[2], 'addClass', 'ng-invalid-custom-error');

    $animate.queue = [];
    form.$setValidity('custom-error', true);

    assertValidAnimation($animate.queue[0], 'addClass', 'ng-valid');
    assertValidAnimation($animate.queue[1], 'removeClass', 'ng-invalid');
    assertValidAnimation($animate.queue[2], 'addClass', 'ng-valid-custom-error');
    assertValidAnimation($animate.queue[3], 'removeClass', 'ng-invalid-custom-error');
  }));
});
