import {Directionality} from '@angular/cdk/bidi';
import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
import {Component, DebugElement} from '@angular/core';
import {async, ComponentFixture, TestBed, inject} from '@angular/core/testing';
import {AbstractControl, AsyncValidatorFn, FormControl, FormGroup, ReactiveFormsModule,
  ValidationErrors, Validators} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {map} from 'rxjs/operators/map';
import {take} from 'rxjs/operators/take';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {MatStepperModule} from './index';
import {MatHorizontalStepper, MatStep, MatStepper, MatVerticalStepper} from './stepper';
import {MatStepperNext, MatStepperPrevious} from './stepper-button';
import {MatStepperIntl} from './stepper-intl';

const VALID_REGEX = /valid/;

describe('MatHorizontalStepper', () => {
  let dir = 'ltr';

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [MatStepperModule, NoopAnimationsModule, ReactiveFormsModule],
      declarations: [
        SimpleMatHorizontalStepperApp,
        SimplePreselectedMatHorizontalStepperApp,
        LinearMatHorizontalStepperApp
      ],
      providers: [
        {provide: Directionality, useFactory: () => ({value: dir})}
      ]
    });

    TestBed.compileComponents();
  }));

  describe('basic horizontal stepper', () => {
    let fixture: ComponentFixture<SimpleMatHorizontalStepperApp>;

    beforeEach(() => {
      fixture = TestBed.createComponent(SimpleMatHorizontalStepperApp);
      fixture.detectChanges();
    });

    it('should default to the first step', () => {
      let stepperComponent = fixture.debugElement
          .query(By.css('mat-horizontal-stepper')).componentInstance;
      expect(stepperComponent.selectedIndex).toBe(0);
    });

    it('should change selected index on header click', () => {
      let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
      assertSelectionChangeOnHeaderClick(fixture, stepHeaders);
    });

    it('should set the "tablist" role on stepper', () => {
      let stepperEl = fixture.debugElement.query(By.css('mat-horizontal-stepper')).nativeElement;
      expect(stepperEl.getAttribute('role')).toBe('tablist');
    });

    it('should set the proper "aria-orientation"', () => {
      let stepperEl = fixture.debugElement.query(By.css('mat-horizontal-stepper')).nativeElement;
      expect(stepperEl.getAttribute('aria-orientation')).toBe('horizontal');
    });

    it('should set aria-expanded of content correctly', () => {
      let stepContents = fixture.debugElement.queryAll(By.css(`.mat-horizontal-stepper-content`));
      assertCorrectAriaExpandedAttribute(fixture, stepContents);
    });

    it('should display the correct label', () => {
      assertCorrectStepLabel(fixture);
    });

    it('should go to next available step when the next button is clicked', () => {
      assertNextStepperButtonClick(fixture);
    });

    it('should go to previous available step when the previous button is clicked', () => {
      assertPreviousStepperButtonClick(fixture);
    });

    it('should set the correct step position for animation', () => {
      assertCorrectStepAnimationDirection(fixture);
    });

    it('should support keyboard events to move and select focus', () => {
      let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
      assertCorrectKeyboardInteraction(fixture, stepHeaders);
    });

    it('should not set focus on header of selected step if header is not clicked', () => {
      assertStepHeaderFocusNotCalled(fixture);
    });

    it('should only be able to return to a previous step if it is editable', () => {
      assertEditableStepChange(fixture);
    });

    it('should set create icon if step is editable and completed', () => {
      assertCorrectStepIcon(fixture, true, 'edit');
    });

    it('should set done icon if step is not editable and is completed', () => {
      assertCorrectStepIcon(fixture, false, 'done');
    });

    it('should re-render when the i18n labels change',
      inject([MatStepperIntl], (intl: MatStepperIntl) => {
        const header = fixture.debugElement.queryAll(By.css('mat-step-header'))[2].nativeElement;
        const optionalLabel = header.querySelector('.mat-step-optional');

        expect(optionalLabel).toBeTruthy();
        expect(optionalLabel.textContent).toBe('Optional');

        intl.optionalLabel = 'Valgfri';
        intl.changes.next();
        fixture.detectChanges();

        expect(optionalLabel.textContent).toBe('Valgfri');
      }));
  });

  describe('RTL', () => {
    let fixture: ComponentFixture<SimpleMatHorizontalStepperApp>;

    beforeEach(() => {
      dir = 'rtl';
      fixture = TestBed.createComponent(SimpleMatHorizontalStepperApp);
      fixture.detectChanges();
    });

    it('should reverse arrow key focus in RTL mode', () => {
      let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
      assertArrowKeyInteractionInRtl(fixture, stepHeaders);
    });

    it('should reverse animation in RTL mode', () => {
      assertCorrectStepAnimationDirection(fixture, 'rtl');
    });
  });

  describe('linear horizontal stepper', () => {
    let fixture: ComponentFixture<LinearMatHorizontalStepperApp>;
    let testComponent: LinearMatHorizontalStepperApp;
    let stepperComponent: MatHorizontalStepper;

    beforeEach(() => {
      fixture = TestBed.createComponent(LinearMatHorizontalStepperApp);
      fixture.detectChanges();

      testComponent = fixture.componentInstance;
      stepperComponent = fixture.debugElement
          .query(By.css('mat-horizontal-stepper')).componentInstance;
    });

    it('should have true linear attribute', () => {
      expect(stepperComponent.linear).toBe(true);
    });

    it('should not move to next step if current step is invalid', () => {
      expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe('');
      expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false);
      expect(testComponent.oneGroup.valid).toBe(false);
      expect(testComponent.oneGroup.invalid).toBe(true);
      expect(stepperComponent.selectedIndex).toBe(0);

      let stepHeaderEl = fixture.debugElement
          .queryAll(By.css('.mat-horizontal-stepper-header'))[1].nativeElement;
      assertLinearStepperValidity(stepHeaderEl, testComponent, fixture);
    });

    it('should not move to next step if current step is pending', () => {
      let stepHeaderEl = fixture.debugElement
          .queryAll(By.css('.mat-horizontal-stepper-header'))[2].nativeElement;

      assertLinearStepperPending(stepHeaderEl, testComponent, fixture);
    });

    it('should not focus step header upon click if it is not able to be selected', () => {
      assertStepHeaderBlurred(fixture);
    });

    it('should be able to move to next step even when invalid if current step is optional', () => {
      assertOptionalStepValidity(testComponent, fixture);
    });

    it('should not throw when there is a pre-defined selectedIndex', () => {
      fixture.destroy();

      let preselectedFixture = TestBed.createComponent(SimplePreselectedMatHorizontalStepperApp);
      let debugElement = preselectedFixture.debugElement;

      expect(() => preselectedFixture.detectChanges()).not.toThrow();

      let stepHeaders = debugElement.queryAll(By.css('.mat-horizontal-stepper-header'));
      assertSelectionChangeOnHeaderClick(preselectedFixture, stepHeaders);
    });
  });
});

