import {animate, style, transition, trigger} from '@angular/animations';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {
  BACKSPACE,
  DELETE,
  DOWN_ARROW,
  END,
  ENTER,
  HOME,
  LEFT_ARROW,
  RIGHT_ARROW,
  SPACE,
  TAB,
  UP_ARROW,
} from '@angular/cdk/keycodes';
import {
  createKeyboardEvent,
  dispatchEvent,
  dispatchFakeEvent,
  dispatchKeyboardEvent,
  patchElementFocus,
  typeInElement,
} from '@angular/cdk/testing/private';
import {
  ChangeDetectorRef,
  Component,
  DebugElement,
  EventEmitter,
  QueryList,
  Type,
  ViewChild,
  ViewChildren,
  inject,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing';
import {FormControl, FormsModule, NgForm, ReactiveFormsModule, Validators} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MatChipEvent, MatChipGrid, MatChipInputEvent, MatChipRow, MatChipsModule} from './index';

describe('MatChipGrid', () => {
  let chipGridDebugElement: DebugElement;
  let chipGridNativeElement: HTMLElement;
  let chipGridInstance: MatChipGrid;
  let chips: QueryList<MatChipRow>;
  let testComponent: StandardChipGrid;
  let directionality: {value: Direction; change: EventEmitter<Direction>};
  let primaryActions: NodeListOf<HTMLElement>;

  const expectNoCellFocused = () => {
    expect(Array.from(primaryActions)).not.toContain(document.activeElement as HTMLElement);
  };

  const expectLastCellFocused = () => {
    expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
  };

  describe('StandardChipGrid', () => {
    describe('basic behaviors', () => {
      let fixture: ComponentFixture<StandardChipGrid>;

      beforeEach(() => {
        fixture = createComponent(StandardChipGrid);
      });

      it('should add the `mat-mdc-chip-set` class', () => {
        expect(chipGridNativeElement.classList).toContain('mat-mdc-chip-set');
      });

      it('should toggle the chips disabled state based on whether it is disabled', () => {
        expect(chips.toArray().every(chip => chip.disabled)).toBe(false);

        chipGridInstance.disabled = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(chips.toArray().every(chip => chip.disabled)).toBe(true);

        chipGridInstance.disabled = false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(chips.toArray().every(chip => chip.disabled)).toBe(false);
      });

      it('should disable a chip that is added after the list became disabled', fakeAsync(() => {
        expect(chips.toArray().every(chip => chip.disabled)).toBe(false);

        chipGridInstance.disabled = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(chips.toArray().every(chip => chip.disabled)).toBe(true);

        fixture.componentInstance.chips.push(5, 6);
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        tick();
        fixture.detectChanges();

        expect(chips.toArray().every(chip => chip.disabled)).toBe(true);
      }));

      it('should not set a role on the grid when the list is empty', () => {
        testComponent.chips = [];
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(chipGridNativeElement.hasAttribute('role')).toBe(false);
      });

      it('should be able to set a custom role', () => {
        testComponent.role = 'listbox';
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(chipGridNativeElement.getAttribute('role')).toBe('listbox');
      });
    });

    describe('focus behaviors', () => {
      let fixture:
        | ComponentFixture<StandardChipGrid>
        | ComponentFixture<StandardChipGridWithAnimations>;

      beforeEach(() => {
        fixture = createComponent(StandardChipGrid);
      });

      it('should focus the first chip on focus', () => {
        chipGridInstance.focus();
        fixture.detectChanges();

        expect(document.activeElement).toBe(primaryActions[0]);
      });

      it('should focus the primary action when calling the `focus` method', () => {
        chips.last.focus();
        fixture.detectChanges();

        expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
      });

      it('should not be able to become focused when disabled', () => {
        expect(chipGridInstance.focused)
          .withContext('Expected grid to not be focused.')
          .toBe(false);

        chipGridInstance.disabled = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        chipGridInstance.focus();
        fixture.detectChanges();

        expect(chipGridInstance.focused)
          .withContext('Expected grid to continue not to be focused')
          .toBe(false);
      });

      it('should remove the tabindex from the grid if it is disabled', () => {
        expect(chipGridNativeElement.getAttribute('tabindex')).toBe('0');

        chipGridInstance.disabled = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(chipGridNativeElement.getAttribute('tabindex')).toBe('-1');
      });

      describe('on chip destroy', () => {
        it('should focus the next item', () => {
          const midItem = chips.get(2)!;

          // Focus the middle item
          midItem.focus();

          // Destroy the middle item
          testComponent.chips.splice(2, 1);
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();

          // It focuses the 4th item
          expect(document.activeElement).toBe(primaryActions[3]);
        });

        it('should focus the previous item', () => {
          // Focus the last item
          chips.last.focus();

          // Destroy the last item
          testComponent.chips.pop();
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();

          // It focuses the next-to-last item
          expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
        });

        it('should not focus if chip grid is not focused', fakeAsync(() => {
          const midItem = chips.get(2)!;

          // Focus and blur the middle item
          midItem.focus();
          (document.activeElement as HTMLElement).blur();
          tick();

          // Destroy the middle item
          testComponent.chips.splice(2, 1);
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();
          flush();

          // Should not have focus
          expect(chipGridNativeElement.contains(document.activeElement)).toBe(false);
        }));

        it('should focus the grid if the last focused item is removed', () => {
          testComponent.chips = [0];
          fixture.changeDetectorRef.markForCheck();

          spyOn(chipGridInstance, 'focus');
          patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement);
          chips.last.focus();

          testComponent.chips.pop();
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();

          expect(chipGridInstance.focus).toHaveBeenCalled();
        });

        it('should move focus to the last chip when the focused chip was deleted inside a component with animations', fakeAsync(() => {
          fixture.destroy();
          TestBed.resetTestingModule();

          fixture = createComponent(StandardChipGridWithAnimations, BrowserAnimationsModule);

          patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement);
          chips.last.focus();
          fixture.detectChanges();

          dispatchKeyboardEvent(chips.last._elementRef.nativeElement, 'keydown', BACKSPACE);
          fixture.detectChanges();
          tick(500);

          expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
        }));
      });

      it('should have a focus indicator', () => {
        const focusIndicators = chipGridNativeElement.querySelectorAll(
          '.mat-mdc-chip-primary-focus-indicator',
        );
        expect(focusIndicators.length).toBeGreaterThan(0);
        expect(focusIndicators.length).toBe(chips.length);
      });
    });

    describe('keyboard behavior', () => {
      describe('LTR (default)', () => {
        let fixture: ComponentFixture<ChipGridWithRemove>;
        let trailingActions: NodeListOf<HTMLElement>;

        beforeEach(fakeAsync(() => {
          fixture = createComponent(ChipGridWithRemove);
          flush();
          trailingActions = chipGridNativeElement.querySelectorAll(
            '.mdc-evolution-chip__action--trailing',
          );
        }));

        it('should focus previous column when press LEFT ARROW', () => {
          const lastIndex = primaryActions.length - 1;

          // Focus the first column of the last chip in the array
          chips.last.focus();
          expect(document.activeElement).toBe(primaryActions[lastIndex]);

          // Press the LEFT arrow
          dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', LEFT_ARROW);
          fixture.detectChanges();

          // It focuses the last column of the previous chip
          expect(document.activeElement).toBe(trailingActions[lastIndex - 1]);
        });

        it('should focus next column when press RIGHT ARROW', () => {
          // Focus the first column of the first chip in the array
          chips.first.focus();
          expect(document.activeElement).toBe(primaryActions[0]);

          // Press the RIGHT arrow
          dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
          fixture.detectChanges();

          // It focuses the next column of the chip
          expect(document.activeElement).toBe(trailingActions[0]);
        });

        it('should not handle arrow key events from non-chip elements', () => {
          const previousActiveElement = document.activeElement;

          dispatchKeyboardEvent(chipGridNativeElement, 'keydown', RIGHT_ARROW);
          fixture.detectChanges();

          expect(document.activeElement)
            .withContext('Expected focused item not to have changed.')
            .toBe(previousActiveElement);
        });

        it('should focus primary action in next row when pressing DOWN ARROW on primary action', () => {
          chips.first.focus();
          expect(document.activeElement).toBe(primaryActions[0]);

          dispatchKeyboardEvent(primaryActions[0], 'keydown', DOWN_ARROW);
          fixture.detectChanges();

          expect(document.activeElement).toBe(primaryActions[1]);
        });

        it('should focus primary action in previous row when pressing UP ARROW on primary action', () => {
          const lastIndex = primaryActions.length - 1;
          chips.last.focus();
          expect(document.activeElement).toBe(primaryActions[lastIndex]);

          dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', UP_ARROW);
          fixture.detectChanges();

          expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
        });

        it('should focus(trailing action in next row when pressing DOWN ARROW on(trailing action', () => {
          trailingActions[0].focus();
          expect(document.activeElement).toBe(trailingActions[0]);

          dispatchKeyboardEvent(trailingActions[0], 'keydown', DOWN_ARROW);
          fixture.detectChanges();

          expect(document.activeElement).toBe(trailingActions[1]);
        });

        it('should focus trailing action in previous row when pressing UP ARROW on trailing action', () => {
          const lastIndex = trailingActions.length - 1;
          trailingActions[lastIndex].focus();
          expect(document.activeElement).toBe(trailingActions[lastIndex]);

          dispatchKeyboardEvent(trailingActions[lastIndex], 'keydown', UP_ARROW);
          fixture.detectChanges();

          expect(document.activeElement).toBe(trailingActions[lastIndex - 1]);
        });
      });

      describe('RTL', () => {
        let fixture: ComponentFixture<StandardChipGrid>;

        beforeEach(() => {
          fixture = createComponent(StandardChipGrid, undefined, 'rtl');
        });

        it('should focus previous column when press RIGHT ARROW', () => {
          const lastIndex = primaryActions.length - 1;

          // Focus the first column of the last chip in the array
          chips.last.focus();
          expect(document.activeElement).toBe(primaryActions[lastIndex]);

          // Press the RIGHT arrow
          dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', RIGHT_ARROW);
          fixture.detectChanges();

          // It focuses the last column of the previous chip
          expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
        });

        it('should focus next column when press LEFT ARROW', () => {
          // Focus the first column of the first chip in the array
          chips.first.focus();
          expect(document.activeElement).toBe(primaryActions[0]);

          // Press the LEFT arrow
          dispatchKeyboardEvent(primaryActions[0], 'keydown', LEFT_ARROW);
          fixture.detectChanges();

          // It focuses the next column of the chip
          expect(document.activeElement).toBe(primaryActions[1]);
        });

        it('should allow focus to escape when tabbing away', fakeAsync(() => {
          let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
          let firstNativeChip = nativeChips[0] as HTMLElement;

          dispatchKeyboardEvent(firstNativeChip, 'keydown', TAB);

          expect(chipGridNativeElement.tabIndex)
            .withContext('Expected tabIndex to be set to -1 temporarily.')
            .toBe(-1);

          flush();

          expect(chipGridNativeElement.tabIndex)
            .withContext('Expected tabIndex to be reset back to 0')
            .toBe(0);
        }));

        it('should use user defined tabIndex', fakeAsync(() => {
          chipGridInstance.tabIndex = 4;
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();

          expect(chipGridNativeElement.tabIndex)
            .withContext('Expected tabIndex to be set to user defined value 4.')
            .toBe(4);

          let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
          let firstNativeChip = nativeChips[0] as HTMLElement;

          dispatchKeyboardEvent(firstNativeChip, 'keydown', TAB);
          expect(chipGridNativeElement.tabIndex)
            .withContext('Expected tabIndex to be set to -1 temporarily.')
            .toBe(-1);

          flush();

          expect(chipGridNativeElement.tabIndex)
            .withContext('Expected tabIndex to be reset back to 4')
            .toBe(4);
        }));
      });

      describe('keydown behavior', () => {
        let fixture: ComponentFixture<StandardChipGrid>;

        beforeEach(() => {
          fixture = createComponent(StandardChipGrid);
        });

        it('should account for the direction changing', () => {
          chips.first.focus();
          expect(document.activeElement).toBe(primaryActions[0]);

          dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
          fixture.detectChanges();

          expect(document.activeElement).toBe(primaryActions[1]);

          directionality.value = 'rtl';
          directionality.change.next('rtl');
          fixture.detectChanges();

          dispatchKeyboardEvent(primaryActions[1], 'keydown', RIGHT_ARROW);
          fixture.detectChanges();

          expect(document.activeElement).toBe(primaryActions[0]);
        });

        it('should move focus to the first chip when pressing HOME', () => {
          chips.last.focus();
          expect(document.activeElement).toBe(primaryActions[4]);

          const event = dispatchKeyboardEvent(primaryActions[4], 'keydown', HOME);
          fixture.detectChanges();

          expect(event.defaultPrevented).toBe(true);
          expect(document.activeElement).toBe(primaryActions[0]);
        });

        it('should move focus to the last chip when pressing END', () => {
          chips.first.focus();
          expect(document.activeElement).toBe(primaryActions[0]);

          const event = dispatchKeyboardEvent(primaryActions[0], 'keydown', END);
          fixture.detectChanges();

          expect(event.defaultPrevented).toBe(true);
          expect(document.activeElement).toBe(primaryActions[4]);
        });

        it('should ignore all non-tab navigation keyboard events from an editing chip', fakeAsync(() => {
          testComponent.editable = true;
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();

          chips.first.focus();
          expect(document.activeElement).toBe(primaryActions[0]);

          dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER);
          fixture.detectChanges();
          flush();

          const previousActiveElement = document.activeElement;
          const keysToIgnore = [HOME, END, LEFT_ARROW, RIGHT_ARROW];

          for (const key of keysToIgnore) {
            dispatchKeyboardEvent(document.activeElement!, 'keydown', key);
            fixture.detectChanges();
            flush();

            expect(document.activeElement).toBe(previousActiveElement);
          }
        }));
      });
    });
  });

  describe('FormFieldChipGrid', () => {
    let fixture: ComponentFixture<FormFieldChipGrid>;

    beforeEach(() => {
      fixture = createComponent(FormFieldChipGrid);
    });

    describe('keyboard behavior', () => {
      it('should maintain focus if the active chip is deleted', () => {
        const secondChip = fixture.nativeElement.querySelectorAll('.mat-mdc-chip')[1];
        const secondChipAction = secondChip.querySelector('.mdc-evolution-chip__action--primary');

        secondChipAction.focus();
        fixture.detectChanges();

        expect(chipGridInstance._chips.toArray().findIndex(chip => chip._hasFocus())).toBe(1);

        dispatchKeyboardEvent(secondChip, 'keydown', DELETE);
        fixture.detectChanges();

        expect(chipGridInstance._chips.toArray().findIndex(chip => chip._hasFocus())).toBe(1);
      });

      describe('when the input has focus', () => {
        it('should not focus the last chip when press DELETE', () => {
          let nativeInput = fixture.nativeElement.querySelector('input');

          // Focus the input
          nativeInput.focus();
          expectNoCellFocused();

          // Press the DELETE key
          dispatchKeyboardEvent(nativeInput, 'keydown', DELETE);
          fixture.detectChanges();

          // It doesn't focus the last chip
          expectNoCellFocused();
        });

        it('should focus the last chip when press BACKSPACE', () => {
          let nativeInput = fixture.nativeElement.querySelector('input');

          // Focus the input
          nativeInput.focus();
          expectNoCellFocused();

          // Press the BACKSPACE key
          dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE);
          fixture.detectChanges();

          // It focuses the last chip
          expectLastCellFocused();
        });

        it('should not focus the last chip when pressing BACKSPACE on a non-empty input', () => {
          const nativeInput = fixture.nativeElement.querySelector('input');
          nativeInput.value = 'hello';
          nativeInput.focus();
          fixture.detectChanges();

          expectNoCellFocused();

          dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE);
          fixture.detectChanges();

          expectNoCellFocused();
        });
      });
    });

    it('should complete the stateChanges stream on destroy', () => {
      const spy = jasmine.createSpy('stateChanges complete');
      const subscription = chipGridInstance.stateChanges.subscribe({complete: spy});

      fixture.destroy();
      expect(spy).toHaveBeenCalled();
      subscription.unsubscribe();
    });
  });

  describe('with chip remove', () => {
    let fixture: ComponentFixture<ChipGridWithRemove>;
    let trailingActions: NodeListOf<HTMLElement>;

    beforeEach(fakeAsync(() => {
      fixture = createComponent(ChipGridWithRemove);
      flush();
      trailingActions = chipGridNativeElement.querySelectorAll(
        '.mdc-evolution-chip__action--trailing',
      );
    }));

    it('should properly focus next item if chip is removed through click', fakeAsync(() => {
      const chip = chips.get(2)!;
      chip.focus();
      fixture.detectChanges();

      // Destroy the third focused chip by dispatching a bubbling click event on the
      // associated chip remove element.
      trailingActions[2].click();
      fixture.detectChanges();
      flush();

      expect(document.activeElement).toBe(primaryActions[3]);
    }));
  });

  describe('chip grid with chip input', () => {
    let fixture: ComponentFixture<InputChipGrid>;
    let nativeChips: HTMLElement[];
    let nativeInput: HTMLInputElement;
    let nativeChipGrid: HTMLElement;

    beforeEach(() => {
      fixture = createComponent(InputChipGrid);

      nativeChips = fixture.debugElement
        .queryAll(By.css('mat-chip-row'))
        .map(chip => chip.nativeElement);

      nativeChipGrid = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement;
      nativeInput = fixture.nativeElement.querySelector('input');
    });

    it('should take an initial view value with reactive forms', () => {
      fixture.componentInstance.control = new FormControl('[pizza-1]');
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(fixture.componentInstance.chipGrid.value).toEqual('[pizza-1]');
    });

    it('should set the view value from the form', () => {
      const chipGrid = fixture.componentInstance.chipGrid;

      expect(chipGrid.value).withContext('Expect chip grid to have no initial value').toBeFalsy();

      fixture.componentInstance.control.setValue('[pizza-1]');
      fixture.detectChanges();

      expect(chipGrid.value).toEqual('[pizza-1]');
    });

    it('should update the form value when the view changes', fakeAsync(() => {
      expect(fixture.componentInstance.control.value)
        .withContext(`Expected the control's value to be empty initially.`)
        .toEqual(null);

      nativeInput.focus();

      typeInElement(nativeInput, '123');
      fixture.detectChanges();
      dispatchKeyboardEvent(nativeInput, 'keydown', ENTER);
      fixture.detectChanges();
      flush();

      dispatchFakeEvent(nativeInput, 'blur');
      flush();

      expect(fixture.componentInstance.control.value).toContain('123-8');
    }));

    it('should clear the value when the control is reset', () => {
      fixture.componentInstance.control.setValue('pizza-1');
      fixture.detectChanges();

      fixture.componentInstance.control.reset();
      fixture.detectChanges();

      expect(fixture.componentInstance.chipGrid.value).toEqual(null);
    });

    it('should set the control to touched when the chip grid is touched', fakeAsync(() => {
      expect(fixture.componentInstance.control.touched)
        .withContext('Expected the control to start off as untouched.')
        .toBe(false);

      dispatchFakeEvent(nativeChipGrid, 'blur');
      tick();

      expect(fixture.componentInstance.control.touched)
        .withContext('Expected the control to be touched.')
        .toBe(true);
    }));

    it('should not set touched when a disabled chip grid is touched', fakeAsync(() => {
      expect(fixture.componentInstance.control.touched)
        .withContext('Expected the control to start off as untouched.')
        .toBe(false);

      fixture.componentInstance.control.disable();
      dispatchFakeEvent(nativeChipGrid, 'blur');
      tick();

      expect(fixture.componentInstance.control.touched)
        .withContext('Expected the control to stay untouched.')
        .toBe(false);
    }));

    it("should set the control to dirty when the chip grid's value changes in the DOM", fakeAsync(() => {
      expect(fixture.componentInstance.control.dirty)
        .withContext(`Expected control to start out pristine.`)
        .toEqual(false);

      nativeInput.focus();

      typeInElement(nativeInput, '123');
      fixture.detectChanges();
      dispatchKeyboardEvent(nativeInput, 'keydown', ENTER);
      fixture.detectChanges();
      flush();

      dispatchFakeEvent(nativeInput, 'blur');
      flush();

      expect(fixture.componentInstance.control.dirty)
        .withContext(`Expected control to be dirty after value was changed by user.`)
        .toEqual(true);
    }));

    it('should not set the control to dirty when the value changes programmatically', () => {
      expect(fixture.componentInstance.control.dirty)
        .withContext(`Expected control to start out pristine.`)
        .toEqual(false);

      fixture.componentInstance.control.setValue('pizza-1');

      expect(fixture.componentInstance.control.dirty)
        .withContext(`Expected control to stay pristine after programmatic change.`)
        .toEqual(false);
    });

    it('should set an asterisk after the placeholder if the control is required', () => {
      let requiredMarker = fixture.debugElement.query(
        By.css('.mat-mdc-form-field-required-marker'),
      )!;
      expect(requiredMarker)
        .withContext(`Expected placeholder not to have an asterisk, as control was not required.`)
        .toBeNull();

      fixture.componentInstance.chipGrid.required = true;
      fixture.detectChanges();

      requiredMarker = fixture.debugElement.query(By.css('.mat-mdc-form-field-required-marker'))!;
      expect(requiredMarker)
        .not.withContext(`Expected placeholder to have an asterisk, as control was required.`)
        .toBeNull();
    });

    it('should mark the component as required if the control has a required validator', () => {
      fixture.destroy();
      fixture = TestBed.createComponent(InputChipGrid);
      fixture.componentInstance.control = new FormControl('', [Validators.required]);
      fixture.detectChanges();

      expect(
        fixture.nativeElement.querySelector('.mat-mdc-form-field-required-marker'),
      ).toBeTruthy();
    });

    it('should blur the form field when the active chip is blurred', fakeAsync(() => {
      const formField: HTMLElement = fixture.nativeElement.querySelector('.mat-mdc-form-field');
      const firstAction = nativeChips[0].querySelector('.mat-mdc-chip-action') as HTMLElement;

      patchElementFocus(firstAction);
      firstAction.focus();
      fixture.detectChanges();

      expect(formField.classList).toContain('mat-focused');

      firstAction.blur();
      fixture.detectChanges();
      fixture.detectChanges();
      fixture.detectChanges();
      flush();

      expect(formField.classList).not.toContain('mat-focused');
    }));

    it('should keep focus on the input after adding the first chip', fakeAsync(() => {
      const chipEls = Array.from<HTMLElement>(
        fixture.nativeElement.querySelectorAll('mat-chip-row'),
      ).reverse();

      // Remove the chips via backspace to simulate the user removing them.
      chipEls.forEach(chip => {
        chip.focus();
        dispatchKeyboardEvent(chip, 'keydown', BACKSPACE);
        fixture.detectChanges();
        tick();
      });

      nativeInput.focus();
      expect(fixture.componentInstance.foods)
        .withContext('Expected all chips to be removed.')
        .toEqual([]);
      expect(document.activeElement).withContext('Expected input to be focused.').toBe(nativeInput);

      typeInElement(nativeInput, '123');
      fixture.detectChanges();
      dispatchKeyboardEvent(nativeInput, 'keydown', ENTER);
      fixture.detectChanges();
      tick();

      expect(document.activeElement)
        .withContext('Expected input to remain focused.')
        .toBe(nativeInput);
    }));

    it('should set aria-invalid if the form field is invalid', fakeAsync(() => {
      fixture.componentInstance.control = new FormControl('', [Validators.required]);
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const input: HTMLInputElement = fixture.nativeElement.querySelector('input');

      expect(input.getAttribute('aria-invalid')).toBe('true');

      typeInElement(input, '123');
      fixture.detectChanges();
      dispatchKeyboardEvent(input, 'keydown', ENTER);
      fixture.detectChanges();
      flush();

      dispatchFakeEvent(input, 'blur');
      flush();

      fixture.detectChanges();
      expect(input.getAttribute('aria-invalid')).toBe('false');
    }));

    describe('when the input has focus', () => {
      beforeEach(() => {
        nativeInput.focus();
        expectNoCellFocused();
      });

      it('should not focus the last chip when pressing DELETE', () => {
        dispatchKeyboardEvent(nativeInput, 'keydown', DELETE);
        expectNoCellFocused();
      });

      it('should focus the last chip when pressing BACKSPACE when input is empty', () => {
        dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE);
        expectLastCellFocused();
      });

      it('should not focus the last chip when the BACKSPACE key is being repeated', () => {
        // Only now should it focus the last element
        const event = createKeyboardEvent('keydown', BACKSPACE);
        Object.defineProperty(event, 'repeat', {
          get: () => true,
        });
        dispatchEvent(nativeInput, event);
        expectNoCellFocused();
      });

      it('should focus last chip after pressing BACKSPACE after creating a chip', () => {
        // Create a chip
        typeInElement(nativeInput, '123');
        dispatchKeyboardEvent(nativeInput, 'keydown', ENTER);

        expectNoCellFocused();

        dispatchKeyboardEvent(nativeInput, 'keydown', BACKSPACE);
        expectLastCellFocused();
      });
    });
  });

  describe('error messages', () => {
    let fixture: ComponentFixture<ChipGridWithFormErrorMessages>;
    let errorTestComponent: ChipGridWithFormErrorMessages;
    let containerEl: HTMLElement;
    let chipGridEl: HTMLElement;
    let inputEl: HTMLElement;

    beforeEach(fakeAsync(() => {
      fixture = createComponent(ChipGridWithFormErrorMessages);
      flush();
      fixture.detectChanges();

      errorTestComponent = fixture.componentInstance;
      containerEl = fixture.debugElement.query(By.css('mat-form-field'))!.nativeElement;
      chipGridEl = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement;
      inputEl = fixture.debugElement.query(By.css('input'))!.nativeElement;
    }));

    it('should not show any errors if the user has not interacted', () => {
      expect(errorTestComponent.formControl.untouched)
        .withContext('Expected untouched form control')
        .toBe(true);
      expect(containerEl.querySelectorAll('mat-error').length)
        .withContext('Expected no error message')
        .toBe(0);
      expect(chipGridEl.getAttribute('aria-invalid'))
        .withContext('Expected aria-invalid to be set to "false".')
        .toBe('false');
    });

    it('should display an error message when the grid is touched and invalid', fakeAsync(() => {
      expect(errorTestComponent.formControl.invalid)
        .withContext('Expected form control to be invalid')
        .toBe(true);
      expect(containerEl.querySelectorAll('mat-error').length)
        .withContext('Expected no error message')
        .toBe(0);

      errorTestComponent.formControl.markAsTouched();
      fixture.detectChanges();
      tick();

      expect(containerEl.classList)
        .withContext('Expected container to have the invalid CSS class.')
        .toContain('mat-form-field-invalid');
      expect(containerEl.querySelectorAll('mat-error').length)
        .withContext('Expected one error message to have been rendered.')
        .toBe(1);
      expect(chipGridEl.getAttribute('aria-invalid'))
        .withContext('Expected aria-invalid to be set to "true".')
        .toBe('true');
    }));

    it('should display an error message when the parent form is submitted', fakeAsync(() => {
      expect(errorTestComponent.form.submitted)
        .withContext('Expected form not to have been submitted')
        .toBe(false);
      expect(errorTestComponent.formControl.invalid)
        .withContext('Expected form control to be invalid')
        .toBe(true);
      expect(containerEl.querySelectorAll('mat-error').length)
        .withContext('Expected no error message')
        .toBe(0);

      dispatchFakeEvent(fixture.debugElement.query(By.css('form'))!.nativeElement, 'submit');
      flush();
      fixture.detectChanges();

      fixture.whenStable().then(() => {
        expect(errorTestComponent.form.submitted)
          .withContext('Expected form to have been submitted')
          .toBe(true);
        expect(containerEl.classList)
          .withContext('Expected container to have the invalid CSS class.')
          .toContain('mat-form-field-invalid');
        expect(containerEl.querySelectorAll('mat-error').length)
          .withContext('Expected one error message to have been rendered.')
          .toBe(1);
        expect(chipGridEl.getAttribute('aria-invalid'))
          .withContext('Expected aria-invalid to be set to "true".')
          .toBe('true');
      });
      flush();
    }));

    it('should hide the errors and show the hints once the chip grid becomes valid', fakeAsync(() => {
      errorTestComponent.formControl.markAsTouched();
      flush();
      fixture.detectChanges();

      fixture.whenStable().then(() => {
        expect(containerEl.classList)
          .withContext('Expected container to have the invalid CSS class.')
          .toContain('mat-form-field-invalid');
        expect(containerEl.querySelectorAll('mat-error').length)
          .withContext('Expected one error message to have been rendered.')
          .toBe(1);
        expect(containerEl.querySelectorAll('mat-hint').length)
          .withContext('Expected no hints to be shown.')
          .toBe(0);

        errorTestComponent.formControl.setValue('something');
        flush();
        fixture.detectChanges();

        fixture.whenStable().then(() => {
          expect(containerEl.classList).not.toContain(
            'mat-form-field-invalid',
            'Expected container not to have the invalid class when valid.',
          );
          expect(containerEl.querySelectorAll('mat-error').length)
            .withContext('Expected no error messages when the input is valid.')
            .toBe(0);
          expect(containerEl.querySelectorAll('mat-hint').length)
            .withContext('Expected one hint to be shown once the input is valid.')
            .toBe(1);
        });

        flush();
      });
    }));

    it('should set the proper aria-live attribute on the error messages', () => {
      errorTestComponent.formControl.markAsTouched();
      fixture.detectChanges();

      expect(containerEl.querySelector('mat-error')!.getAttribute('aria-live')).toBe('polite');
    });

    it('sets the aria-describedby on the input to reference errors when in error state', fakeAsync(() => {
      let hintId = fixture.debugElement
        .query(By.css('.mat-mdc-form-field-hint'))!
        .nativeElement.getAttribute('id');
      let describedBy = inputEl.getAttribute('aria-describedby');

      expect(hintId).withContext('hint should be shown').toBeTruthy();
      expect(describedBy).toBe(hintId);

      fixture.componentInstance.formControl.markAsTouched();
      fixture.detectChanges();

      // Flush the describedby timer and detect changes caused by it.
      flush();
      fixture.detectChanges();

      let errorIds = fixture.debugElement
        .queryAll(By.css('.mat-mdc-form-field-error'))
        .map(el => el.nativeElement.getAttribute('id'))
        .join(' ');
      let errorDescribedBy = inputEl.getAttribute('aria-describedby');

      expect(errorIds).withContext('errors should be shown').toBeTruthy();
      expect(errorDescribedBy).toBe(errorIds);
    }));
  });

  function createComponent<T>(
    component: Type<T>,
    animationsModule:
      | Type<NoopAnimationsModule>
      | Type<BrowserAnimationsModule> = NoopAnimationsModule,
    direction: Direction = 'ltr',
  ): ComponentFixture<T> {
    directionality = {
      value: direction,
      change: new EventEmitter<Direction>(),
    } as Directionality;

    TestBed.configureTestingModule({
      imports: [
        FormsModule,
        ReactiveFormsModule,
        MatChipsModule,
        MatFormFieldModule,
        MatInputModule,
        animationsModule,
      ],
      providers: [{provide: Directionality, useValue: directionality}],
      declarations: [component],
    });

    const fixture = TestBed.createComponent<T>(component);
    fixture.detectChanges();

    chipGridDebugElement = fixture.debugElement.query(By.directive(MatChipGrid))!;
    chipGridNativeElement = chipGridDebugElement.nativeElement;
    chipGridInstance = chipGridDebugElement.componentInstance;
    testComponent = fixture.debugElement.componentInstance;
    chips = chipGridInstance._chips;
    primaryActions = chipGridNativeElement.querySelectorAll<HTMLElement>(
      '.mdc-evolution-chip__action--primary',
    );

    return fixture;
  }
});

