import {Direction, Directionality} from '@angular/cdk/bidi';
import {END, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@angular/cdk/keycodes';
import {
  dispatchFakeEvent,
  dispatchKeyboardEvent,
  patchElementFocus,
} from '@angular/cdk/testing/private';
import {
  Component,
  DebugElement,
  EventEmitter,
  QueryList,
  Type,
  ViewChild,
  ViewChildren,
} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {MatChipListbox, MatChipOption, MatChipsModule} from './index';

describe('MatChipListbox', () => {
  let fixture: ComponentFixture<any>;
  let chipListboxDebugElement: DebugElement;
  let chipListboxNativeElement: HTMLElement;
  let chipListboxInstance: MatChipListbox;
  let testComponent: StandardChipListbox;
  let chips: QueryList<MatChipOption>;
  let directionality: {value: Direction; change: EventEmitter<Direction>};
  let primaryActions: NodeListOf<HTMLElement>;

  describe('StandardChipList', () => {
    describe('basic behaviors', () => {
      beforeEach(() => {
        createComponent(StandardChipListbox);
      });

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

      it('should not have the aria-selected attribute when it is not selectable', fakeAsync(() => {
        testComponent.selectable = false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        tick();

        const chipsValid = chips
          .toArray()
          .every(
            chip =>
              !chip.selectable && !chip._elementRef.nativeElement.hasAttribute('aria-selected'),
          );

        expect(chipsValid).toBe(true);
      }));

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

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

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

        chipListboxInstance.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 listbox became disabled', fakeAsync(() => {
        expect(chips.toArray().every(chip => chip.disabled)).toBe(false);

        chipListboxInstance.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(chipListboxNativeElement.hasAttribute('role')).toBe(false);
      });

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

        expect(chipListboxNativeElement.getAttribute('role')).toBe('grid');
      });

      it('should not set aria-required when it does not have a role', () => {
        testComponent.chips = [];
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(chipListboxNativeElement.hasAttribute('role')).toBe(false);
        expect(chipListboxNativeElement.hasAttribute('aria-required')).toBe(false);
      });

      it('should toggle the chips disabled state based on whether it is disabled', fakeAsync(() => {
        fixture.destroy();
        TestBed.resetTestingModule();
        const disabledFixture = createComponent(IndividuallyDisabledChipInsideForm);
        disabledFixture.detectChanges();
        flush();
        expect(disabledFixture.componentInstance.chip.disabled).toBe(true);
      }));
    });

    describe('with selected chips', () => {
      beforeEach(() => {
        fixture = createComponent(SelectedChipListbox);
      });

      it('should not override chips selected', () => {
        const instanceChips = fixture.componentInstance.chips.toArray();

        expect(instanceChips[0].selected)
          .withContext('Expected first option to be selected.')
          .toBe(true);
        expect(instanceChips[1].selected)
          .withContext('Expected second option to be not selected.')
          .toBe(false);
        expect(instanceChips[2].selected)
          .withContext('Expected third option to be selected.')
          .toBe(true);
      });

      it('should have role listbox', () => {
        expect(chipListboxNativeElement.getAttribute('role')).toBe('listbox');
      });

      it('should not have role when empty', () => {
        fixture.componentInstance.foods = [];
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(chipListboxNativeElement.getAttribute('role'))
          .withContext('Expect no role attribute')
          .toBeNull();
      });
    });

    describe('focus behaviors', () => {
      beforeEach(() => {
        createComponent(StandardChipListbox);
      });

      it('should focus the first chip on focus', () => {
        chipListboxInstance.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(chipListboxInstance.focused)
          .withContext('Expected listbox to not be focused.')
          .toBe(false);

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

        chipListboxInstance.focus();
        fixture.detectChanges();

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

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

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

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

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

          // Focus the middle item
          patchElementFocus(midItem.primaryAction!._elementRef.nativeElement);
          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
          patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement);
          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 listbox 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();
          tick();

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

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

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

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

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

    describe('keyboard behavior', () => {
      describe('LTR (default)', () => {
        beforeEach(() => {
          createComponent(StandardChipListbox);
        });

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

          // Focus the last item 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 next-to-last item
          expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
        });

        it('should focus next item when press RIGHT ARROW', () => {
          // Focus the last item 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-to-last item
          expect(document.activeElement).toBe(primaryActions[1]);
        });

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

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

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

        it('should focus the first item when pressing HOME', () => {
          const lastAction = primaryActions[primaryActions.length - 1];
          chips.last.focus();
          expect(document.activeElement).toBe(lastAction);

          const event = dispatchKeyboardEvent(lastAction, 'keydown', HOME);
          fixture.detectChanges();

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

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

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

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

      describe('RTL', () => {
        beforeEach(() => {
          createComponent(StandardChipListbox, 'rtl');
        });

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

          // Focus the last item 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 next-to-last item
          expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
        });

        it('should focus next item when press LEFT ARROW', () => {
          // Focus the last item 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-to-last item
          expect(document.activeElement).toBe(primaryActions[1]);
        });

        it('should allow focus to escape when tabbing away', fakeAsync(() => {
          dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', TAB);

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

          flush();

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

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

          fixture.detectChanges();

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

          dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', TAB);

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

          flush();

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

      it('should account for the direction changing', () => {
        createComponent(StandardChipListbox);

        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]);
      });
    });

    describe('selection logic', () => {
      it('should remove selection if chip has been removed', fakeAsync(() => {
        fixture = createComponent(BasicChipListbox);
        const instanceChips = fixture.componentInstance.chips;
        const chipListbox = fixture.componentInstance.chipListbox;
        dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE);
        fixture.detectChanges();

        expect(instanceChips.first.selected)
          .withContext('Expected first option to be selected.')
          .toBe(true);
        expect(chipListbox.selected)
          .withContext('Expected first option to be selected.')
          .toBe(chips.first);

        fixture.componentInstance.foods = [];
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        tick();

        expect(chipListbox.selected)
          .withContext('Expected selection to be removed when option no longer exists.')
          .toBe(undefined);
      }));

      it('should select an option that was added after initialization', () => {
        fixture = createComponent(BasicChipListbox);
        fixture.componentInstance.foods.push({viewValue: 'Potatoes', value: 'potatoes-8'});
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>(
          '.mdc-evolution-chip__action--primary',
        );

        dispatchKeyboardEvent(primaryActions[8], 'keydown', SPACE);
        fixture.detectChanges();

        expect(fixture.componentInstance.chipListbox.value)
          .withContext('Expect value contain the value of the last option')
          .toContain('potatoes-8');
        expect(fixture.componentInstance.chips.last.selected)
          .withContext('Expect last option selected')
          .toBeTruthy();
      });

      it('should not select disabled chips', () => {
        fixture = createComponent(BasicChipListbox);
        const array = chips.toArray();
        dispatchKeyboardEvent(primaryActions[2], 'keydown', SPACE);
        fixture.detectChanges();

        expect(fixture.componentInstance.chipListbox.value)
          .withContext('Expect value to be undefined')
          .toBeUndefined();
        expect(array[2].selected).withContext('Expect disabled chip not selected').toBeFalsy();
        expect(fixture.componentInstance.chipListbox.selected)
          .withContext('Expect no selected chips')
          .toBeUndefined();
      });

      it('should not select when is not selectable', fakeAsync(() => {
        const falsyFixture = createComponent(FalsyBasicChipListbox);
        falsyFixture.detectChanges();
        tick();
        falsyFixture.detectChanges();

        const chipListboxElement = falsyFixture.debugElement.query(By.directive(MatChipListbox))!;
        const _chips = chipListboxElement.componentInstance._chips;
        const nativeChips = (
          chipListboxElement.nativeElement as HTMLElement
        ).querySelectorAll<HTMLElement>('.mdc-evolution-chip__action--primary');

        expect(_chips.first.selected)
          .withContext('Expected first option not to be selected')
          .toBe(false);

        dispatchKeyboardEvent(nativeChips[0], 'keydown', SPACE);
        falsyFixture.detectChanges();
        flush();

        expect(_chips.first.selected)
          .withContext('Expected first option not to be selected.')
          .toBe(false);
      }));

      it('should set `aria-selected` based on the selection state in single selection mode', fakeAsync(() => {
        const getAriaSelected = () =>
          Array.from(primaryActions).map(action => action.getAttribute('aria-selected'));

        fixture = createComponent(BasicChipListbox);
        // Use a shorter list so we can keep the assertions smaller
        fixture.componentInstance.foods = [
          {value: 'steak-0', viewValue: 'Steak'},
          {value: 'pizza-1', viewValue: 'Pizza'},
          {value: 'tacos-2', viewValue: 'Tacos'},
        ];
        fixture.componentInstance.selectable = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>(
          '.mdc-evolution-chip__action--primary',
        );

        expect(getAriaSelected()).toEqual(['false', 'false', 'false']);

        primaryActions[1].click();
        fixture.detectChanges();
        flush();
        expect(getAriaSelected()).toEqual(['false', 'true', 'false']);

        primaryActions[2].click();
        fixture.detectChanges();
        flush();
        expect(getAriaSelected()).toEqual(['false', 'false', 'true']);

        primaryActions[0].click();
        fixture.detectChanges();
        flush();
        expect(getAriaSelected()).toEqual(['true', 'false', 'false']);
      }));

      it('should set `aria-selected` based on the selection state in multi-selection mode', fakeAsync(() => {
        const getAriaSelected = () =>
          Array.from(primaryActions).map(action => action.getAttribute('aria-selected'));

        fixture = createComponent(MultiSelectionChipListbox);
        fixture.detectChanges();

        // Use a shorter list so we can keep the assertions smaller
        fixture.componentInstance.foods = [
          {value: 'steak-0', viewValue: 'Steak'},
          {value: 'pizza-1', viewValue: 'Pizza'},
          {value: 'tacos-2', viewValue: 'Tacos'},
        ];
        fixture.componentInstance.selectable = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>(
          '.mdc-evolution-chip__action--primary',
        );

        expect(getAriaSelected()).toEqual(['false', 'false', 'false']);

        primaryActions[1].click();
        fixture.detectChanges();
        flush();
        expect(getAriaSelected()).toEqual(['false', 'true', 'false']);

        primaryActions[2].click();
        fixture.detectChanges();
        flush();
        expect(getAriaSelected()).toEqual(['false', 'true', 'true']);

        primaryActions[0].click();
        fixture.detectChanges();
        flush();
        expect(getAriaSelected()).toEqual(['true', 'true', 'true']);

        primaryActions[1].click();
        fixture.detectChanges();
        flush();
        expect(getAriaSelected()).toEqual(['true', 'false', 'true']);
      }));
    });

    describe('chip list with chip input', () => {
      describe('single selection', () => {
        beforeEach(() => {
          fixture = createComponent(BasicChipListbox);
        });

        it('should take an initial view value with reactive forms', fakeAsync(() => {
          fixture.componentInstance.control = new FormControl('pizza-1');
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();
          tick();
          const array = chips.toArray();

          expect(array[1].selected).withContext('Expect pizza-1 chip to be selected').toBeTruthy();

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

          expect(array[1].selected)
            .withContext('Expect chip to be not selected after toggle selected')
            .toBeFalsy();
        }));

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

          expect(chipListbox.value)
            .withContext('Expect chip listbox to have no initial value')
            .toBeFalsy();

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

          expect(array[1].selected).withContext('Expect chip to be selected').toBeTruthy();
        });

        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);

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

          expect(fixture.componentInstance.control.value)
            .withContext(`Expected control's value to be set to the new option.`)
            .toEqual('steak-0');
        }));

        it('should clear the selection when a nonexistent option value is selected', () => {
          const array = chips.toArray();

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

          expect(array[1].selected)
            .withContext(`Expected chip with the value to be selected.`)
            .toBeTruthy();

          fixture.componentInstance.control.setValue('gibberish');

          fixture.detectChanges();

          expect(array[1].selected)
            .withContext(`Expected chip with the old value not to be selected.`)
            .toBeFalsy();
        });

        it('should clear the selection when the control is reset', () => {
          const array = chips.toArray();

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

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

          expect(array[1].selected)
            .withContext(`Expected chip with the old value not to be selected.`)
            .toBeFalsy();
        });

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

          const nativeChipListbox = fixture.debugElement.query(
            By.css('mat-chip-listbox'),
          )!.nativeElement;
          dispatchFakeEvent(nativeChipListbox, '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 listbox is touched', fakeAsync(() => {
          expect(fixture.componentInstance.control.touched)
            .withContext('Expected the control to start off as untouched.')
            .toBe(false);

          fixture.componentInstance.control.disable();
          const nativeChipListbox = fixture.debugElement.query(
            By.css('mat-chip-listbox'),
          )!.nativeElement;
          dispatchFakeEvent(nativeChipListbox, '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 listbox's value changes in the DOM", () => {
          expect(fixture.componentInstance.control.dirty)
            .withContext(`Expected control to start out pristine.`)
            .toEqual(false);

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

          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 be able to programmatically select a falsy option', () => {
          fixture.destroy();
          TestBed.resetTestingModule();

          const falsyFixture = createComponent(FalsyValueChipListbox);
          falsyFixture.componentInstance.control.setValue([0]);
          falsyFixture.detectChanges();
          falsyFixture.detectChanges();

          expect(falsyFixture.componentInstance.chips.first.selected)
            .withContext('Expected first option to be selected')
            .toBe(true);
        });

        it('should not focus the active chip when the value is set programmatically', () => {
          const chipArray = fixture.componentInstance.chips.toArray();

          spyOn(chipArray[4], 'focus').and.callThrough();

          fixture.componentInstance.control.setValue('chips-4');
          fixture.detectChanges();

          expect(chipArray[4].focus).not.toHaveBeenCalled();
        });
      });

      describe('multiple selection', () => {
        it('should take an initial view value with reactive forms', fakeAsync(() => {
          fixture = createComponent(MultiSelectionChipListbox, undefined, initFixture => {
            initFixture.componentInstance.control = new FormControl(['pizza-1', 'pasta-6']);
            initFixture.componentInstance.selectable = true;
          });
          fixture.detectChanges();
          flush();

          const array = fixture.componentInstance.chips.toArray();

          expect(array[1].selected).withContext('Expect pizza-1 chip to be selected').toBe(true);
          expect(array[6].selected).withContext('Expect pasta-6 chip to be selected').toBe(true);

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

          expect(array[1].selected)
            .withContext('Expect pizza-1 chip to no longer be selected')
            .toBe(false);
          expect(array[6].selected)
            .withContext('Expect pasta-6 chip to remain selected')
            .toBe(true);
        }));

        it('should set the view value from the form', () => {
          fixture = createComponent(MultiSelectionChipListbox);
          const chipListbox = fixture.componentInstance.chipListbox;
          const array = fixture.componentInstance.chips.toArray();

          expect(chipListbox.value)
            .withContext('Expect chip listbox to have no initial value')
            .toBeFalsy();

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

          expect(array[1].selected).withContext('Expect chip to be selected').toBeTruthy();
        });

        it('should update the form value when the view changes', () => {
          fixture = createComponent(MultiSelectionChipListbox);

          expect(fixture.componentInstance.control.value)
            .withContext(`Expected the control's value to be empty initially.`)
            .toEqual(null);

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

          expect(fixture.componentInstance.control.value)
            .withContext(`Expected control's value to be set to the new option.`)
            .toEqual(['steak-0']);
        });

        it('should clear the selection when a nonexistent option value is selected', () => {
          fixture = createComponent(MultiSelectionChipListbox);
          chips = fixture.componentInstance.chips;

          const array = fixture.componentInstance.chips.toArray();
          fixture.componentInstance.control.setValue(['pizza-1']);
          fixture.detectChanges();

          expect(array[1].selected)
            .withContext(`Expected chip with the value to be selected.`)
            .toBeTruthy();

          fixture.componentInstance.control.setValue(['gibberish']);

          fixture.detectChanges();

          expect(array[1].selected)
            .withContext(`Expected chip with the old value not to be selected.`)
            .toBeFalsy();
        });

        it('should clear the selection when the control is reset', () => {
          fixture = createComponent(MultiSelectionChipListbox);
          const array = fixture.componentInstance.chips.toArray();

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

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

          expect(array[1].selected)
            .withContext(`Expected chip with the old value not to be selected.`)
            .toBeFalsy();
        });
      });
    });
  });

  function createComponent<T>(
    component: Type<T>,
    direction: Direction = 'ltr',
    beforeInitialChangeDetection?: (fixture: ComponentFixture<T>) => void,
  ): ComponentFixture<T> {
    directionality = {
      value: direction,
      change: new EventEmitter(),
    };

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

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

    chipListboxDebugElement = fixture.debugElement.query(By.directive(MatChipListbox))!;
    chipListboxNativeElement = chipListboxDebugElement.nativeElement;
    chipListboxInstance = chipListboxDebugElement.componentInstance;
    testComponent = fixture.debugElement.componentInstance;
    chips = chipListboxInstance._chips;
    primaryActions = chipListboxNativeElement.querySelectorAll<HTMLElement>(
      '.mdc-evolution-chip__action--primary',
    );

    return fixture;
  }
});