describe('MatVerticalStepper', () => {
  let dir = 'ltr';

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [MatStepperModule, NoopAnimationsModule, ReactiveFormsModule],
      declarations: [
        SimpleMatVerticalStepperApp,
        LinearMatVerticalStepperApp
      ],
      providers: [
        {provide: Directionality, useFactory: () => ({value: dir})}
      ]
    });

    TestBed.compileComponents();
  }));

  describe('basic vertical stepper', () => {
    let fixture: ComponentFixture<SimpleMatVerticalStepperApp>;

    beforeEach(() => {
      fixture = TestBed.createComponent(SimpleMatVerticalStepperApp);
      fixture.detectChanges();
    });

    it('should default to the first step', () => {
      let stepperComponent = fixture.debugElement
          .query(By.css('mat-vertical-stepper')).componentInstance;
      expect(stepperComponent.selectedIndex).toBe(0);
    });

    it('should change selected index on header click', () => {
      let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
      assertSelectionChangeOnHeaderClick(fixture, stepHeaders);

    });

    it('should set the "tablist" role on stepper', () => {
      let stepperEl = fixture.debugElement.query(By.css('mat-vertical-stepper')).nativeElement;
      expect(stepperEl.getAttribute('role')).toBe('tablist');
    });

    it('should set the proper "aria-orientation"', () => {
      let stepperEl = fixture.debugElement.query(By.css('mat-vertical-stepper')).nativeElement;
      expect(stepperEl.getAttribute('aria-orientation')).toBe('vertical');
    });

    it('should set aria-expanded of content correctly', () => {
      let stepContents = fixture.debugElement.queryAll(By.css(`.mat-vertical-stepper-content`));
      assertCorrectAriaExpandedAttribute(fixture, stepContents);
    });

    it('should display the correct label', () => {
      assertCorrectStepLabel(fixture);
    });

    it('should go to next available step when the next button is clicked', () => {
      assertNextStepperButtonClick(fixture);
    });

    it('should go to previous available step when the previous button is clicked', () => {
      assertPreviousStepperButtonClick(fixture);
    });

    it('should set the correct step position for animation', () => {
      assertCorrectStepAnimationDirection(fixture);
    });

    it('should support keyboard events to move and select focus', () => {
      let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
      assertCorrectKeyboardInteraction(fixture, stepHeaders);
    });

    it('should not set focus on header of selected step if header is not clicked', () => {
      assertStepHeaderFocusNotCalled(fixture);
    });

    it('should only be able to return to a previous step if it is editable', () => {
      assertEditableStepChange(fixture);
    });

    it('should set create icon if step is editable and completed', () => {
      assertCorrectStepIcon(fixture, true, 'edit');
    });

    it('should set done icon if step is not editable and is completed', () => {
      assertCorrectStepIcon(fixture, false, 'done');
    });
  });

  describe('RTL', () => {
    let fixture: ComponentFixture<SimpleMatVerticalStepperApp>;

    beforeEach(() => {
      dir = 'rtl';
      fixture = TestBed.createComponent(SimpleMatVerticalStepperApp);
      fixture.detectChanges();
    });

    it('should reverse arrow key focus in RTL mode', () => {
      let stepHeaders = fixture.debugElement.queryAll(By.css('.mat-vertical-stepper-header'));
      assertArrowKeyInteractionInRtl(fixture, stepHeaders);
    });

    it('should reverse animation in RTL mode', () => {
      assertCorrectStepAnimationDirection(fixture, 'rtl');
    });
  });

  describe('linear vertical stepper', () => {
    let fixture: ComponentFixture<LinearMatVerticalStepperApp>;
    let testComponent: LinearMatVerticalStepperApp;
    let stepperComponent: MatVerticalStepper;

    beforeEach(() => {
      fixture = TestBed.createComponent(LinearMatVerticalStepperApp);
      fixture.detectChanges();

      testComponent = fixture.componentInstance;
      stepperComponent = fixture.debugElement
          .query(By.css('mat-vertical-stepper')).componentInstance;
    });

    it('should have true linear attribute', () => {
      expect(stepperComponent.linear).toBe(true);
    });

    it('should not move to next step if current step is invalid', () => {
      expect(testComponent.oneGroup.get('oneCtrl')!.value).toBe('');
      expect(testComponent.oneGroup.get('oneCtrl')!.valid).toBe(false);
      expect(testComponent.oneGroup.valid).toBe(false);
      expect(testComponent.oneGroup.invalid).toBe(true);
      expect(stepperComponent.selectedIndex).toBe(0);

      let stepHeaderEl = fixture.debugElement
          .queryAll(By.css('.mat-vertical-stepper-header'))[1].nativeElement;

      assertLinearStepperValidity(stepHeaderEl, testComponent, fixture);
    });

    it('should not move to next step if current step is pending', () => {
      let stepHeaderEl = fixture.debugElement
          .queryAll(By.css('.mat-vertical-stepper-header'))[2].nativeElement;

      assertLinearStepperPending(stepHeaderEl, testComponent, fixture);
    });

    it('should not focus step header upon click if it is not able to be selected', () => {
      assertStepHeaderBlurred(fixture);
    });

    it('should be able to move to next step even when invalid if current step is optional', () => {
      assertOptionalStepValidity(testComponent, fixture);
    });
  });
});