@Component({
  template: `
    <mat-chip-grid [tabIndex]="tabIndex" [role]="role" #chipGrid>
      @for (i of chips; track i) {
        <mat-chip-row [editable]="editable">{{name}} {{i + 1}}</mat-chip-row>
      }
    </mat-chip-grid>
    <input name="test" [matChipInputFor]="chipGrid"/>`,
  standalone: false,
})
class StandardChipGrid {
  name: string = 'Test';
  tabIndex: number = 0;
  chips = [0, 1, 2, 3, 4];
  editable = false;
  role: string | null = null;
}

@Component({
  template: `
    <mat-form-field>
      <mat-label>Add a chip</mat-label>
      <mat-chip-grid #chipGrid>
        @for (chip of chips; track chip) {
          <mat-chip-row (removed)="remove(chip)">{{chip}}</mat-chip-row>
        }
      </mat-chip-grid>
      <input name="test" [matChipInputFor]="chipGrid"/>
    </mat-form-field>
  `,
  standalone: false,
})
class FormFieldChipGrid {
  chips = ['Chip 0', 'Chip 1', 'Chip 2'];

  remove(chip: string) {
    const index = this.chips.indexOf(chip);

    if (index > -1) {
      this.chips.splice(index, 1);
    }
  }
}

@Component({
  template: `
    <mat-form-field>
      <mat-label>New food...</mat-label>
      <mat-chip-grid #chipGrid placeholder="Food" [formControl]="control">
        @for (food of foods; track food) {
          <mat-chip-row [value]="food.value" (removed)="remove(food)">
            {{ food.viewValue }}
          </mat-chip-row>
        }
      </mat-chip-grid>
      <input
          [matChipInputFor]="chipGrid"
          [matChipInputSeparatorKeyCodes]="separatorKeyCodes"
          [matChipInputAddOnBlur]="addOnBlur"
          (matChipInputTokenEnd)="add($event)"/>
    </mat-form-field>
  `,
  standalone: false,
})
class InputChipGrid {
  foods: any[] = [
    {value: 'steak-0', viewValue: 'Steak'},
    {value: 'pizza-1', viewValue: 'Pizza'},
    {value: 'tacos-2', viewValue: 'Tacos', disabled: true},
    {value: 'sandwich-3', viewValue: 'Sandwich'},
    {value: 'chips-4', viewValue: 'Chips'},
    {value: 'eggs-5', viewValue: 'Eggs'},
    {value: 'pasta-6', viewValue: 'Pasta'},
    {value: 'sushi-7', viewValue: 'Sushi'},
  ];
  control = new FormControl<string | null>(null);

