import {LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing/private';
import {AsyncPipe} from '@angular/common';
import {Component, DebugElement, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
import {
  ComponentFixture,
  TestBed,
  fakeAsync,
  flush,
  tick,
  waitForAsync,
} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Observable} from 'rxjs';
import {
  MAT_TABS_CONFIG,
  MatTab,
  MatTabGroup,
  MatTabHeader,
  MatTabHeaderPosition,
  MatTabsModule,
} from './index';

describe('MatTabGroup', () => {
  beforeEach(fakeAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        MatTabsModule,
        NoopAnimationsModule,
        SimpleTabsTestApp,
        SimpleDynamicTabsTestApp,
        BindedTabsTestApp,
        AsyncTabsTestApp,
        DisabledTabsTestApp,
        TabGroupWithSimpleApi,
        TemplateTabs,
        TabGroupWithAriaInputs,
        TabGroupWithIsActiveBinding,
        NestedTabs,
        TabGroupWithIndirectDescendantTabs,
        TabGroupWithSpaceAbove,
        NestedTabGroupWithLabel,
        TabsWithClassesTestApp,
      ],
    });
  }));

  describe('basic behavior', () => {
    let fixture: ComponentFixture<SimpleTabsTestApp>;
    let element: HTMLElement;

    beforeEach(() => {
      fixture = TestBed.createComponent(SimpleTabsTestApp);
      element = fixture.nativeElement;
    });

    it('should default to the first tab', () => {
      checkSelectedIndex(1, fixture);
    });

    it('will properly load content on first change detection pass', () => {
      fixture.detectChanges();
      const tabBodies = element.querySelectorAll('.mat-mdc-tab-body');
      expect(tabBodies[1].querySelectorAll('span').length).toBe(3);
    });

    it('should change selected index on click', () => {
      let component = fixture.debugElement.componentInstance;
      component.selectedIndex = 0;
      checkSelectedIndex(0, fixture);

      // select the second tab
      let tabLabel = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'))[1];
      tabLabel.nativeElement.click();
      checkSelectedIndex(1, fixture);

      // select the third tab
      tabLabel = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'))[2];
      tabLabel.nativeElement.click();
      checkSelectedIndex(2, fixture);
    });

    it('should support two-way binding for selectedIndex', fakeAsync(() => {
      let component = fixture.componentInstance;
      component.selectedIndex = 0;

      fixture.detectChanges();

      let tabLabel = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'))[1];
      tabLabel.nativeElement.click();
      fixture.detectChanges();
      tick();

      expect(component.selectedIndex).toBe(1);
    }));

    // Note: needs to be `async` in order to fail when we expect it to.
    it('should set to correct tab on fast change', waitForAsync(() => {
      let component = fixture.componentInstance;
      component.selectedIndex = 0;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      setTimeout(() => {
        component.selectedIndex = 1;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        setTimeout(() => {
          component.selectedIndex = 0;
          fixture.changeDetectorRef.markForCheck();
          fixture.detectChanges();
          fixture.whenStable().then(() => {
            expect(component.selectedIndex).toBe(0);
          });
        }, 1);
      }, 1);
    }));

    it('should change tabs based on selectedIndex', fakeAsync(() => {
      let component = fixture.componentInstance;
      let tabComponent = fixture.debugElement.query(By.css('mat-tab-group')).componentInstance;

      spyOn(component, 'handleSelection').and.callThrough();

      checkSelectedIndex(1, fixture);

      tabComponent.selectedIndex = 2;
      fixture.changeDetectorRef.markForCheck();

      checkSelectedIndex(2, fixture);
      tick();

      expect(component.handleSelection).toHaveBeenCalledTimes(1);
      expect(component.selectEvent.index).toBe(2);
    }));

    it('should update tab positions when selected index is changed', () => {
      fixture.detectChanges();
      const component: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      ).componentInstance;
      const tabs: MatTab[] = component._tabs.toArray();

      expect(tabs[0].position).toBeLessThan(0);
      expect(tabs[1].position).toBe(0);
      expect(tabs[2].position).toBeGreaterThan(0);

      // Move to third tab
      component.selectedIndex = 2;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(tabs[0].position).toBeLessThan(0);
      expect(tabs[1].position).toBeLessThan(0);
      expect(tabs[2].position).toBe(0);

      // Move to the first tab
      component.selectedIndex = 0;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(tabs[0].position).toBe(0);
      expect(tabs[1].position).toBeGreaterThan(0);
      expect(tabs[2].position).toBeGreaterThan(0);
    });

    it('should clamp the selected index to the size of the number of tabs', () => {
      fixture.detectChanges();
      const component: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      ).componentInstance;

      // Set the index to be negative, expect first tab selected
      fixture.componentInstance.selectedIndex = -1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(component.selectedIndex).toBe(0);

      // Set the index beyond the size of the tabs, expect last tab selected
      fixture.componentInstance.selectedIndex = 3;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(component.selectedIndex).toBe(2);
    });

    it('should not crash when setting the selected index to NaN', () => {
      let component = fixture.debugElement.componentInstance;

      expect(() => {
        component.selectedIndex = NaN;
        fixture.detectChanges();
      }).not.toThrow();
    });

    it('should show ripples for tab-group labels', () => {
      fixture.detectChanges();

      const testElement = fixture.nativeElement;
      const tabLabel = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'))[1];

      expect(testElement.querySelectorAll('.mat-ripple-element').length)
        .withContext('Expected no ripples to show up initially.')
        .toBe(0);

      dispatchFakeEvent(tabLabel.nativeElement, 'mousedown');

      expect(testElement.querySelectorAll('.mat-ripple-element').length)
        .withContext('Expected one ripple to show up on label mousedown.')
        .toBe(1);
    });

    it('should allow disabling ripples for tab-group labels', () => {
      fixture.componentInstance.disableRipple = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const testElement = fixture.nativeElement;
      const tabLabel = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'))[1];

      expect(testElement.querySelectorAll('.mat-ripple-element').length)
        .withContext('Expected no ripples to show up initially.')
        .toBe(0);

      dispatchFakeEvent(tabLabel.nativeElement, 'mousedown');
      dispatchFakeEvent(tabLabel.nativeElement, 'mouseup');

      expect(testElement.querySelectorAll('.mat-ripple-element').length)
        .withContext('Expected no ripple to show up on label mousedown.')
        .toBe(0);
    });

    it('should set the isActive flag on each of the tabs', fakeAsync(() => {
      fixture.detectChanges();
      tick();

      const tabs = fixture.componentInstance.tabs.toArray();

      expect(tabs[0].isActive).toBe(false);
      expect(tabs[1].isActive).toBe(true);
      expect(tabs[2].isActive).toBe(false);

      fixture.componentInstance.selectedIndex = 2;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(tabs[0].isActive).toBe(false);
      expect(tabs[1].isActive).toBe(false);
      expect(tabs[2].isActive).toBe(true);
    }));

    it('should fire animation done event', fakeAsync(() => {
      fixture.detectChanges();

      spyOn(fixture.componentInstance, 'animationDone');
      let tabLabel = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'))[1];
      tabLabel.nativeElement.click();
      fixture.detectChanges();
      tick();

      expect(fixture.componentInstance.animationDone).toHaveBeenCalledTimes(1);
    }));

    it('should add the proper `aria-setsize` and `aria-posinset`', () => {
      fixture.detectChanges();

      const labels = Array.from(element.querySelectorAll('.mat-mdc-tab'));

      expect(labels.map(label => label.getAttribute('aria-posinset'))).toEqual(['1', '2', '3']);
      expect(labels.every(label => label.getAttribute('aria-setsize') === '3')).toBe(true);
    });

    it('should emit focusChange event on click', () => {
      spyOn(fixture.componentInstance, 'handleFocus');
      fixture.detectChanges();

      const tabLabels = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'));

      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(0);

      tabLabels[2].nativeElement.click();
      fixture.detectChanges();

      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(1);
      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledWith(
        jasmine.objectContaining({index: 2}),
      );
    });

    it('should emit focusChange on arrow key navigation', () => {
      spyOn(fixture.componentInstance, 'handleFocus');
      fixture.detectChanges();

      const tabLabels = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'));
      const tabLabelContainer = fixture.debugElement.query(By.css('.mat-mdc-tab-label-container'))
        .nativeElement as HTMLElement;

      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(0);

      // In order to verify that the `focusChange` event also fires with the correct
      // index, we focus the third tab before testing the keyboard navigation.
      tabLabels[2].nativeElement.click();
      fixture.detectChanges();

      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(1);

      dispatchKeyboardEvent(tabLabelContainer, 'keydown', LEFT_ARROW);

      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(2);
      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledWith(
        jasmine.objectContaining({index: 1}),
      );
    });

    it('should clean up the tabs QueryList on destroy', () => {
      const component: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      )!.componentInstance;
      const spy = jasmine.createSpy('complete spy');
      const subscription = component._tabs.changes.subscribe({complete: spy});

      fixture.destroy();

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

    it('should have a focus indicator', () => {
      const tabLabelNativeElements = [
        ...fixture.debugElement.nativeElement.querySelectorAll('.mat-mdc-tab'),
      ];

      expect(tabLabelNativeElements.every(el => el.classList.contains('mat-focus-indicator'))).toBe(
        true,
      );
    });

    it('should emit focusChange when a tab receives focus', fakeAsync(() => {
      spyOn(fixture.componentInstance, 'handleFocus');
      fixture.detectChanges();

      const tabLabels = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'));

      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(0);

      // In order to verify that the `focusChange` event also fires with the correct
      // index, we focus the second tab before testing the keyboard navigation.
      dispatchFakeEvent(tabLabels[2].nativeElement, 'focus');
      fixture.detectChanges();
      flush();
      fixture.detectChanges();

      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledTimes(1);
      expect(fixture.componentInstance.handleFocus).toHaveBeenCalledWith(
        jasmine.objectContaining({index: 2}),
      );
    }));

    it('should be able to programmatically focus a particular tab', () => {
      fixture.detectChanges();
      const tabGroup: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      ).componentInstance;
      const tabHeader: MatTabHeader = fixture.debugElement.query(
        By.css('mat-tab-header'),
      ).componentInstance;

      expect(tabHeader.focusIndex).not.toBe(3);

      tabGroup.focusTab(3);
      fixture.detectChanges();

      expect(tabHeader.focusIndex).not.toBe(3);
    });

    it('should be able to set a tabindex on the inner content element', () => {
      fixture.componentInstance.contentTabIndex = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      const contentElements = Array.from<HTMLElement>(
        fixture.nativeElement.querySelectorAll('mat-tab-body'),
      );

      expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual([null, '1', null]);

      fixture.componentInstance.selectedIndex = 0;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(contentElements.map(e => e.getAttribute('tabindex'))).toEqual(['1', null, null]);
    });

    it('should update the tabindex of the labels when navigating via keyboard', () => {
      fixture.detectChanges();

      const tabLabels = fixture.debugElement
        .queryAll(By.css('.mat-mdc-tab'))
        .map(label => label.nativeElement);
      const tabLabelContainer = fixture.debugElement.query(By.css('.mat-mdc-tab-label-container'))
        .nativeElement as HTMLElement;

      expect(tabLabels.map(label => label.getAttribute('tabindex'))).toEqual(['-1', '0', '-1']);

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

      expect(tabLabels.map(label => label.getAttribute('tabindex'))).toEqual(['-1', '-1', '0']);
    });

    it('should be able to set the aria-label of the tablist', fakeAsync(() => {
      fixture.detectChanges();
      tick();

      const tabList = fixture.nativeElement.querySelector('.mat-mdc-tab-list') as HTMLElement;
      expect(tabList.hasAttribute('aria-label')).toBe(false);

      fixture.componentInstance.ariaLabel = 'hello';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(tabList.getAttribute('aria-label')).toBe('hello');

      fixture.componentInstance.ariaLabel = '';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(tabList.hasAttribute('aria-label')).toBe(false);
    }));

    it('should be able to set the aria-labelledby of the tablist', fakeAsync(() => {
      fixture.detectChanges();
      tick();

      const tabList = fixture.nativeElement.querySelector('.mat-mdc-tab-list') as HTMLElement;
      expect(tabList.hasAttribute('aria-labelledby')).toBe(false);

      fixture.componentInstance.ariaLabelledby = 'some-label';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(tabList.getAttribute('aria-labelledby')).toBe('some-label');

      fixture.componentInstance.ariaLabelledby = '';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(tabList.hasAttribute('aria-labelledby')).toBe(false);
    }));
  });

  describe('aria labelling', () => {
    let fixture: ComponentFixture<TabGroupWithAriaInputs>;
    let tab: HTMLElement;

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(TabGroupWithAriaInputs);
      fixture.detectChanges();
      tick();
      tab = fixture.nativeElement.querySelector('.mat-mdc-tab');
    }));

    it('should not set aria-label or aria-labelledby attributes if they are not passed in', () => {
      expect(tab.hasAttribute('aria-label')).toBe(false);
      expect(tab.hasAttribute('aria-labelledby')).toBe(false);
    });

    it('should set the aria-label attribute', () => {
      fixture.componentInstance.ariaLabel = 'Fruit';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(tab.getAttribute('aria-label')).toBe('Fruit');
    });

    it('should set the aria-labelledby attribute', () => {
      fixture.componentInstance.ariaLabelledby = 'fruit-label';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(tab.getAttribute('aria-labelledby')).toBe('fruit-label');
    });

    it('should not be able to set both an aria-label and aria-labelledby', () => {
      fixture.componentInstance.ariaLabel = 'Fruit';
      fixture.componentInstance.ariaLabelledby = 'fruit-label';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(tab.getAttribute('aria-label')).toBe('Fruit');
      expect(tab.hasAttribute('aria-labelledby')).toBe(false);

      fixture.componentInstance.ariaLabel = 'Veggie';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      expect(tab.getAttribute('aria-label')).toBe('Veggie');
    });
  });

  describe('aria labelling of tab panels', () => {
    let fixture: ComponentFixture<BindedTabsTestApp>;
    let tabPanels: HTMLElement[];

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(BindedTabsTestApp);
      fixture.detectChanges();
      tick();
      tabPanels = Array.from(fixture.nativeElement.querySelectorAll('.mat-mdc-tab-body'));
    }));

    it('should set `aria-hidden="true"` on inactive tab panels', () => {
      fixture.detectChanges();

      expect(tabPanels[0].getAttribute('aria-hidden')).not.toBe('true');
      expect(tabPanels[1].getAttribute('aria-hidden')).toBe('true');
    });
  });

  describe('disable tabs', () => {
    let fixture: ComponentFixture<DisabledTabsTestApp>;

    beforeEach(() => {
      fixture = TestBed.createComponent(DisabledTabsTestApp);
    });

    it('should have one disabled tab', () => {
      fixture.detectChanges();
      const labels = fixture.debugElement.queryAll(By.css('.mat-mdc-tab-disabled'));
      expect(labels.length).toBe(1);
      expect(labels[0].nativeElement.getAttribute('aria-disabled')).toBe('true');
    });

    it('should set the disabled flag on tab', () => {
      fixture.detectChanges();

      const tabs = fixture.componentInstance.tabs.toArray();
      let labels = fixture.debugElement.queryAll(By.css('.mat-mdc-tab-disabled'));
      expect(tabs[2].disabled).toBe(false);
      expect(labels.length).toBe(1);
      expect(labels[0].nativeElement.getAttribute('aria-disabled')).toBe('true');

      fixture.componentInstance.isDisabled = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(tabs[2].disabled).toBe(true);
      labels = fixture.debugElement.queryAll(By.css('.mat-mdc-tab-disabled'));
      expect(labels.length).toBe(2);
      expect(
        labels.every(label => label.nativeElement.getAttribute('aria-disabled') === 'true'),
      ).toBe(true);
    });
  });

  describe('dynamic binding tabs', () => {
    let fixture: ComponentFixture<SimpleDynamicTabsTestApp>;

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(SimpleDynamicTabsTestApp);
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
    }));

    it('should be able to add a new tab, select it, and have correct origin position', fakeAsync(() => {
      const component: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      ).componentInstance;

      let tabs: MatTab[] = component._tabs.toArray();
      expect(tabs[0].origin).toBe(null);
      expect(tabs[1].origin).toBe(0);
      expect(tabs[2].origin).toBe(null);

      // Add a new tab on the right and select it, expect an origin >= than 0 (animate right)
      fixture.componentInstance.tabs.push({label: 'New tab', content: 'to right of index'});
      fixture.componentInstance.selectedIndex = 4;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      tabs = component._tabs.toArray();
      expect(tabs[3].origin).toBeGreaterThanOrEqual(0);

      // Add a new tab in the beginning and select it, expect an origin < than 0 (animate left)
      fixture.componentInstance.selectedIndex = 0;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      fixture.componentInstance.tabs.push({label: 'New tab', content: 'to left of index'});
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      tabs = component._tabs.toArray();
      expect(tabs[0].origin).toBeLessThan(0);
    }));

    it('should update selected index if the last tab removed while selected', fakeAsync(() => {
      const component: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      ).componentInstance;

      const numberOfTabs = component._tabs.length;
      fixture.componentInstance.selectedIndex = numberOfTabs - 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      // Remove last tab while last tab is selected, expect next tab over to be selected
      fixture.componentInstance.tabs.pop();
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(component.selectedIndex).toBe(numberOfTabs - 2);
    }));

    it('should maintain the selected tab if a new tab is added', () => {
      fixture.detectChanges();
      const component: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      ).componentInstance;

      fixture.componentInstance.selectedIndex = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      // Add a new tab at the beginning.
      fixture.componentInstance.tabs.unshift({label: 'New tab', content: 'at the start'});
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(component.selectedIndex).toBe(2);
      expect(component._tabs.toArray()[2].isActive).toBe(true);
    });

    it('should maintain the selected tab if a tab is removed', () => {
      // Select the second tab.
      fixture.componentInstance.selectedIndex = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const component: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      ).componentInstance;

      // Remove the first tab that is right before the selected one.
      fixture.componentInstance.tabs.splice(0, 1);
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      // Since the first tab has been removed and the second one was selected before, the selected
      // tab moved one position to the right. Meaning that the tab is now the first tab.
      expect(component.selectedIndex).toBe(0);
      expect(component._tabs.toArray()[0].isActive).toBe(true);
    });

    it('should be able to select a new tab after creation', fakeAsync(() => {
      fixture.detectChanges();
      const component: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      ).componentInstance;

      fixture.componentInstance.tabs.push({label: 'Last tab', content: 'at the end'});
      fixture.componentInstance.selectedIndex = 3;
      fixture.changeDetectorRef.markForCheck();

      fixture.detectChanges();
      tick();

      expect(component.selectedIndex).toBe(3);
      expect(component._tabs.toArray()[3].isActive).toBe(true);
    }));

    it('should not fire `selectedTabChange` when the amount of tabs changes', fakeAsync(() => {
      fixture.detectChanges();
      fixture.componentInstance.selectedIndex = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      // Add a new tab at the beginning.
      spyOn(fixture.componentInstance, 'handleSelection');
      fixture.componentInstance.tabs.unshift({label: 'New tab', content: 'at the start'});
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(fixture.componentInstance.handleSelection).not.toHaveBeenCalled();
    }));

    it('should update the newly-selected tab if the previously-selected tab is replaced', fakeAsync(() => {
      const component: MatTabGroup = fixture.debugElement.query(
        By.css('mat-tab-group'),
      )!.componentInstance;

      spyOn(fixture.componentInstance, 'handleSelection');

      fixture.componentInstance.tabs[fixture.componentInstance.selectedIndex] = {
        label: 'New',
        content: 'New',
      };
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(component._tabs.get(1)?.isActive).toBe(true);
      expect(fixture.componentInstance.handleSelection).toHaveBeenCalledWith(
        jasmine.objectContaining({index: 1}),
      );
    }));

    it('should be able to disable the pagination', fakeAsync(() => {
      fixture.componentInstance.disablePagination = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      for (let i = 0; i < 50; i++) {
        fixture.componentInstance.tabs.push({label: `Extra ${i}`, content: ''});
      }
      fixture.changeDetectorRef.markForCheck();

      fixture.detectChanges();
      tick();

      expect(
        fixture.nativeElement.querySelector('.mat-mdc-tab-header-pagination-controls-enabled'),
      ).toBeFalsy();
    }));
  });

  describe('async tabs', () => {
    let fixture: ComponentFixture<AsyncTabsTestApp>;

    it('should show tabs when they are available', fakeAsync(() => {
      fixture = TestBed.createComponent(AsyncTabsTestApp);

      expect(fixture.debugElement.queryAll(By.css('.mat-mdc-tab')).length).toBe(0);

      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      tick();

      expect(fixture.debugElement.queryAll(By.css('.mat-mdc-tab')).length).toBe(2);
    }));
  });

  describe('with simple api', () => {
    let fixture: ComponentFixture<TabGroupWithSimpleApi>;
    let tabGroup: MatTabGroup;

    beforeEach(fakeAsync(() => {
      fixture = TestBed.createComponent(TabGroupWithSimpleApi);
      fixture.detectChanges();
      tick();

      tabGroup = fixture.debugElement.query(By.directive(MatTabGroup))
        .componentInstance as MatTabGroup;
    }));

    it('should support a tab-group with the simple api', fakeAsync(() => {
      expect(getSelectedLabel(fixture).textContent).toMatch('Junk food');
      expect(getSelectedContent(fixture).textContent).toMatch('Pizza, fries');

      tabGroup.selectedIndex = 2;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(getSelectedLabel(fixture).textContent).toMatch('Fruit');
      expect(getSelectedContent(fixture).textContent).toMatch('Apples, grapes');

      fixture.componentInstance.otherLabel = 'Chips';
      fixture.componentInstance.otherContent = 'Salt, vinegar';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(getSelectedLabel(fixture).textContent).toMatch('Chips');
      expect(getSelectedContent(fixture).textContent).toMatch('Salt, vinegar');
    }));

    it('should support @ViewChild in the tab content', () => {
      expect(fixture.componentInstance.legumes).toBeTruthy();
    });

    it('should only have the active tab in the DOM', fakeAsync(() => {
      expect(fixture.nativeElement.textContent).toContain('Pizza, fries');
      expect(fixture.nativeElement.textContent).not.toContain('Peanuts');

      tabGroup.selectedIndex = 3;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(fixture.nativeElement.textContent).not.toContain('Pizza, fries');
      expect(fixture.nativeElement.textContent).toContain('Peanuts');
    }));

    it('should support setting the header position', () => {
      let tabGroupNode = fixture.debugElement.query(By.css('mat-tab-group')).nativeElement;

      expect(tabGroupNode.classList).not.toContain('mat-mdc-tab-group-inverted-header');

      tabGroup.headerPosition = 'below';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(tabGroupNode.classList).toContain('mat-mdc-tab-group-inverted-header');
    });

    it('should be able to opt into keeping the inactive tab content in the DOM', fakeAsync(() => {
      fixture.componentInstance.preserveContent = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(fixture.nativeElement.textContent).toContain('Pizza, fries');
      expect(fixture.nativeElement.textContent).not.toContain('Peanuts');

      tabGroup.selectedIndex = 3;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(fixture.nativeElement.textContent).toContain('Pizza, fries');
      expect(fixture.nativeElement.textContent).toContain('Peanuts');
    }));

    it('should visibly hide the content of inactive tabs', fakeAsync(() => {
      const contentElements: HTMLElement[] = Array.from(
        fixture.nativeElement.querySelectorAll('.mat-mdc-tab-body-content'),
      );

      expect(contentElements.map(element => element.style.visibility)).toEqual([
        'visible',
        'hidden',
        'hidden',
        'hidden',
      ]);

      tabGroup.selectedIndex = 2;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(contentElements.map(element => element.style.visibility)).toEqual([
        'hidden',
        'hidden',
        'visible',
        'hidden',
      ]);

      tabGroup.selectedIndex = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      tick();

      expect(contentElements.map(element => element.style.visibility)).toEqual([
        'hidden',
        'visible',
        'hidden',
        'hidden',
      ]);
    }));
  });

  describe('lazy loaded tabs', () => {
    it('should lazy load the second tab', fakeAsync(() => {
      const fixture = TestBed.createComponent(TemplateTabs);
      fixture.detectChanges();
      tick();

      const secondLabel = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'))[1];
      secondLabel.nativeElement.click();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      const child = fixture.debugElement.query(By.css('.child'));
      expect(child.nativeElement).toBeDefined();
    }));
  });

  describe('special cases', () => {
    it('should not throw an error when binding isActive to the view', fakeAsync(() => {
      const fixture = TestBed.createComponent(TabGroupWithIsActiveBinding);

      expect(() => {
        fixture.detectChanges();
        tick();
        fixture.detectChanges();
      }).not.toThrow();

      expect(fixture.nativeElement.textContent).toContain('pizza is active');
    }));

    it('should not pick up mat-tab-label from a child tab', fakeAsync(() => {
      const fixture = TestBed.createComponent(NestedTabGroupWithLabel);
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      const labels = fixture.nativeElement.querySelectorAll('.mdc-tab__text-label');
      const contents = Array.from<HTMLElement>(labels).map(label => label.textContent?.trim());

      expect(contents).toEqual([
        'Parent 1',
        'Parent 2',
        'Parent 3',
        'Child 1',
        'Child 2',
        'Child 3',
      ]);
    }));
  });

  describe('nested tabs', () => {
    it('should not pick up the tabs from descendant tab groups', fakeAsync(() => {
      const fixture = TestBed.createComponent(NestedTabs);
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      const groups = fixture.componentInstance.groups.toArray();

      expect(groups.length).toBe(2);
      expect(groups[0]._tabs.map((tab: MatTab) => tab.textLabel)).toEqual(['One', 'Two']);
      expect(groups[1]._tabs.map((tab: MatTab) => tab.textLabel)).toEqual([
        'Inner tab one',
        'Inner tab two',
      ]);
    }));

    it('should pick up indirect descendant tabs', fakeAsync(() => {
      const fixture = TestBed.createComponent(TabGroupWithIndirectDescendantTabs);
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      const tabs = fixture.componentInstance.tabGroup._tabs;
      expect(tabs.map((tab: MatTab) => tab.textLabel)).toEqual(['One', 'Two']);
    }));
  });

  describe('tall tabs', () => {
    beforeEach(() => {
      window.scrollTo({top: 0});
    });

    it('should not scroll when changing tabs by clicking', fakeAsync(() => {
      const fixture = TestBed.createComponent(TabGroupWithSpaceAbove);
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      window.scrollBy(0, 250);
      expect(window.scrollY).toBe(250);

      // select the second tab
      let tabLabel = fixture.debugElement.queryAll(By.css('.mat-mdc-tab'))[1];
      tabLabel.nativeElement.click();
      checkSelectedIndex(1, fixture);

      expect(window.scrollY).toBe(250);
      tick();
    }));

    it('should not scroll when changing tabs programatically', fakeAsync(() => {
      const fixture = TestBed.createComponent(TabGroupWithSpaceAbove);
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      window.scrollBy(0, 250);
      expect(window.scrollY).toBe(250);

      fixture.componentInstance.tabGroup.selectedIndex = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(window.scrollY).toBe(250);
      tick();
    }));
  });

  describe('tabs with custom css classes', () => {
    let fixture: ComponentFixture<TabsWithClassesTestApp>;
    let labelElements: DebugElement[];
    let bodyElements: DebugElement[];

    beforeEach(() => {
      fixture = TestBed.createComponent(TabsWithClassesTestApp);
      fixture.detectChanges();
      labelElements = fixture.debugElement.queryAll(By.css('.mdc-tab'));
      bodyElements = fixture.debugElement.queryAll(By.css('mat-tab-body'));
    });

    it('should apply label/body classes', () => {
      expect(labelElements[1].nativeElement.classList).toContain('hardcoded-label-class');
      expect(bodyElements[1].nativeElement.classList).toContain('hardcoded-body-class');
    });

    it('should set classes as strings dynamically', () => {
      expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
      expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');

      fixture.componentInstance.labelClassList = 'custom-label-class';
      fixture.componentInstance.bodyClassList = 'custom-body-class';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
      expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');

      delete fixture.componentInstance.labelClassList;
      delete fixture.componentInstance.bodyClassList;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
      expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
    });

    it('should set classes as strings array dynamically', () => {
      expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
      expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');

      fixture.componentInstance.labelClassList = ['custom-label-class'];
      fixture.componentInstance.bodyClassList = ['custom-body-class'];
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(labelElements[0].nativeElement.classList).toContain('custom-label-class');
      expect(bodyElements[0].nativeElement.classList).toContain('custom-body-class');

      delete fixture.componentInstance.labelClassList;
      delete fixture.componentInstance.bodyClassList;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(labelElements[0].nativeElement.classList).not.toContain('custom-label-class');
      expect(bodyElements[0].nativeElement.classList).not.toContain('custom-body-class');
    });
  });

  /**
   * Checks that the `selectedIndex` has been updated; checks that the label and body have their
   * respective `active` classes
   */
  function checkSelectedIndex(expectedIndex: number, fixture: ComponentFixture<any>) {
    fixture.detectChanges();

    let tabComponent: MatTabGroup = fixture.debugElement.query(
      By.css('mat-tab-group'),
    ).componentInstance;
    expect(tabComponent.selectedIndex).toBe(expectedIndex);

    let tabLabelElement = fixture.debugElement.query(
      By.css(`.mat-mdc-tab:nth-of-type(${expectedIndex + 1})`),
    ).nativeElement;
    expect(tabLabelElement.classList.contains('mdc-tab--active')).toBe(true);

    let tabContentElement = fixture.debugElement.query(
      By.css(`mat-tab-body:nth-of-type(${expectedIndex + 1})`),
    ).nativeElement;
    expect(tabContentElement.classList.contains('mat-mdc-tab-body-active')).toBe(true);
  }

  function getSelectedLabel(fixture: ComponentFixture<any>): HTMLElement {
    return fixture.nativeElement.querySelector('.mdc-tab--active');
  }

  function getSelectedContent(fixture: ComponentFixture<any>): HTMLElement {
    return fixture.nativeElement.querySelector('.mat-mdc-tab-body-active');
  }
});