/** Asserts that `selectedIndex` updates correctly when header of another step is clicked. */
function assertSelectionChangeOnHeaderClick(fixture: ComponentFixture<any>,
                                            stepHeaders: DebugElement[]) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;

  expect(stepperComponent.selectedIndex).toBe(0);
  expect(stepperComponent.selected instanceof MatStep).toBe(true);

  // select the second step
  let stepHeaderEl = stepHeaders[1].nativeElement;
  stepHeaderEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(1);
  expect(stepperComponent.selected instanceof MatStep).toBe(true);

  // select the third step
  stepHeaderEl = stepHeaders[2].nativeElement;
  stepHeaderEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(2);
  expect(stepperComponent.selected instanceof MatStep).toBe(true);
}

/** Asserts that 'aria-expanded' attribute is correct for expanded content of step. */
function assertCorrectAriaExpandedAttribute(fixture: ComponentFixture<any>,
                                            stepContents: DebugElement[]) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;
  let firstStepContentEl = stepContents[0].nativeElement;
  expect(firstStepContentEl.getAttribute('aria-expanded')).toBe('true');

  stepperComponent.selectedIndex = 1;
  fixture.detectChanges();

  expect(firstStepContentEl.getAttribute('aria-expanded')).toBe('false');
  let secondStepContentEl = stepContents[1].nativeElement;
  expect(secondStepContentEl.getAttribute('aria-expanded')).toBe('true');
}