  separatorKeyCodes = [ENTER, SPACE];
  addOnBlur: boolean = true;

  add(event: MatChipInputEvent): void {
    const value = (event.value || '').trim();

    // Add our foods
    if (value) {
      this.foods.push({
        value: `${value.toLowerCase()}-${this.foods.length}`,
        viewValue: value,
      });
    }

    // Reset the input value
    event.chipInput!.clear();
  }

  remove(food: any): void {
    const index = this.foods.indexOf(food);

    if (index > -1) {
      this.foods.splice(index, 1);
    }
  }

  @ViewChild(MatChipGrid) chipGrid: MatChipGrid;
  @ViewChildren(MatChipRow) chips: QueryList<MatChipRow>;
}

@Component({
  template: `
<form #form="ngForm" novalidate>
  <mat-form-field>
    <mat-chip-grid #chipGrid [formControl]="formControl">
      @for (food of foods; track food) {
        <mat-chip-row [value]="food.value">{{food.viewValue}}</mat-chip-row>
      }
    </mat-chip-grid>
    <input name="test" [matChipInputFor]="chipGrid"/>
    <mat-hint>Please select a chip, or type to add a new chip</mat-hint>
    <mat-error>Should have value</mat-error>
  </mat-form-field>
</form>
  `,
  standalone: false,
})
class ChipGridWithFormErrorMessages {
  foods: any[] = [
    {value: 0, viewValue: 'Steak'},
    {value: 1, viewValue: 'Pizza'},
    {value: 2, viewValue: 'Pasta'},
  ];
  @ViewChildren(MatChipRow) chips: QueryList<MatChipRow>;