@Component({
  template: `
    <mat-chip-listbox [tabIndex]="tabIndex" [selectable]="selectable" [role]="role">
      @for (i of chips; track i) {
        <mat-chip-option (select)="chipSelect(i)" (deselect)="chipDeselect(i)">
          {{name}} {{i + 1}}
        </mat-chip-option>
      }
    </mat-chip-listbox>`,
  standalone: false,
})
class StandardChipListbox {
  name: string = 'Test';
  selectable: boolean = true;
  chipSelect: (index?: number) => void = () => {};
  chipDeselect: (index?: number) => void = () => {};
  tabIndex: number = 0;
  chips = [0, 1, 2, 3, 4];
  role: string | null = null;
}

@Component({
  template: `
      <mat-chip-listbox [formControl]="control" [required]="isRequired"
        [tabIndex]="tabIndexOverride" [selectable]="selectable">
        @for (food of foods; track food) {
          <mat-chip-option [value]="food.value" [disabled]="food.disabled">
            {{ food.viewValue }}
          </mat-chip-option>
        }
      </mat-chip-listbox>
  `,
  standalone: false,
})
class BasicChipListbox {
  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);
  isRequired: boolean;
  tabIndexOverride: number;
  selectable: boolean;

  @ViewChild(MatChipListbox) chipListbox: MatChipListbox;
  @ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}