/** Asserts that step has correct label. */
function assertCorrectStepLabel(fixture: ComponentFixture<any>) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;
  let selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
  expect(selectedLabel.textContent).toMatch('Step 1');

  stepperComponent.selectedIndex = 2;
  fixture.detectChanges();

  selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
  expect(selectedLabel.textContent).toMatch('Step 3');

  fixture.componentInstance.inputLabel = 'New Label';
  fixture.detectChanges();

  selectedLabel = fixture.nativeElement.querySelector('[aria-selected="true"]');
  expect(selectedLabel.textContent).toMatch('New Label');
}

/** Asserts that clicking on MatStepperNext button updates `selectedIndex` correctly. */
function assertNextStepperButtonClick(fixture: ComponentFixture<any>) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;

  expect(stepperComponent.selectedIndex).toBe(0);

  let nextButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperNext))[0].nativeElement;
  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(1);

  nextButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperNext))[1].nativeElement;
  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(2);

  nextButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperNext))[2].nativeElement;
  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(2);
}

/** Asserts that clicking on MatStepperPrevious button updates `selectedIndex` correctly. */
function assertPreviousStepperButtonClick(fixture: ComponentFixture<any>) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;

  expect(stepperComponent.selectedIndex).toBe(0);

  stepperComponent.selectedIndex = 2;
  let previousButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperPrevious))[2].nativeElement;
  previousButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(1);

  previousButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperPrevious))[1].nativeElement;
  previousButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(0);

  previousButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperPrevious))[0].nativeElement;
  previousButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(0);
}