  @ViewChild('form') form: NgForm;
  formControl = new FormControl('', Validators.required);

  private readonly _changeDetectorRef = inject(ChangeDetectorRef);

  constructor() {
    this.formControl.events.pipe(takeUntilDestroyed()).subscribe(() => {
      this._changeDetectorRef.markForCheck();
    });
  }
}

@Component({
  template: `
    <mat-chip-grid #chipGrid>
      @for (i of numbers; track i) {
        <mat-chip-row (removed)="remove(i)">{{i}}</mat-chip-row>
      }
      <input name="test" [matChipInputFor]="chipGrid"/>
    </mat-chip-grid>`,
  animations: [
    // For the case we're testing this animation doesn't
    // have to be used anywhere, it just has to be defined.
    trigger('dummyAnimation', [
      transition(':leave', [style({opacity: 0}), animate('500ms', style({opacity: 1}))]),
    ]),
  ],
  standalone: false,
})
class StandardChipGridWithAnimations {
  numbers = [0, 1, 2, 3, 4];

  remove(item: number): void {
    const index = this.numbers.indexOf(item);

    if (index > -1) {
      this.numbers.splice(index, 1);
    }
  }
}

@Component({
  template: `
    <mat-form-field>
      <mat-chip-grid #chipGrid>
        @for (i of chips; track i) {
          <mat-chip-row [value]="i" (removed)="removeChip($event)">
            Chip {{i + 1}}
            <span matChipRemove>Remove</span>
          </mat-chip-row>
        }
      </mat-chip-grid>
      <input name="test" [matChipInputFor]="chipGrid"/>
    </mat-form-field>
  `,
  standalone: false,
})
class ChipGridWithRemove {
  chips = [0, 1, 2, 3, 4];

  removeChip(event: MatChipEvent) {
    this.chips.splice(event.chip.value, 1);
  }
}