@Component({
  template: `
      <mat-chip-listbox [multiple]="true" [formControl]="control"
        [required]="isRequired"
        [tabIndex]="tabIndexOverride" [selectable]="selectable">
        @for (food of foods; track food) {
          <mat-chip-option [value]="food.value" [disabled]="food.disabled">
            {{ food.viewValue }}
          </mat-chip-option>
        }
      </mat-chip-listbox>
  `,
  standalone: false,
})
class MultiSelectionChipListbox {
  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);
  isRequired: boolean;
  tabIndexOverride: number;
  selectable: boolean;

  @ViewChild(MatChipListbox) chipListbox: MatChipListbox;
  @ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}

@Component({
  template: `
      <mat-chip-listbox [formControl]="control">
        @for (food of foods; track food) {
          <mat-chip-option [value]="food.value">{{ food.viewValue }}</mat-chip-option>
        }
      </mat-chip-listbox>
  `,
  standalone: false,
})
class FalsyValueChipListbox {
  foods: any[] = [
    {value: 0, viewValue: 'Steak'},
    {value: 1, viewValue: 'Pizza'},
  ];
  control = new FormControl([] as number[]);
  @ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}

@Component({
  template: `
    <mat-chip-listbox>
      @for (food of foods; track food) {
        <mat-chip-option [value]="food.value" [selected]="food.selected">
            {{ food.viewValue }}
        </mat-chip-option>
      }
    </mat-chip-listbox>
  `,
  standalone: false,
})
class SelectedChipListbox {
  foods: any[] = [
    {value: 0, viewValue: 'Steak', selected: true},
    {value: 1, viewValue: 'Pizza', selected: false},
    {value: 2, viewValue: 'Pasta', selected: true},
  ];
  @ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}

@Component({
  template: `
      <mat-chip-listbox [formControl]="control" [required]="isRequired"
        [tabIndex]="tabIndexOverride" [selectable]="selectable">
        @for (food of foods; track food) {
          <mat-chip-option [value]="food.value" [disabled]="food.disabled">
            {{ food.viewValue }}
          </mat-chip-option>
        }
      </mat-chip-listbox>
  `,
  standalone: false,
})
class FalsyBasicChipListbox {
  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);
  isRequired: boolean;
  tabIndexOverride: number;
  selectable: boolean = false;

  @ViewChild(MatChipListbox) chipListbox: MatChipListbox;
  @ViewChildren(MatChipOption) chips: QueryList<MatChipOption>;
}

// Based on #29783.
@Component({
  template: `
    <form>
      <mat-chip-listbox name="test" [ngModel]="null">
        <mat-chip-option value="1" disabled>Hello</mat-chip-option>
      </mat-chip-listbox>
    </form>
  `,
  standalone: false,
})
class IndividuallyDisabledChipInsideForm {
  @ViewChild(MatChipOption) chip: MatChipOption;
}