/** Asserts that step position is correct for animation. */
function assertCorrectStepAnimationDirection(fixture: ComponentFixture<any>, rtl?: 'rtl') {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;

  expect(stepperComponent._getAnimationDirection(0)).toBe('current');
  if (rtl === 'rtl') {
    expect(stepperComponent._getAnimationDirection(1)).toBe('previous');
    expect(stepperComponent._getAnimationDirection(2)).toBe('previous');
  } else {
    expect(stepperComponent._getAnimationDirection(1)).toBe('next');
    expect(stepperComponent._getAnimationDirection(2)).toBe('next');
  }

  stepperComponent.selectedIndex = 1;
  fixture.detectChanges();

  if (rtl === 'rtl') {
    expect(stepperComponent._getAnimationDirection(0)).toBe('next');
    expect(stepperComponent._getAnimationDirection(2)).toBe('previous');
  } else {
    expect(stepperComponent._getAnimationDirection(0)).toBe('previous');
    expect(stepperComponent._getAnimationDirection(2)).toBe('next');
  }
  expect(stepperComponent._getAnimationDirection(1)).toBe('current');

  stepperComponent.selectedIndex = 2;
  fixture.detectChanges();

  if (rtl === 'rtl') {
    expect(stepperComponent._getAnimationDirection(0)).toBe('next');
    expect(stepperComponent._getAnimationDirection(1)).toBe('next');
  } else {
    expect(stepperComponent._getAnimationDirection(0)).toBe('previous');
    expect(stepperComponent._getAnimationDirection(1)).toBe('previous');
  }
  expect(stepperComponent._getAnimationDirection(2)).toBe('current');
}

/** Asserts that keyboard interaction works correctly. */
function assertCorrectKeyboardInteraction(fixture: ComponentFixture<any>,
                                          stepHeaders: DebugElement[]) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;

  expect(stepperComponent._focusIndex).toBe(0);
  expect(stepperComponent.selectedIndex).toBe(0);

  let stepHeaderEl = stepHeaders[0].nativeElement;
  dispatchKeyboardEvent(stepHeaderEl, 'keydown', RIGHT_ARROW);
  fixture.detectChanges();

  expect(stepperComponent._focusIndex)
      .toBe(1, 'Expected index of focused step to increase by 1 after RIGHT_ARROW event.');
  expect(stepperComponent.selectedIndex)
      .toBe(0, 'Expected index of selected step to remain unchanged after RIGHT_ARROW event.');

  stepHeaderEl = stepHeaders[1].nativeElement;
  dispatchKeyboardEvent(stepHeaderEl, 'keydown', ENTER);
  fixture.detectChanges();

  expect(stepperComponent._focusIndex)
      .toBe(1, 'Expected index of focused step to remain unchanged after ENTER event.');
  expect(stepperComponent.selectedIndex)
      .toBe(1,
          'Expected index of selected step to change to index of focused step after ENTER event.');

  stepHeaderEl = stepHeaders[1].nativeElement;
  dispatchKeyboardEvent(stepHeaderEl, 'keydown', LEFT_ARROW);
  fixture.detectChanges();

  expect(stepperComponent._focusIndex)
      .toBe(0, 'Expected index of focused step to decrease by 1 after LEFT_ARROW event.');
  expect(stepperComponent.selectedIndex)
      .toBe(1, 'Expected index of selected step to remain unchanged after LEFT_ARROW event.');

  // When the focus is on the last step and right arrow key is pressed, the focus should cycle
  // through to the first step.
  stepperComponent._focusIndex = 2;
  stepHeaderEl = stepHeaders[2].nativeElement;
  dispatchKeyboardEvent(stepHeaderEl, 'keydown', RIGHT_ARROW);
  fixture.detectChanges();

  expect(stepperComponent._focusIndex)
      .toBe(0,
          'Expected index of focused step to cycle through to index 0 after RIGHT_ARROW event.');
  expect(stepperComponent.selectedIndex)
      .toBe(1, 'Expected index of selected step to remain unchanged after RIGHT_ARROW event.');

  stepHeaderEl = stepHeaders[0].nativeElement;
  dispatchKeyboardEvent(stepHeaderEl, 'keydown', SPACE);
  fixture.detectChanges();

  expect(stepperComponent._focusIndex)
      .toBe(0, 'Expected index of focused to remain unchanged after SPACE event.');
  expect(stepperComponent.selectedIndex)
      .toBe(0,
          'Expected index of selected step to change to index of focused step after SPACE event.');
}

/** Asserts that step selection change using stepper buttons does not focus step header. */
function assertStepHeaderFocusNotCalled(fixture: ComponentFixture<any>) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;
  let stepHeaderEl = fixture.debugElement.queryAll(By.css('mat-step-header'))[1].nativeElement;
  let nextButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperNext))[0].nativeElement;
  spyOn(stepHeaderEl, 'focus');
  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(1);
  expect(stepHeaderEl.focus).not.toHaveBeenCalled();
}