describe('nested MatTabGroup with enabled animations', () => {
  beforeEach(fakeAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        MatTabsModule,
        BrowserAnimationsModule,
        NestedTabs,
        TabsWithCustomAnimationDuration,
      ],
    });
  }));

  it('should not throw when creating a component with nested tab groups', fakeAsync(() => {
    expect(() => {
      let fixture = TestBed.createComponent(NestedTabs);
      fixture.detectChanges();
      tick();
    }).not.toThrow();
  }));

  it('should not throw when setting an animationDuration without units', fakeAsync(() => {
    expect(() => {
      let fixture = TestBed.createComponent(TabsWithCustomAnimationDuration);
      fixture.detectChanges();
      tick();
    }).not.toThrow();
  }));

  it('should set appropiate css variable given a specified animationDuration', fakeAsync(() => {
    let fixture = TestBed.createComponent(TabsWithCustomAnimationDuration);
    fixture.detectChanges();
    tick();

    const tabGroup = fixture.nativeElement.querySelector('.mat-mdc-tab-group');
    expect(tabGroup.style.getPropertyValue('--mat-tab-animation-duration')).toBe('500ms');
  }));
});

describe('MatTabGroup with ink bar fit to content', () => {
  let fixture: ComponentFixture<TabGroupWithInkBarFitToContent>;

  beforeEach(fakeAsync(() => {
    TestBed.configureTestingModule({
      imports: [MatTabsModule, BrowserAnimationsModule, TabGroupWithInkBarFitToContent],
    });
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TabGroupWithInkBarFitToContent);
    fixture.detectChanges();
  });

  it('should properly nest the ink bar when fit to content', () => {
    const tabElement = fixture.nativeElement.querySelector('.mdc-tab');
    const contentElement = tabElement.querySelector('.mdc-tab__content');
    const indicatorElement = tabElement.querySelector('.mdc-tab-indicator');
    expect(indicatorElement.parentElement).toBeTruthy();
    expect(indicatorElement.parentElement).toBe(contentElement);
  });

  it('should be able to move the ink bar between content and full', () => {
    fixture.componentInstance.fitInkBarToContent = false;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();

    const tabElement = fixture.nativeElement.querySelector('.mdc-tab');
    const indicatorElement = tabElement.querySelector('.mdc-tab-indicator');
    expect(indicatorElement.parentElement).toBeTruthy();
    expect(indicatorElement.parentElement).toBe(tabElement);

    fixture.componentInstance.fitInkBarToContent = true;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();

    const contentElement = tabElement.querySelector('.mdc-tab__content');
    expect(indicatorElement.parentElement).toBeTruthy();
    expect(indicatorElement.parentElement).toBe(contentElement);
  });
});

