import {A11yModule} from '@angular/cdk/a11y';
import {Direction} from '@angular/cdk/bidi';
import {ESCAPE} from '@angular/cdk/keycodes';
import {CdkScrollable} from '@angular/cdk/scrolling';
import {
  createKeyboardEvent,
  dispatchEvent,
  dispatchKeyboardEvent,
} from '@angular/cdk/testing/private';
import {Component, ElementRef, ErrorHandler, ViewChild} from '@angular/core';
import {
  ComponentFixture,
  TestBed,
  discardPeriodicTasks,
  fakeAsync,
  flush,
  tick,
  waitForAsync,
} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MatDrawer, MatDrawerContainer, MatSidenavModule} from './index';

describe('MatDrawer', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        MatSidenavModule,
        A11yModule,
        NoopAnimationsModule,
        BasicTestApp,
        DrawerContainerNoDrawerTestApp,
        DrawerSetToOpenedFalse,
        DrawerSetToOpenedTrue,
        DrawerDynamicPosition,
        DrawerWithFocusableElements,
        DrawerOpenBinding,
        DrawerWithoutFocusableElements,
        IndirectDescendantDrawer,
        NestedDrawerContainers,
      ],
    });
  }));

  describe('methods', () => {
    it('should be able to open', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.detectChanges();

      const testComponent: BasicTestApp = fixture.debugElement.componentInstance;
      const container = fixture.debugElement.query(By.css('mat-drawer-container'))!.nativeElement;

      fixture.debugElement.query(By.css('.open'))!.nativeElement.click();
      fixture.detectChanges();

      expect(testComponent.openCount).toBe(0);
      expect(testComponent.openStartCount).toBe(0);
      expect(container.classList).not.toContain('mat-drawer-container-has-open');

      tick();
      expect(testComponent.openStartCount).toBe(1);
      fixture.detectChanges();

      expect(testComponent.openCount).toBe(1);
      expect(testComponent.openStartCount).toBe(1);
      expect(container.classList).toContain('mat-drawer-container-has-open');
    }));

    it('should be able to close', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.detectChanges();

      const testComponent: BasicTestApp = fixture.debugElement.componentInstance;
      const container = fixture.debugElement.query(By.css('mat-drawer-container'))!.nativeElement;

      fixture.debugElement.query(By.css('.open'))!.nativeElement.click();
      fixture.detectChanges();
      flush();
      fixture.detectChanges();

      fixture.debugElement.query(By.css('.close'))!.nativeElement.click();
      fixture.detectChanges();

      expect(testComponent.closeCount).toBe(0);
      expect(testComponent.closeStartCount).toBe(0);
      expect(container.classList).toContain('mat-drawer-container-has-open');

      flush();
      expect(testComponent.closeStartCount).toBe(1);
      fixture.detectChanges();

      expect(testComponent.closeCount).toBe(1);
      expect(testComponent.closeStartCount).toBe(1);
      expect(container.classList).not.toContain('mat-drawer-container-has-open');
    }));

    it('should resolve the open method promise with the new state of the drawer', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.detectChanges();
      const drawer: MatDrawer = fixture.debugElement.query(
        By.directive(MatDrawer),
      )!.componentInstance;

      drawer.open().then(result => expect(result).toBe('open'));
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
    }));

    it('should resolve the close method promise with the new state of the drawer', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.detectChanges();
      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!;
      const drawerInstance: MatDrawer = drawer.componentInstance;

      drawerInstance.open();
      fixture.detectChanges();
      flush();
      fixture.detectChanges();

      drawerInstance.close().then(result => expect(result).toBe('close'));
      fixture.detectChanges();
      flush();
      fixture.detectChanges();
    }));

    it('should be able to close while the open animation is running', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.detectChanges();

      const testComponent: BasicTestApp = fixture.debugElement.componentInstance;
      fixture.debugElement.query(By.css('.open'))!.nativeElement.click();
      fixture.detectChanges();

      expect(testComponent.openCount).toBe(0);
      expect(testComponent.closeCount).toBe(0);

      tick();
      fixture.debugElement.query(By.css('.close'))!.nativeElement.click();
      fixture.detectChanges();

      flush();
      fixture.detectChanges();

      expect(testComponent.openCount).toBe(1);
      expect(testComponent.closeCount).toBe(1);
    }));

    it('does not throw when created without a drawer', fakeAsync(() => {
      expect(() => {
        const fixture = TestBed.createComponent(BasicTestApp);
        fixture.detectChanges();
        tick();
      }).not.toThrow();
    }));

    it('should emit the backdropClick event when the backdrop is clicked', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.detectChanges();

      const testComponent: BasicTestApp = fixture.debugElement.componentInstance;
      const openButtonElement = fixture.debugElement.query(By.css('.open'))!.nativeElement;

      openButtonElement.click();
      fixture.detectChanges();
      flush();

      expect(testComponent.backdropClickedCount).toBe(0);

      fixture.debugElement.query(By.css('.mat-drawer-backdrop'))!.nativeElement.click();
      fixture.detectChanges();
      flush();

      expect(testComponent.backdropClickedCount).toBe(1);

      openButtonElement.click();
      fixture.detectChanges();
      flush();

      fixture.debugElement.query(By.css('.close'))!.nativeElement.click();
      fixture.detectChanges();
      flush();

      expect(testComponent.backdropClickedCount).toBe(1);
    }));

    it('should close when pressing escape', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);

      fixture.detectChanges();

      const testComponent: BasicTestApp = fixture.debugElement.componentInstance;
      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!;

      drawer.componentInstance.open();
      fixture.detectChanges();
      tick();

      expect(testComponent.openCount).withContext('Expected one open event.').toBe(1);
      expect(testComponent.openStartCount).withContext('Expected one open start event.').toBe(1);
      expect(testComponent.closeCount).withContext('Expected no close events.').toBe(0);
      expect(testComponent.closeStartCount).withContext('Expected no close start events.').toBe(0);

      const event = dispatchKeyboardEvent(drawer.nativeElement, 'keydown', ESCAPE);
      fixture.detectChanges();
      flush();

      expect(testComponent.closeCount).withContext('Expected one close event.').toBe(1);
      expect(testComponent.closeStartCount).withContext('Expected one close start event.').toBe(1);
      expect(event.defaultPrevented).toBe(true);
    }));

    it('should not close when pressing escape with a modifier', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);

      fixture.detectChanges();

      const testComponent: BasicTestApp = fixture.debugElement.componentInstance;
      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!;

      drawer.componentInstance.open();
      fixture.detectChanges();
      tick();

      expect(testComponent.closeCount).withContext('Expected no close events.').toBe(0);
      expect(testComponent.closeStartCount).withContext('Expected no close start events.').toBe(0);

      const event = createKeyboardEvent('keydown', ESCAPE, undefined, {alt: true});
      dispatchEvent(drawer.nativeElement, event);
      fixture.detectChanges();
      flush();

      expect(testComponent.closeCount).withContext('Expected still no close events.').toBe(0);
      expect(testComponent.closeStartCount)
        .withContext('Expected still no close start events.')
        .toBe(0);
      expect(event.defaultPrevented).toBe(false);
    }));

    it('should fire the open event when open on init', fakeAsync(() => {
      const fixture = TestBed.createComponent(DrawerSetToOpenedTrue);

      fixture.detectChanges();
      tick();

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

    it('should not close by pressing escape when disableClose is set', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      const testComponent = fixture.debugElement.componentInstance;
      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!;

      drawer.componentInstance.disableClose = true;
      fixture.changeDetectorRef.markForCheck();
      drawer.componentInstance.open();
      fixture.detectChanges();
      tick();

      dispatchKeyboardEvent(drawer.nativeElement, 'keydown', ESCAPE);
      fixture.detectChanges();
      tick();

      expect(testComponent.closeCount).toBe(0);
      expect(testComponent.closeStartCount).toBe(0);
    }));

    it('should not close by clicking on the backdrop when disableClose is set', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      const testComponent = fixture.debugElement.componentInstance;
      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!.componentInstance;

      drawer.disableClose = true;
      drawer.open();
      fixture.detectChanges();
      tick();

      fixture.debugElement.query(By.css('.mat-drawer-backdrop'))!.nativeElement.click();
      fixture.detectChanges();
      tick();

      expect(testComponent.closeCount).toBe(0);
      expect(testComponent.closeStartCount).toBe(0);
    }));

    it('should restore focus on close if backdrop has been clicked', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.detectChanges();

      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!.componentInstance;
      const openButton = fixture.componentInstance.openButton.nativeElement;

      openButton.focus();
      drawer.open();
      fixture.detectChanges();
      flush();
      fixture.detectChanges();

      const backdrop = fixture.nativeElement.querySelector('.mat-drawer-backdrop');
      expect(backdrop).toBeTruthy();

      // Ensure the element that has been focused on drawer open is blurred. This simulates
      // the behavior where clicks on the backdrop blur the active element.
      if (document.activeElement !== null && document.activeElement instanceof HTMLElement) {
        document.activeElement.blur();
      }

      backdrop.click();
      fixture.detectChanges();
      flush();

      expect(document.activeElement)
        .withContext('Expected focus to be restored to the open button on close.')
        .toBe(openButton);
    }));

    it('should restore focus on close if focus is on drawer', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);

      fixture.detectChanges();

      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!.componentInstance;
      const openButton = fixture.componentInstance.openButton.nativeElement;
      const drawerButton = fixture.componentInstance.drawerButton.nativeElement;

      openButton.focus();
      drawer.open();
      fixture.detectChanges();
      flush();
      fixture.detectChanges();
      drawerButton.focus();

      drawer.close();
      fixture.detectChanges();
      flush();

      expect(document.activeElement)
        .withContext('Expected focus to be restored to the open button on close.')
        .toBe(openButton);
    }));

    it('should restore focus to an SVG element', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.detectChanges();

      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!.componentInstance;
      const svg = fixture.componentInstance.svg.nativeElement;
      const drawerButton = fixture.componentInstance.drawerButton.nativeElement;

      svg.focus();
      drawer.open();
      fixture.detectChanges();
      flush();
      fixture.detectChanges();
      drawerButton.focus();

      drawer.close();
      fixture.detectChanges();
      flush();

      expect(document.activeElement)
        .withContext('Expected focus to be restored to the SVG element on close.')
        .toBe(svg);
    }));

    it('should not restore focus on close if focus is outside drawer', fakeAsync(() => {
      const fixture = TestBed.createComponent(BasicTestApp);
      const drawer: MatDrawer = fixture.debugElement.query(
        By.directive(MatDrawer),
      )!.componentInstance;
      fixture.detectChanges();

      const openButton = fixture.componentInstance.openButton.nativeElement;
      const closeButton = fixture.componentInstance.closeButton.nativeElement;

      openButton.focus();
      drawer.open();

      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      closeButton.focus();

      drawer.close();
      fixture.detectChanges();
      tick();

      expect(document.activeElement)
        .withContext('Expected focus not to be restored to the open button on close.')
        .toBe(closeButton);
    }));

    it('should pick up drawers that are not direct descendants', fakeAsync(() => {
      const fixture = TestBed.createComponent(IndirectDescendantDrawer);
      fixture.detectChanges();

      expect(fixture.componentInstance.drawer.opened).toBe(false);

      fixture.componentInstance.container.open();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(fixture.componentInstance.drawer.opened).toBe(true);
    }));

    it('should not pick up drawers from nested containers', fakeAsync(() => {
      const fixture = TestBed.createComponent(NestedDrawerContainers);
      const instance = fixture.componentInstance;
      fixture.detectChanges();

      expect(instance.outerDrawer.opened).toBe(false);
      expect(instance.innerDrawer.opened).toBe(false);

      instance.outerContainer.open();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(instance.outerDrawer.opened).toBe(true);
      expect(instance.innerDrawer.opened).toBe(false);

      instance.innerContainer.open();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

      expect(instance.outerDrawer.opened).toBe(true);
      expect(instance.innerDrawer.opened).toBe(true);
    }));
  });

  describe('attributes', () => {
    it('should correctly parse opened="false"', () => {
      const fixture = TestBed.createComponent(DrawerSetToOpenedFalse);

      fixture.detectChanges();

      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!.componentInstance;

      expect((drawer as MatDrawer).opened).toBe(false);
    });

    it('should correctly parse opened="true"', () => {
      const fixture = TestBed.createComponent(DrawerSetToOpenedTrue);

      fixture.detectChanges();

      const drawer = fixture.debugElement.query(By.directive(MatDrawer))!.componentInstance;

      expect((drawer as MatDrawer).opened).toBe(true);
    });

    it('should remove align attr from DOM', () => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.detectChanges();

      const drawerEl = fixture.debugElement.query(By.css('mat-drawer'))!.nativeElement;
      expect(drawerEl.hasAttribute('align'))
        .withContext('Expected drawer not to have a native align attribute.')
        .toBe(false);
    });

    it('should throw when multiple drawers have the same position', fakeAsync(() => {
      const errorHandler = jasmine.createSpyObj(['handleError']);

      TestBed.configureTestingModule({
        imports: [DrawerDynamicPosition],
        providers: [
          {
            provide: ErrorHandler,
            useValue: errorHandler,
          },
        ],
      });

      const fixture = TestBed.createComponent(DrawerDynamicPosition);
      fixture.detectChanges();
      tick();

      const testComponent: DrawerDynamicPosition = fixture.debugElement.componentInstance;
      testComponent.drawer1Position = 'end';
      fixture.changeDetectorRef.markForCheck();

      fixture.detectChanges();
      tick();

      expect(errorHandler.handleError).toHaveBeenCalled();
    }));

    it('should not throw when drawers swap positions', () => {
      const fixture = TestBed.createComponent(DrawerDynamicPosition);
      fixture.detectChanges();

      const testComponent: DrawerDynamicPosition = fixture.debugElement.componentInstance;
      testComponent.drawer1Position = 'end';
      testComponent.drawer2Position = 'start';
      fixture.changeDetectorRef.markForCheck();

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

    it('should bind 2-way bind on opened property', fakeAsync(() => {
      const fixture = TestBed.createComponent(DrawerOpenBinding);
      fixture.detectChanges();

      const drawer: MatDrawer = fixture.debugElement.query(
        By.directive(MatDrawer),
      )!.componentInstance;

      drawer.open();
      fixture.detectChanges();
      tick();

      expect(fixture.componentInstance.isOpen).toBe(true);
    }));

    it('should not throw when a two-way binding is toggled quickly while animating', fakeAsync(() => {
      TestBed.resetTestingModule().configureTestingModule({
        imports: [MatSidenavModule, BrowserAnimationsModule, DrawerOpenBinding],
      });

      const fixture = TestBed.createComponent(DrawerOpenBinding);
      fixture.detectChanges();

      // Note that we need actual timeouts and the `BrowserAnimationsModule`
      // in order to test it correctly.
      setTimeout(() => {
        fixture.componentInstance.isOpen = !fixture.componentInstance.isOpen;
        fixture.changeDetectorRef.markForCheck();
        expect(() => fixture.detectChanges()).not.toThrow();

        setTimeout(() => {
          fixture.componentInstance.isOpen = !fixture.componentInstance.isOpen;
          fixture.changeDetectorRef.markForCheck();
          expect(() => fixture.detectChanges()).not.toThrow();
        }, 1);

        tick(1);
      }, 1);

      tick(1);
    }));
  });

  describe('focus trapping behavior', () => {
    let fixture: ComponentFixture<DrawerWithFocusableElements>;
    let testComponent: DrawerWithFocusableElements;
    let drawer: MatDrawer;
    let firstFocusableElement: HTMLElement;
    let lastFocusableElement: HTMLElement;

    beforeEach(() => {
      fixture = TestBed.createComponent(DrawerWithFocusableElements);
      fixture.detectChanges();
      testComponent = fixture.debugElement.componentInstance;
      drawer = fixture.debugElement.query(By.directive(MatDrawer))!.componentInstance;
      firstFocusableElement = fixture.debugElement.query(By.css('.input1'))!.nativeElement;
      lastFocusableElement = fixture.debugElement.query(By.css('.input2'))!.nativeElement;
      lastFocusableElement.focus();
    });

    it('should trap focus when opened in "over" mode', fakeAsync(() => {
      testComponent.mode = 'over';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      lastFocusableElement.focus();

      drawer.open();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

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

    it('should trap focus when opened in "push" mode', fakeAsync(() => {
      testComponent.mode = 'push';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      lastFocusableElement.focus();

      drawer.open();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

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

    it('should trap focus when opened in "side" mode if backdrop is explicitly enabled', fakeAsync(() => {
      testComponent.mode = 'push';
      testComponent.hasBackdrop = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      lastFocusableElement.focus();

      drawer.open();
      fixture.detectChanges();
      tick();
      fixture.detectChanges();

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

    it('should not auto-focus by default when opened in "side" mode', fakeAsync(() => {
      testComponent.mode = 'side';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      lastFocusableElement.focus();

      drawer.open();
      fixture.detectChanges();
      tick();

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

    it(
      'should auto-focus to first tabbable element when opened in "side" mode' +
        'when enabled explicitly',
      fakeAsync(() => {
        drawer.autoFocus = 'first-tabbable';
        testComponent.mode = 'side';
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        lastFocusableElement.focus();

        drawer.open();
        fixture.detectChanges();
        tick();
        fixture.detectChanges();

        expect(document.activeElement).toBe(firstFocusableElement);
      }),
    );

    it(
      'should auto-focus to first tabbable element when opened in "push" mode' +
        'when backdrop is enabled explicitly',
      fakeAsync(() => {
        testComponent.mode = 'push';
        testComponent.hasBackdrop = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        lastFocusableElement.focus();

        drawer.open();
        fixture.detectChanges();
        tick();
        fixture.detectChanges();

        expect(document.activeElement).toBe(firstFocusableElement);
      }),
    );

    it('should focus the drawer if there are no focusable elements', fakeAsync(() => {
      fixture.destroy();

      const nonFocusableFixture = TestBed.createComponent(DrawerWithoutFocusableElements);
      const drawerEl = nonFocusableFixture.debugElement.query(By.directive(MatDrawer))!;
      nonFocusableFixture.detectChanges();

      drawerEl.componentInstance.open();
      nonFocusableFixture.detectChanges();
      tick();
      nonFocusableFixture.detectChanges();

      expect(document.activeElement).toBe(drawerEl.nativeElement);
    }));

    it('should be able to disable auto focus', fakeAsync(() => {
      drawer.autoFocus = 'dialog';
      testComponent.mode = 'push';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      lastFocusableElement.focus();

      drawer.open();
      fixture.detectChanges();
      tick();

      expect(document.activeElement).not.toBe(firstFocusableElement);
    }));

    it('should update the focus trap enable state if the mode changes while open', fakeAsync(() => {
      testComponent.mode = 'side';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      drawer.open();
      fixture.detectChanges();
      tick();

      const anchors = Array.from<HTMLElement>(
        fixture.nativeElement.querySelectorAll('.cdk-focus-trap-anchor'),
      );

      expect(anchors.every(anchor => !anchor.hasAttribute('tabindex')))
        .withContext('Expected focus trap anchors to be disabled in side mode.')
        .toBe(true);

      testComponent.mode = 'over';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(anchors.every(anchor => anchor.getAttribute('tabindex') === '0'))
        .withContext('Expected focus trap anchors to be enabled in over mode.')
        .toBe(true);
    }));

    it('should disable the focus trap while closed', fakeAsync(() => {
      testComponent.mode = 'over';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      flush();

      const anchors = Array.from<HTMLElement>(
        fixture.nativeElement.querySelectorAll('.cdk-focus-trap-anchor'),
      );
      expect(anchors.map(anchor => anchor.getAttribute('tabindex'))).toEqual([null, null]);

      drawer.open();
      fixture.detectChanges();
      flush();
      expect(anchors.map(anchor => anchor.getAttribute('tabindex'))).toEqual(['0', '0']);

      drawer.close();
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      flush();
      expect(anchors.map(anchor => anchor.getAttribute('tabindex'))).toEqual([null, null]);
    }));
  });

  it('should mark the drawer content as scrollable', () => {
    const fixture = TestBed.createComponent(BasicTestApp);
    fixture.detectChanges();

    const content = fixture.debugElement.query(By.css('.mat-drawer-inner-container'));
    const scrollable = content.injector.get(CdkScrollable);
    expect(scrollable).toBeTruthy();
    expect(scrollable.getElementRef().nativeElement).toBe(content.nativeElement);
  });

  describe('DOM position', () => {
    it('should project start drawer before the content', () => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.componentInstance.position = 'start';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const allNodes = getDrawerNodesArray(fixture);
      const drawerIndex = allNodes.indexOf(fixture.nativeElement.querySelector('.mat-drawer'));
      const contentIndex = allNodes.indexOf(
        fixture.nativeElement.querySelector('.mat-drawer-content'),
      );

      expect(drawerIndex)
        .withContext('Expected drawer to be inside the container')
        .toBeGreaterThan(-1);
      expect(contentIndex)
        .withContext('Expected content to be inside the container')
        .toBeGreaterThan(-1);
      expect(drawerIndex)
        .withContext('Expected drawer to be before the content')
        .toBeLessThan(contentIndex);
    });

    it('should project end drawer after the content', () => {
      const fixture = TestBed.createComponent(BasicTestApp);
      fixture.componentInstance.position = 'end';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const allNodes = getDrawerNodesArray(fixture);
      const drawerIndex = allNodes.indexOf(fixture.nativeElement.querySelector('.mat-drawer'));
      const contentIndex = allNodes.indexOf(
        fixture.nativeElement.querySelector('.mat-drawer-content'),
      );

      expect(drawerIndex)
        .withContext('Expected drawer to be inside the container')
        .toBeGreaterThan(-1);
      expect(contentIndex)
        .withContext('Expected content to be inside the container')
        .toBeGreaterThan(-1);
      expect(drawerIndex)
        .withContext('Expected drawer to be after the content')
        .toBeGreaterThan(contentIndex);
    });

    it(
      'should move the drawer before/after the content when its position changes after being ' +
        'initialized at `start`',
      () => {
        const fixture = TestBed.createComponent(BasicTestApp);
        fixture.componentInstance.position = 'start';
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        const drawer = fixture.nativeElement.querySelector('.mat-drawer');
        const content = fixture.nativeElement.querySelector('.mat-drawer-content');

        let allNodes = getDrawerNodesArray(fixture);
        const startDrawerIndex = allNodes.indexOf(drawer);
        const startContentIndex = allNodes.indexOf(content);

        expect(startDrawerIndex)
          .withContext('Expected drawer to be inside the container')
          .toBeGreaterThan(-1);
        expect(startContentIndex)
          .withContext('Expected content to be inside the container')
          .toBeGreaterThan(-1);
        expect(startDrawerIndex)
          .withContext('Expected drawer to be before the content on init')
          .toBeLessThan(startContentIndex);

        fixture.componentInstance.position = 'end';
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        allNodes = getDrawerNodesArray(fixture);

        expect(allNodes.indexOf(drawer))
          .withContext('Expected drawer to be after content when position changes to `end`')
          .toBeGreaterThan(allNodes.indexOf(content));

        fixture.componentInstance.position = 'start';
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        allNodes = getDrawerNodesArray(fixture);

        expect(allNodes.indexOf(drawer))
          .withContext('Expected drawer to be before content when position changes back to `start`')
          .toBeLessThan(allNodes.indexOf(content));
      },
    );

    it(
      'should move the drawer before/after the content when its position changes after being ' +
        'initialized at `end`',
      () => {
        const fixture = TestBed.createComponent(BasicTestApp);
        fixture.componentInstance.position = 'end';
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        const drawer = fixture.nativeElement.querySelector('.mat-drawer');
        const content = fixture.nativeElement.querySelector('.mat-drawer-content');

        let allNodes = getDrawerNodesArray(fixture);
        const startDrawerIndex = allNodes.indexOf(drawer);
        const startContentIndex = allNodes.indexOf(content);

        expect(startDrawerIndex).toBeGreaterThan(-1, 'Expected drawer to be inside the container');
        expect(startContentIndex).toBeGreaterThan(
          -1,
          'Expected content to be inside the container',
        );
        expect(startDrawerIndex).toBeGreaterThan(
          startContentIndex,
          'Expected drawer to be after the content on init',
        );

        fixture.componentInstance.position = 'start';
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        allNodes = getDrawerNodesArray(fixture);

        expect(allNodes.indexOf(drawer)).toBeLessThan(
          allNodes.indexOf(content),
          'Expected drawer to be before content when position changes to `start`',
        );

        fixture.componentInstance.position = 'end';
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
        allNodes = getDrawerNodesArray(fixture);

        expect(allNodes.indexOf(drawer)).toBeGreaterThan(
          allNodes.indexOf(content),
          'Expected drawer to be after content when position changes back to `end`',
        );
      },
    );

    function getDrawerNodesArray(fixture: ComponentFixture<any>): HTMLElement[] {
      return Array.from(fixture.nativeElement.querySelector('.mat-drawer-container').childNodes);
    }
  });
});

describe('MatDrawerContainer', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        MatSidenavModule,
        A11yModule,
        NoopAnimationsModule,
        DrawerContainerTwoDrawerTestApp,
        DrawerDelayed,
        DrawerSetToOpenedTrue,
        DrawerContainerStateChangesTestApp,
        AutosizeDrawer,
        BasicTestApp,
        DrawerContainerWithContent,
      ],
    });
  }));

  it('should be able to open and close all drawers', fakeAsync(() => {
    const fixture = TestBed.createComponent(DrawerContainerTwoDrawerTestApp);

    fixture.detectChanges();

    const testComponent: DrawerContainerTwoDrawerTestApp = fixture.debugElement.componentInstance;
    const drawers = fixture.debugElement.queryAll(By.directive(MatDrawer));

    expect(drawers.every(drawer => drawer.componentInstance.opened)).toBe(false);

    testComponent.drawerContainer.open();
    fixture.detectChanges();
    tick();

    expect(drawers.every(drawer => drawer.componentInstance.opened)).toBe(true);

    testComponent.drawerContainer.close();
    fixture.detectChanges();
    flush();

    expect(drawers.every(drawer => drawer.componentInstance.opened)).toBe(false);
  }));

  it('should animate the content when a drawer is added at a later point', fakeAsync(() => {
    const fixture = TestBed.createComponent(DrawerDelayed);

    fixture.detectChanges();

    const contentElement = fixture.debugElement.nativeElement.querySelector('.mat-drawer-content');

    expect(parseInt(contentElement.style.marginLeft)).toBeFalsy();

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

    fixture.componentInstance.drawer.open();
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    expect(parseInt(contentElement.style.marginLeft)).toBeGreaterThan(0);
  }));

  it('should recalculate the margin if a drawer is destroyed', fakeAsync(() => {
    const fixture = TestBed.createComponent(DrawerContainerStateChangesTestApp);

    fixture.detectChanges();
    fixture.componentInstance.drawer.open();
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    const contentElement = fixture.debugElement.nativeElement.querySelector('.mat-drawer-content');
    const initialMargin = parseInt(contentElement.style.marginLeft);

    expect(initialMargin).toBeGreaterThan(0);

    fixture.componentInstance.renderDrawer = false;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    tick();

    expect(contentElement.style.marginLeft).toBe('');
  }));

  it('should recalculate the margin if the drawer mode is changed', fakeAsync(() => {
    const fixture = TestBed.createComponent(DrawerContainerStateChangesTestApp);

    fixture.detectChanges();
    fixture.componentInstance.drawer.open();
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    const contentElement = fixture.debugElement.nativeElement.querySelector('.mat-drawer-content');
    const initialMargin = parseInt(contentElement.style.marginLeft);

    expect(initialMargin).toBeGreaterThan(0);

    fixture.componentInstance.mode = 'over';
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();

    expect(contentElement.style.marginLeft).toBe('');
  }));

  it('should recalculate the margin if the direction has changed', fakeAsync(() => {
    const fixture = TestBed.createComponent(DrawerContainerStateChangesTestApp);

    fixture.detectChanges();
    fixture.componentInstance.drawer.open();
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    const contentElement = fixture.debugElement.nativeElement.querySelector('.mat-drawer-content');
    const margin = parseInt(contentElement.style.marginLeft);

    expect(margin).toBeGreaterThan(0);

    fixture.componentInstance.direction = 'rtl';
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();

    expect(contentElement.style.marginLeft).toBe('');
    expect(parseInt(contentElement.style.marginRight)).toBe(margin);
  }));

  it('should not animate when the sidenav is open on load', fakeAsync(() => {
    TestBed.resetTestingModule().configureTestingModule({
      imports: [MatSidenavModule, BrowserAnimationsModule, DrawerSetToOpenedTrue],
    });

    const fixture = TestBed.createComponent(DrawerSetToOpenedTrue);

    fixture.detectChanges();
    tick();

    const container = fixture.debugElement.nativeElement.querySelector('.mat-drawer-container');

    expect(container.classList).not.toContain('mat-drawer-transition');
  }));

  it('should recalculate the margin if a drawer changes size while open in autosize mode', fakeAsync(() => {
    const fixture = TestBed.createComponent(AutosizeDrawer);
    fixture.detectChanges();

    // In M3 the drawer has a fixed default width.
    fixture.nativeElement.querySelector('.mat-drawer').style.width = 'auto';
    fixture.componentInstance.drawer.open();
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    const contentEl = fixture.debugElement.nativeElement.querySelector('.mat-drawer-content');
    const initialMargin = parseInt(contentEl.style.marginLeft);

    expect(initialMargin).toBeGreaterThan(0);

    fixture.componentInstance.fillerWidth = 200;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    tick(10);
    fixture.detectChanges();

    expect(parseInt(contentEl.style.marginLeft)).toBeGreaterThan(initialMargin);
    discardPeriodicTasks();
  }));

  it('should not set a style property if it would be zero', fakeAsync(() => {
    const fixture = TestBed.createComponent(AutosizeDrawer);
    fixture.detectChanges();

    const content = fixture.debugElement.nativeElement.querySelector('.mat-drawer-content');
    expect(content.style.marginLeft)
      .withContext('Margin should be omitted when drawer is closed')
      .toBe('');

    // Open the drawer and resolve the open animation.
    fixture.componentInstance.drawer.open();
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    expect(content.style.marginLeft).not.toBe('', 'Margin should be present when drawer is open');

    // Close the drawer and resolve the close animation.
    fixture.componentInstance.drawer.close();
    fixture.detectChanges();
    flush();
    fixture.detectChanges();

    expect(content.style.marginLeft)
      .withContext('Margin should be removed after drawer close.')
      .toBe('');

    discardPeriodicTasks();
  }));

  it('should be able to toggle whether the container has a backdrop', () => {
    const fixture = TestBed.createComponent(BasicTestApp);
    fixture.detectChanges();

    expect(fixture.nativeElement.querySelector('.mat-drawer-backdrop')).toBeTruthy();

    fixture.componentInstance.hasBackdrop = false;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();

    expect(fixture.nativeElement.querySelector('.mat-drawer-backdrop')).toBeFalsy();
  });

  it('should be able to explicitly enable the backdrop in `side` mode', fakeAsync(() => {
    const fixture = TestBed.createComponent(BasicTestApp);
    const root = fixture.nativeElement;
    fixture.detectChanges();

    fixture.componentInstance.drawer.mode = 'side';
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    fixture.componentInstance.drawer.open();
    fixture.detectChanges();
    tick();
    fixture.detectChanges();

    let backdrop = root.querySelector('.mat-drawer-backdrop.mat-drawer-shown');

    expect(backdrop).toBeFalsy();

    fixture.componentInstance.hasBackdrop = true;
    fixture.changeDetectorRef.markForCheck();
    fixture.detectChanges();
    backdrop = root.querySelector('.mat-drawer-backdrop.mat-drawer-shown');

    expect(backdrop).toBeTruthy();
    expect(fixture.componentInstance.drawer.opened).toBe(true);

    backdrop.click();
    fixture.detectChanges();
    tick();

    expect(fixture.componentInstance.drawer.opened).toBe(false);
  }));

  it('should expose a scrollable when the consumer has not specified drawer content', () => {
    const fixture = TestBed.createComponent(DrawerContainerTwoDrawerTestApp);

    fixture.detectChanges();

    expect(fixture.componentInstance.drawerContainer.scrollable instanceof CdkScrollable).toBe(
      true,
    );
  });

  it('should expose a scrollable when the consumer has specified drawer content', () => {
    const fixture = TestBed.createComponent(DrawerContainerWithContent);

    fixture.detectChanges();

    expect(fixture.componentInstance.drawerContainer.scrollable instanceof CdkScrollable).toBe(
      true,
    );
  });

  it('should clean up the drawers stream on destroy', () => {
    const fixture = TestBed.createComponent(DrawerContainerTwoDrawerTestApp);
    fixture.detectChanges();

    const spy = jasmine.createSpy('complete spy');
    const subscription = fixture.componentInstance.drawerContainer._drawers.changes.subscribe({
      complete: spy,
    });

    fixture.destroy();

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

  it('should position the drawers before/after the content in the DOM based on their position', () => {
    const fixture = TestBed.createComponent(DrawerContainerTwoDrawerTestApp);
    fixture.detectChanges();

    const drawerDebugElements = fixture.debugElement.queryAll(By.directive(MatDrawer));
    const [start, end] = drawerDebugElements.map(el => el.componentInstance);
    const [startNode, endNode] = drawerDebugElements.map(el => el.nativeElement);
    const contentNode = fixture.nativeElement.querySelector('.mat-drawer-content');
    const allNodes: HTMLElement[] = Array.from(
      fixture.nativeElement.querySelector('.mat-drawer-container').childNodes,
    );
    const startIndex = allNodes.indexOf(startNode);
    const endIndex = allNodes.indexOf(endNode);
    const contentIndex = allNodes.indexOf(contentNode);

    expect(start.position).toBe('start');
    expect(end.position).toBe('end');
    expect(contentIndex)
      .withContext('Expected content to be inside the container')
      .toBeGreaterThan(-1);
    expect(startIndex)
      .withContext('Expected start drawer to be inside the container')
      .toBeGreaterThan(-1);
    expect(endIndex)
      .withContext('Expected end drawer to be inside the container')
      .toBeGreaterThan(-1);

    expect(startIndex)
      .withContext('Expected start drawer to be before content')
      .toBeLessThan(contentIndex);
    expect(endIndex)
      .withContext('Expected end drawer to be after content')
      .toBeGreaterThan(contentIndex);
  });
});

/** Test component that contains an MatDrawerContainer but no MatDrawer. */
@Component({
  template: `<mat-drawer-container></mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerContainerNoDrawerTestApp {}

/** Test component that contains an MatDrawerContainer and 2 MatDrawer in the same position. */
@Component({
  template: `
    <mat-drawer-container>
      <mat-drawer position="start"></mat-drawer>
      <mat-drawer position="end"></mat-drawer>
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerContainerTwoDrawerTestApp {
  @ViewChild(MatDrawerContainer) drawerContainer: MatDrawerContainer;
}

/** Test component that contains an MatDrawerContainer and one MatDrawer. */
@Component({
  template: `
    <mat-drawer-container (backdropClick)="backdropClicked()" [hasBackdrop]="hasBackdrop">
      <mat-drawer #drawer="matDrawer" [position]="position"
                 (opened)="open()"
                 (openedStart)="openStart()"
                 (closed)="close()"
                 (closedStart)="closeStart()">
        <button #drawerButton>Content</button>
      </mat-drawer>
      <button (click)="drawer.open()" class="open" #openButton></button>
      <button (click)="drawer.close()" class="close" #closeButton></button>
      <svg
        viewBox="0 0 100 100"
        xmlns="http://www.w3.org/2000/svg"
        tabindex="0"
        focusable="true"
        #svg>
        <circle cx="50" cy="50" r="50"/>
      </svg>
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class BasicTestApp {
  openCount = 0;
  openStartCount = 0;
  closeCount = 0;
  closeStartCount = 0;
  backdropClickedCount = 0;
  hasBackdrop: boolean | null = null;
  position = 'start';

  @ViewChild('drawer') drawer: MatDrawer;
  @ViewChild('drawerButton') drawerButton: ElementRef<HTMLButtonElement>;
  @ViewChild('openButton') openButton: ElementRef<HTMLButtonElement>;
  @ViewChild('svg') svg: ElementRef<SVGElement>;
  @ViewChild('closeButton') closeButton: ElementRef<HTMLButtonElement>;

  open() {
    this.openCount++;
  }

  openStart() {
    this.openStartCount++;
  }

  close() {
    this.closeCount++;
  }

  closeStart() {
    this.closeStartCount++;
  }

  backdropClicked() {
    this.backdropClickedCount++;
  }
}

@Component({
  template: `
    <mat-drawer-container>
      <mat-drawer #drawer mode="side" opened="false">
        Closed Drawer.
      </mat-drawer>
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerSetToOpenedFalse {}

@Component({
  template: `
    <mat-drawer-container>
      <mat-drawer #drawer mode="side" opened="true" (opened)="openCallback()">
        Closed Drawer.
      </mat-drawer>
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerSetToOpenedTrue {
  openCallback = jasmine.createSpy('open callback');
}

@Component({
  template: `
    <mat-drawer-container>
      <mat-drawer #drawer mode="side" [(opened)]="isOpen">
        Closed Drawer.
      </mat-drawer>
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerOpenBinding {
  isOpen = false;
}

@Component({
  template: `
    <mat-drawer-container>
      <mat-drawer #drawer1 [position]="drawer1Position"></mat-drawer>
      <mat-drawer #drawer2 [position]="drawer2Position"></mat-drawer>
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerDynamicPosition {
  drawer1Position = 'start';
  drawer2Position = 'end';
}

@Component({
  // Note: we use inputs here, because they're guaranteed
  // to be focusable across all platforms.
  template: `
    <mat-drawer-container [hasBackdrop]="hasBackdrop">
      <mat-drawer position="start" [mode]="mode">
        <input type="text" class="input1"/>
      </mat-drawer>
      <input type="text" class="input2"/>
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerWithFocusableElements {
  mode: string = 'over';
  hasBackdrop: boolean | null = null;
}

@Component({
  template: `
    <mat-drawer-container>
      <mat-drawer position="start" mode="over">
        <button disabled>Not focusable</button>
      </mat-drawer>
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerWithoutFocusableElements {}

@Component({
  template: `
    <mat-drawer-container>
      @if (showDrawer) {
        <mat-drawer #drawer mode="side">Drawer</mat-drawer>
      }
    </mat-drawer-container>
  `,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerDelayed {
  @ViewChild(MatDrawer) drawer: MatDrawer;
  showDrawer = false;
}

@Component({
  template: `
    <mat-drawer-container [dir]="direction">
      @if (renderDrawer) {
        <mat-drawer [mode]="mode" style="width:100px"></mat-drawer>
      }
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerContainerStateChangesTestApp {
  @ViewChild(MatDrawer) drawer: MatDrawer;
  @ViewChild(MatDrawerContainer) drawerContainer: MatDrawerContainer;

  direction: Direction = 'ltr';
  mode = 'side';
  renderDrawer = true;
}

@Component({
  template: `
    <mat-drawer-container autosize style="min-height: 200px;">
      <mat-drawer mode="push" [position]="drawer1Position">
        Text
        <div [style.width.px]="fillerWidth" style="height: 200px; background: red;"></div>
      </mat-drawer>
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class AutosizeDrawer {
  @ViewChild(MatDrawer) drawer: MatDrawer;
  @ViewChild(MatDrawerContainer) drawerContainer: MatDrawerContainer;
  fillerWidth = 0;
}

@Component({
  template: `
    <mat-drawer-container>
      <mat-drawer>Drawer</mat-drawer>
      <mat-drawer-content>Content</mat-drawer-content>
    </mat-drawer-container>
  `,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class DrawerContainerWithContent {
  @ViewChild(MatDrawerContainer) drawerContainer: MatDrawerContainer;
}

@Component({
  // Note that we need the `@if` so that there's an embedded
  // view between the container and the drawer.
  template: `
    <mat-drawer-container #container>
      @if (true) {
        <mat-drawer #drawer>Drawer</mat-drawer>
      }
    </mat-drawer-container>`,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class IndirectDescendantDrawer {
  @ViewChild('container') container: MatDrawerContainer;
  @ViewChild('drawer') drawer: MatDrawer;
}

@Component({
  template: `
    <mat-drawer-container #outerContainer>
      <mat-drawer #outerDrawer>Drawer</mat-drawer>
      <mat-drawer-content>
        <mat-drawer-container #innerContainer>
          <mat-drawer #innerDrawer>Drawer</mat-drawer>
        </mat-drawer-container>
      </mat-drawer-content>
    </mat-drawer-container>
  `,
  standalone: true,
  imports: [MatSidenavModule, A11yModule],
})
class NestedDrawerContainers {
  @ViewChild('outerContainer') outerContainer: MatDrawerContainer;
  @ViewChild('outerDrawer') outerDrawer: MatDrawer;
  @ViewChild('innerContainer') innerContainer: MatDrawerContainer;
  @ViewChild('innerDrawer') innerDrawer: MatDrawer;
}