/** Asserts that arrow key direction works correctly in RTL mode. */
function assertArrowKeyInteractionInRtl(fixture: ComponentFixture<any>,
                                        stepHeaders: DebugElement[]) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;

  expect(stepperComponent._focusIndex).toBe(0);

  let stepHeaderEl = stepHeaders[0].nativeElement;
  dispatchKeyboardEvent(stepHeaderEl, 'keydown', LEFT_ARROW);
  fixture.detectChanges();

  expect(stepperComponent._focusIndex).toBe(1);

  stepHeaderEl = stepHeaders[1].nativeElement;
  dispatchKeyboardEvent(stepHeaderEl, 'keydown', RIGHT_ARROW);
  fixture.detectChanges();

  expect(stepperComponent._focusIndex).toBe(0);
}

/**
 * Asserts that linear stepper does not allow step selection change if current step is not valid.
 */
function assertLinearStepperValidity(stepHeaderEl: HTMLElement,
                                     testComponent:
                                         LinearMatHorizontalStepperApp |
                                         LinearMatVerticalStepperApp,
                                     fixture: ComponentFixture<any>) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;

  stepHeaderEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(0);

  let nextButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperNext))[0].nativeElement;
  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(0);

  testComponent.oneGroup.get('oneCtrl')!.setValue('answer');
  stepHeaderEl.click();
  fixture.detectChanges();

  expect(testComponent.oneGroup.valid).toBe(true);
  expect(stepperComponent.selectedIndex).toBe(1);
}

/** Asserts that linear stepper does not allow step selection change if current step is pending. */
function assertLinearStepperPending(stepHeaderEl: HTMLElement,
                                    testComponent:
                                        LinearMatHorizontalStepperApp |
                                        LinearMatVerticalStepperApp,
                                    fixture: ComponentFixture<any>) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;
  let nextButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperNext))[1].nativeElement;

  testComponent.oneGroup.get('oneCtrl')!.setValue('input');
  testComponent.twoGroup.get('twoCtrl')!.setValue('input');
  stepperComponent.selectedIndex = 1;
  fixture.detectChanges();
  expect(stepperComponent.selectedIndex).toBe(1);

  // Step status = PENDING
  // Assert that linear stepper does not allow step selection change
  expect(testComponent.twoGroup.pending).toBe(true);

  stepHeaderEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(1);

  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(1);

  // Trigger asynchronous validation
  testComponent.validationTrigger.next();
  // Asynchronous validation completed:
  // Step status = VALID
  expect(testComponent.twoGroup.pending).toBe(false);
  expect(testComponent.twoGroup.valid).toBe(true);

  stepHeaderEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(2);

  stepperComponent.selectedIndex = 1;
  fixture.detectChanges();
  expect(stepperComponent.selectedIndex).toBe(1);

  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(2);
}

/** Asserts that step header focus is blurred if the step cannot be selected upon header click. */
function assertStepHeaderBlurred(fixture: ComponentFixture<any>) {
  let stepHeaderEl = fixture.debugElement
      .queryAll(By.css('mat-step-header'))[1].nativeElement;
  spyOn(stepHeaderEl, 'blur');
  stepHeaderEl.click();
  fixture.detectChanges();

  expect(stepHeaderEl.blur).toHaveBeenCalled();
}

/** Asserts that it is only possible to go back to a previous step if the step is editable. */
function assertEditableStepChange(fixture: ComponentFixture<any>) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;

  stepperComponent.selectedIndex = 1;
  stepperComponent._steps.toArray()[0].editable = false;
  let previousButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperPrevious))[1].nativeElement;
  previousButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(1);

  stepperComponent._steps.toArray()[0].editable = true;
  previousButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(0);
}

/**
 * Asserts that it is possible to skip an optional step in linear stepper if there is no input
 * or the input is valid.
 */