describe('MatTabNavBar with a default config', () => {
  let fixture: ComponentFixture<SimpleTabsTestApp>;

  beforeEach(fakeAsync(() => {
    TestBed.configureTestingModule({
      imports: [MatTabsModule, BrowserAnimationsModule, SimpleTabsTestApp],
      providers: [
        {
          provide: MAT_TABS_CONFIG,
          useValue: {fitInkBarToContent: true, dynamicHeight: true},
        },
      ],
    });
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SimpleTabsTestApp);
    fixture.detectChanges();
  });

  it('should set whether the ink bar fits to content', () => {
    const tabElement = fixture.nativeElement.querySelector('.mdc-tab');
    const contentElement = tabElement.querySelector('.mdc-tab__content');
    const indicatorElement = tabElement.querySelector('.mdc-tab-indicator');
    expect(indicatorElement.parentElement).toBeTruthy();
    expect(indicatorElement.parentElement).toBe(contentElement);
  });

  it('should set whether the height of the tab group is dynamic', () => {
    expect(fixture.componentInstance.tabGroup.dynamicHeight).toBe(true);
  });
});

describe('MatTabGroup labels aligned with a config', () => {
  it('should work with start align', () => {
    const fixture = TestBed.configureTestingModule({
      imports: [MatTabsModule, BrowserAnimationsModule, TabsWithAlignConfig],
      providers: [
        {
          provide: MAT_TABS_CONFIG,
          useValue: {alignTabs: 'start'},
        },
      ],
    }).createComponent(TabsWithAlignConfig);
    fixture.detectChanges();

    const tabElement = fixture.nativeElement.querySelector('[mat-align-tabs="start"]');
    expect(tabElement).toBeTruthy();
  });

  it('should work with center align', () => {
    const fixture = TestBed.configureTestingModule({
      imports: [MatTabsModule, BrowserAnimationsModule, TabsWithAlignConfig],
      providers: [
        {
          provide: MAT_TABS_CONFIG,
          useValue: {alignTabs: 'center'},
        },
      ],
    }).createComponent(TabsWithAlignConfig);
    fixture.detectChanges();

    const tabElement = fixture.nativeElement.querySelector('[mat-align-tabs="center"]');
    expect(tabElement).toBeTruthy();
  });

  it('should work with end align', () => {
    const fixture = TestBed.configureTestingModule({
      imports: [MatTabsModule, BrowserAnimationsModule, TabsWithAlignConfig],
      providers: [
        {
          provide: MAT_TABS_CONFIG,
          useValue: {alignTabs: 'end'},
        },
      ],
    }).createComponent(TabsWithAlignConfig);
    fixture.detectChanges();

    const tabElement = fixture.nativeElement.querySelector('[mat-align-tabs="end"]');
    expect(tabElement).toBeTruthy();
  });

  it('should not add align if default config doesnt set align', () => {
    const fixture = TestBed.configureTestingModule({
      imports: [MatTabsModule, BrowserAnimationsModule, TabsWithAlignConfig],
    }).createComponent(TabsWithAlignConfig);
    fixture.detectChanges();

    let tabElement = fixture.nativeElement.querySelector('[mat-align-tabs="start"]');
    expect(tabElement).toBeFalsy();

    tabElement = fixture.nativeElement.querySelector('[mat-align-tabs="center"]');
    expect(tabElement).toBeFalsy();

    tabElement = fixture.nativeElement.querySelector('[mat-align-tabs="end"]');
    expect(tabElement).toBeFalsy();

    tabElement = fixture.nativeElement.querySelector('.mat-mdc-tab-group');
    expect(tabElement).toBeTruthy();
  });

  it('should not break if config sets align on already aligned tabs', () => {
    const fixture = TestBed.configureTestingModule({
      imports: [MatTabsModule, BrowserAnimationsModule, TabsWithAlignCenter],
      providers: [{provide: MAT_TABS_CONFIG, useValue: {alignTabs: 'end'}}],
    }).createComponent(TabsWithAlignCenter);
    fixture.detectChanges();

    let tabElement = fixture.nativeElement.querySelector('[mat-align-tabs="start"]');
    expect(tabElement).toBeFalsy();

    tabElement = fixture.nativeElement.querySelector('[mat-align-tabs="center"]');
    expect(tabElement).toBeTruthy();

    tabElement = fixture.nativeElement.querySelector('[mat-align-tabs="end"]');
    expect(tabElement).toBeFalsy();

    tabElement = fixture.nativeElement.querySelector('.mat-mdc-tab-group');
    expect(tabElement).toBeTruthy();
  });
});