function assertOptionalStepValidity(testComponent:
                                        LinearMatHorizontalStepperApp | LinearMatVerticalStepperApp,
                                    fixture: ComponentFixture<any>) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;

  testComponent.oneGroup.get('oneCtrl')!.setValue('input');
  testComponent.twoGroup.get('twoCtrl')!.setValue('input');
  testComponent.validationTrigger.next();
  stepperComponent.selectedIndex = 2;
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex).toBe(2);
  expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(true);

  let nextButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperNext))[2].nativeElement;
  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent.selectedIndex)
      .toBe(3, 'Expected selectedIndex to change when optional step input is empty.');

  stepperComponent.selectedIndex = 2;
  testComponent.threeGroup.get('threeCtrl')!.setValue('input');
  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(false);
  expect(stepperComponent.selectedIndex)
      .toBe(2, 'Expected selectedIndex to remain unchanged when optional step input is invalid.');

  testComponent.threeGroup.get('threeCtrl')!.setValue('valid');
  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(testComponent.threeGroup.get('threeCtrl')!.valid).toBe(true);
  expect(stepperComponent.selectedIndex)
      .toBe(3, 'Expected selectedIndex to change when optional step input is valid.');
}

/** Asserts that step header set the correct icon depending on the state of step. */
function assertCorrectStepIcon(fixture: ComponentFixture<any>,
                               isEditable: boolean,
                               icon: String) {
  let stepperComponent = fixture.debugElement.query(By.directive(MatStepper)).componentInstance;
  let nextButtonNativeEl = fixture.debugElement
      .queryAll(By.directive(MatStepperNext))[0].nativeElement;
  expect(stepperComponent._getIndicatorType(0)).toBe('number');
  stepperComponent._steps.toArray()[0].editable = isEditable;
  nextButtonNativeEl.click();
  fixture.detectChanges();

  expect(stepperComponent._getIndicatorType(0)).toBe(icon);
}

function asyncValidator(minLength: number, validationTrigger: Observable<any>): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return validationTrigger.pipe(
      map(() =>  {
        const success = control.value && control.value.length >= minLength;
        return success ? null : { 'asyncValidation': {}};
      }),
      take(1)
    );
  };
}

@Component({
  template: `
    <mat-horizontal-stepper>
      <mat-step>
        <ng-template matStepLabel>Step 1</ng-template>
        Content 1
        <div>
          <button mat-button matStepperPrevious>Back</button>
          <button mat-button matStepperNext>Next</button>
        </div>
      </mat-step>
      <mat-step>
        <ng-template matStepLabel>Step 2</ng-template>
        Content 2
        <div>
          <button mat-button matStepperPrevious>Back</button>
          <button mat-button matStepperNext>Next</button>
        </div>
      </mat-step>
      <mat-step [label]="inputLabel" optional>
        Content 3
        <div>
          <button mat-button matStepperPrevious>Back</button>
          <button mat-button matStepperNext>Next</button>
        </div>
      </mat-step>
    </mat-horizontal-stepper>
  `
})
class SimpleMatHorizontalStepperApp {
  inputLabel = 'Step 3';
}

@Component({
  template: `
    <mat-horizontal-stepper linear>
      <mat-step [stepControl]="oneGroup">
        <form [formGroup]="oneGroup">
          <ng-template matStepLabel>Step one</ng-template>
          <input formControlName="oneCtrl" required>
          <div>
            <button mat-button matStepperPrevious>Back</button>
            <button mat-button matStepperNext>Next</button>
          </div>
        </form>
      </mat-step>
      <mat-step [stepControl]="twoGroup">
        <form [formGroup]="twoGroup">
          <ng-template matStepLabel>Step two</ng-template>
          <input formControlName="twoCtrl" required>
          <div>
            <button mat-button matStepperPrevious>Back</button>
            <button mat-button matStepperNext>Next</button>
          </div>
        </form>
      </mat-step>
      <mat-step [stepControl]="threeGroup" optional>
        <form [formGroup]="threeGroup">
          <ng-template matStepLabel>Step two</ng-template>
          <input formControlName="threeCtrl">
          <div>
            <button mat-button matStepperPrevious>Back</button>
            <button mat-button matStepperNext>Next</button>
          </div>
        </form>
      </mat-step>
      <mat-step>
        Done
      </mat-step>
    </mat-horizontal-stepper>
  `
})
class LinearMatHorizontalStepperApp {
  oneGroup: FormGroup;
  twoGroup: FormGroup;
  threeGroup: FormGroup;

  validationTrigger: Subject<any> = new Subject();

  ngOnInit() {
    this.oneGroup = new FormGroup({
      oneCtrl: new FormControl('', Validators.required)
    });
    this.twoGroup = new FormGroup({
      twoCtrl: new FormControl('', Validators.required, asyncValidator(3, this.validationTrigger))
    });
    this.threeGroup = new FormGroup({
      threeCtrl: new FormControl('', Validators.pattern(VALID_REGEX))
    });
  }
}

@Component({
  template: `
    <mat-vertical-stepper>
      <mat-step>
        <ng-template matStepLabel>Step 1</ng-template>
        Content 1
        <div>
          <button mat-button matStepperPrevious>Back</button>
          <button mat-button matStepperNext>Next</button>
        </div>
      </mat-step>
      <mat-step>
        <ng-template matStepLabel>Step 2</ng-template>
        Content 2
        <div>
          <button mat-button matStepperPrevious>Back</button>
          <button mat-button matStepperNext>Next</button>
        </div>
      </mat-step>
      <mat-step [label]="inputLabel">
        Content 3
        <div>
          <button mat-button matStepperPrevious>Back</button>
          <button mat-button matStepperNext>Next</button>
        </div>
      </mat-step>
    </mat-vertical-stepper>
  `
})
class SimpleMatVerticalStepperApp {
  inputLabel = 'Step 3';
}

@Component({
  template: `
    <mat-vertical-stepper linear>
      <mat-step [stepControl]="oneGroup">
        <form [formGroup]="oneGroup">
          <ng-template matStepLabel>Step one</ng-template>
          <input formControlName="oneCtrl" required>
          <div>
            <button mat-button matStepperPrevious>Back</button>
            <button mat-button matStepperNext>Next</button>
          </div>
        </form>
      </mat-step>
      <mat-step [stepControl]="twoGroup">
        <form [formGroup]="twoGroup">
          <ng-template matStepLabel>Step two</ng-template>
          <input formControlName="twoCtrl" required>
          <div>
            <button mat-button matStepperPrevious>Back</button>
            <button mat-button matStepperNext>Next</button>
          </div>
        </form>
      </mat-step>
      <mat-step [stepControl]="threeGroup" optional>
        <form [formGroup]="threeGroup">
          <ng-template matStepLabel>Step two</ng-template>
          <input formControlName="threeCtrl">
          <div>
            <button mat-button matStepperPrevious>Back</button>
            <button mat-button matStepperNext>Next</button>
          </div>
        </form>
      </mat-step>
      <mat-step>
        Done
      </mat-step>
    </mat-vertical-stepper>
  `
})
class LinearMatVerticalStepperApp {
  oneGroup: FormGroup;
  twoGroup: FormGroup;
  threeGroup: FormGroup;

  validationTrigger: Subject<any> = new Subject();

  ngOnInit() {
    this.oneGroup = new FormGroup({
      oneCtrl: new FormControl('', Validators.required)
    });
    this.twoGroup = new FormGroup({
      twoCtrl: new FormControl('', Validators.required, asyncValidator(3, this.validationTrigger))
    });
    this.threeGroup = new FormGroup({
      threeCtrl: new FormControl('', Validators.pattern(VALID_REGEX))
    });
  }
}

@Component({
  template: `
    <mat-horizontal-stepper [linear]="true" [selectedIndex]="index">
      <mat-step label="One"></mat-step>
      <mat-step label="Two"></mat-step>
      <mat-step label="Three"></mat-step>
    </mat-horizontal-stepper>
  `
})
class SimplePreselectedMatHorizontalStepperApp {
  index = 0;
}