@Component({
  template: `
    <mat-tab-group class="tab-group"
        [(selectedIndex)]="selectedIndex"
        [headerPosition]="headerPosition"
        [disableRipple]="disableRipple"
        [contentTabIndex]="contentTabIndex"
        [aria-label]="ariaLabel"
        [aria-labelledby]="ariaLabelledby"
        (animationDone)="animationDone()"
        (focusChange)="handleFocus($event)"
        (selectedTabChange)="handleSelection($event)">
      <mat-tab>
        <ng-template mat-tab-label>Tab One</ng-template>
        Tab one content
      </mat-tab>
      <mat-tab>
        <ng-template mat-tab-label>Tab Two</ng-template>
        <span>Tab </span><span>two</span><span>content</span>
      </mat-tab>
      <mat-tab>
        <ng-template mat-tab-label>Tab Three</ng-template>
        Tab three content
      </mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class SimpleTabsTestApp {
  @ViewChild(MatTabGroup) tabGroup: MatTabGroup;
  @ViewChildren(MatTab) tabs: QueryList<MatTab>;
  selectedIndex: number = 1;
  focusEvent: any;
  selectEvent: any;
  disableRipple: boolean = false;
  contentTabIndex: number | null = null;
  headerPosition: MatTabHeaderPosition = 'above';
  ariaLabel: string;
  ariaLabelledby: string;
  handleFocus(event: any) {
    this.focusEvent = event;
  }
  handleSelection(event: any) {
    this.selectEvent = event;
  }
  animationDone() {}
}

@Component({
  template: `
    <mat-tab-group class="tab-group"
        [(selectedIndex)]="selectedIndex"
        (focusChange)="handleFocus($event)"
        (selectedTabChange)="handleSelection($event)"
        [disablePagination]="disablePagination">
      @for (tab of tabs; track tab) {
        <mat-tab>
          <ng-template mat-tab-label>{{tab.label}}</ng-template>
          {{tab.content}}
        </mat-tab>
      }
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class SimpleDynamicTabsTestApp {
  tabs = [
    {label: 'Label 1', content: 'Content 1'},
    {label: 'Label 2', content: 'Content 2'},
    {label: 'Label 3', content: 'Content 3'},
  ];
  selectedIndex: number = 1;
  focusEvent: any;
  selectEvent: any;
  disablePagination = false;
  handleFocus(event: any) {
    this.focusEvent = event;
  }
  handleSelection(event: any) {
    this.selectEvent = event;
  }
}

@Component({
  template: `
    <mat-tab-group class="tab-group" [(selectedIndex)]="selectedIndex">
      @for (tab of tabs; track tab) {
        <mat-tab label="{{tab.label}}">{{tab.content}}</mat-tab>
      }
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class BindedTabsTestApp {
  tabs = [
    {label: 'one', content: 'one'},
    {label: 'two', content: 'two'},
  ];
  selectedIndex = 0;

  addNewActiveTab(): void {
    this.tabs.push({
      label: 'new tab',
      content: 'new content',
    });
    this.selectedIndex = this.tabs.length - 1;
  }
}

@Component({
  template: `
    <mat-tab-group class="tab-group">
      <mat-tab>
        <ng-template mat-tab-label>Tab One</ng-template>
        Tab one content
      </mat-tab>
      <mat-tab disabled>
        <ng-template mat-tab-label>Tab Two</ng-template>
        Tab two content
      </mat-tab>
      <mat-tab [disabled]="isDisabled">
        <ng-template mat-tab-label>Tab Three</ng-template>
        Tab three content
      </mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class DisabledTabsTestApp {
  @ViewChildren(MatTab) tabs: QueryList<MatTab>;
  isDisabled = false;
}

@Component({
  template: `
    <mat-tab-group class="tab-group">
      @for (tab of tabs | async; track tab) {
        <mat-tab>
          <ng-template mat-tab-label>{{ tab.label }}</ng-template>
          {{ tab.content }}
        </mat-tab>
      }
   </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule, AsyncPipe],
})
class AsyncTabsTestApp implements OnInit {
  private _tabs = [
    {label: 'one', content: 'one'},
    {label: 'two', content: 'two'},
  ];

  tabs: Observable<any>;

  ngOnInit() {
    // Use ngOnInit because there is some issue with scheduling the async task in the constructor.
    this.tabs = new Observable((observer: any) => {
      setTimeout(() => observer.next(this._tabs));
    });
  }
}

@Component({
  template: `
  <mat-tab-group [preserveContent]="preserveContent">
    <mat-tab label="Junk food"> Pizza, fries </mat-tab>
    <mat-tab label="Vegetables"> Broccoli, spinach </mat-tab>
    <mat-tab [label]="otherLabel"> {{otherContent}} </mat-tab>
    <mat-tab label="Legumes"> <p #legumes>Peanuts</p> </mat-tab>
  </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabGroupWithSimpleApi {
  preserveContent = false;
  otherLabel = 'Fruit';
  otherContent = 'Apples, grapes';
  @ViewChild('legumes') legumes: any;
}

@Component({
  template: `
    <mat-tab-group>
      <mat-tab label="One">Tab one content</mat-tab>
      <mat-tab label="Two">
        Tab two content
         <mat-tab-group [dynamicHeight]="true">
          <mat-tab label="Inner tab one">Inner content one</mat-tab>
          <mat-tab label="Inner tab two">Inner content two</mat-tab>
        </mat-tab-group>
      </mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class NestedTabs {
  @ViewChildren(MatTabGroup) groups: QueryList<MatTabGroup>;
}

@Component({
  template: `
    <mat-tab-group>
      <mat-tab label="One">
        Eager
      </mat-tab>
      <mat-tab label="Two">
        <ng-template matTabContent>
          <div class="child">Hi</div>
        </ng-template>
      </mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TemplateTabs {}

@Component({
  template: `
  <mat-tab-group>
    <mat-tab [aria-label]="ariaLabel" [aria-labelledby]="ariaLabelledby"></mat-tab>
  </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabGroupWithAriaInputs {
  ariaLabel: string;
  ariaLabelledby: string;
}

@Component({
  template: `
    <mat-tab-group>
      <mat-tab label="Junk food" #pizza> Pizza, fries </mat-tab>
      <mat-tab label="Vegetables"> Broccoli, spinach </mat-tab>
    </mat-tab-group>

    @if (pizza.isActive) {
      <div>pizza is active</div>
    }
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabGroupWithIsActiveBinding {}

@Component({
  template: `
    <mat-tab-group animationDuration="500">
      <mat-tab label="One">Tab one content</mat-tab>
      <mat-tab label="Two">Tab two content</mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabsWithCustomAnimationDuration {}

@Component({
  template: `
    <mat-tab-group>
      @if (true) {
        <mat-tab label="One">Tab one content</mat-tab>
        <mat-tab label="Two">Tab two content</mat-tab>
      }
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabGroupWithIndirectDescendantTabs {
  @ViewChild(MatTabGroup) tabGroup: MatTabGroup;
}

@Component({
  template: `
    <mat-tab-group [fitInkBarToContent]="fitInkBarToContent">
      <mat-tab label="One">Tab one content</mat-tab>
      <mat-tab label="Two">Tab two content</mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabGroupWithInkBarFitToContent {
  fitInkBarToContent = true;
}

@Component({
  template: `
    <div style="height: 300px; background-color: aqua">
      Top Content here
    </div>
    <mat-tab-group>
      <ng-container>
        <mat-tab label="One">
          <div style="height: 3000px; background-color: red"></div>
        </mat-tab>
        <mat-tab label="Two">
          <div style="height: 3000px; background-color: green"></div>
        </mat-tab>
      </ng-container>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabGroupWithSpaceAbove {
  @ViewChild(MatTabGroup) tabGroup: MatTabGroup;
}

@Component({
  template: `
    <mat-tab-group>
      <mat-tab label="Parent 1">
        <mat-tab-group>
          <mat-tab label="Child 1">Content 1</mat-tab>
          <mat-tab>
            <ng-template mat-tab-label>Child 2</ng-template>
            Content 2
          </mat-tab>
          <mat-tab label="Child 3">Child 3</mat-tab>
        </mat-tab-group>
      </mat-tab>
      <mat-tab label="Parent 2">Parent 2</mat-tab>
      <mat-tab label="Parent 3">Parent 3</mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class NestedTabGroupWithLabel {}

@Component({
  template: `
    <mat-tab-group class="tab-group">
      <mat-tab label="Tab One" [labelClass]="labelClassList" [bodyClass]="bodyClassList">
        Tab one content
      </mat-tab>
      <mat-tab label="Tab Two" labelClass="hardcoded-label-class"
               bodyClass="hardcoded-body-class">
        Tab two content
      </mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabsWithClassesTestApp {
  labelClassList?: string | string[];
  bodyClassList?: string | string[];
}

@Component({
  template: `
    <mat-tab-group>
      <mat-tab>
        First
      </mat-tab>
      <mat-tab>
        Second
      </mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabsWithAlignConfig {}

@Component({
  template: `
    <mat-tab-group mat-align-tabs="center">
      <mat-tab>
        First
      </mat-tab>
      <mat-tab>
        Second
      </mat-tab>
    </mat-tab-group>
  `,
  standalone: true,
  imports: [MatTabsModule],
})
class TabsWithAlignCenter {}
