import {LiveAnnouncer} from '@angular/cdk/a11y';
import {OverlayContainer} from '@angular/cdk/overlay';
import {Platform} from '@angular/cdk/platform';
import {
  ChangeDetectionStrategy,
  Component,
  Directive,
  TemplateRef,
  ViewChild,
  ViewContainerRef,
  signal,
  inject,
} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, flush, tick} from '@angular/core/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {
  MAT_SNACK_BAR_DATA,
  MatSnackBar,
  MatSnackBarConfig,
  MatSnackBarContainer,
  MatSnackBarModule,
  MatSnackBarRef,
  SimpleSnackBar,
} from './index';
import {MAT_SNACK_BAR_DEFAULT_OPTIONS} from './snack-bar';

describe('MatSnackBar', () => {
  let snackBar: MatSnackBar;
  let liveAnnouncer: LiveAnnouncer;
  let overlayContainerElement: HTMLElement;

  let testViewContainerRef: ViewContainerRef;
  let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;

  let simpleMessage = 'Burritos are here!';
  let simpleActionLabel = 'pickup';

  const announceDelay = 150;
  const animationFrameDelay = 16;

  beforeEach(fakeAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        MatSnackBarModule,
        NoopAnimationsModule,
        ComponentWithChildViewContainer,
        BurritosNotification,
        DirectiveWithViewContainer,
      ],
    });

    snackBar = TestBed.inject(MatSnackBar);
    liveAnnouncer = TestBed.inject(LiveAnnouncer);
    overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
    viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
    viewContainerFixture.detectChanges();
    testViewContainerRef = viewContainerFixture.componentInstance.childViewContainer;
  }));

  it('should open with content first in the inert region', () => {
    snackBar.open('Snack time!', 'Chew');
    viewContainerFixture.detectChanges();

    const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
    const inertElement = containerElement.querySelector('[aria-hidden]')!;

    expect(inertElement.getAttribute('aria-hidden'))
      .withContext('Expected the non-live region to be aria-hidden')
      .toBe('true');
    expect(inertElement.textContent)
      .withContext('Expected non-live region to contain the snack bar content')
      .toContain('Snack time!');

    const liveElement = containerElement.querySelector('[aria-live]')!;
    expect(liveElement.childNodes.length)
      .withContext('Expected live region to not contain any content')
      .toBe(0);
  });

  it('should move content to the live region after 150ms', fakeAsync(() => {
    snackBar.open('Snack time!', 'Chew');
    viewContainerFixture.detectChanges();

    const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
    const liveElement = containerElement.querySelector('[aria-live]')!;
    tick(announceDelay);

    expect(liveElement.textContent)
      .withContext('Expected live region to contain the snack bar content')
      .toContain('Snack time!');

    const inertElement = containerElement.querySelector('[aria-hidden]')!;
    expect(inertElement)
      .withContext('Expected non-live region to not contain any content')
      .toBeFalsy();
    flush();
  }));

  it('should preserve focus when moving content to the live region', fakeAsync(() => {
    snackBar.open('Snack time!', 'Chew');
    viewContainerFixture.detectChanges();
    tick(animationFrameDelay);

    const actionButton = overlayContainerElement.querySelector(
      '.mat-mdc-simple-snack-bar .mat-mdc-snack-bar-action',
    )! as HTMLElement;
    actionButton.focus();
    expect(document.activeElement)
      .withContext('Expected the focus to move to the action button')
      .toBe(actionButton);

    flush();
    expect(document.activeElement)
      .withContext('Expected the focus to remain on the action button')
      .toBe(actionButton);
  }));

  it(
    'should have aria-live of `assertive` with an `assertive` politeness if no announcement ' +
      'message is provided',
    () => {
      snackBar.openFromComponent(BurritosNotification, {
        announcementMessage: '',
        politeness: 'assertive',
      });

      viewContainerFixture.detectChanges();

      const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
      const liveElement = containerElement.querySelector('[aria-live]')!;

      expect(liveElement.getAttribute('aria-live'))
        .withContext('Expected snack bar container live region to have aria-live="assertive"')
        .toBe('assertive');
    },
  );

  it(
    'should have aria-live of `polite` with an `assertive` politeness if an announcement ' +
      'message is provided',
    () => {
      snackBar.openFromComponent(BurritosNotification, {
        announcementMessage: 'Yay Burritos',
        politeness: 'assertive',
      });
      viewContainerFixture.detectChanges();

      const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
      const liveElement = containerElement.querySelector('[aria-live]')!;

      expect(liveElement.getAttribute('aria-live'))
        .withContext('Expected snack bar container live region to have aria-live="polite"')
        .toBe('polite');
    },
  );

  it('should have aria-live of `polite` with a `polite` politeness', () => {
    snackBar.openFromComponent(BurritosNotification, {politeness: 'polite'});
    viewContainerFixture.detectChanges();

    const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
    const liveElement = containerElement.querySelector('[aria-live]')!;

    expect(liveElement.getAttribute('aria-live'))
      .withContext('Expected snack bar container live region to have aria-live="polite"')
      .toBe('polite');
  });

  it('should have aria-live of `off` if the politeness is turned off', () => {
    snackBar.openFromComponent(BurritosNotification, {politeness: 'off'});
    viewContainerFixture.detectChanges();

    const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
    const liveElement = containerElement.querySelector('[aria-live]')!;

    expect(liveElement.getAttribute('aria-live'))
      .withContext('Expected snack bar container live region to have aria-live="off"')
      .toBe('off');
  });

  it('should have role of `alert` with an `assertive` politeness (Firefox only)', () => {
    const platform = TestBed.inject(Platform);
    snackBar.openFromComponent(BurritosNotification, {politeness: 'assertive'});
    viewContainerFixture.detectChanges();

    const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
    const liveElement = containerElement.querySelector('[aria-live]')!;

    expect(liveElement.getAttribute('role')).toBe(platform.FIREFOX ? 'alert' : null);
  });

  it('should have role of `status` with an `polite` politeness (Firefox only)', () => {
    const platform = TestBed.inject(Platform);
    snackBar.openFromComponent(BurritosNotification, {politeness: 'polite'});
    viewContainerFixture.detectChanges();

    const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
    const liveElement = containerElement.querySelector('[aria-live]')!;

    expect(liveElement.getAttribute('role')).toBe(platform.FIREFOX ? 'status' : null);
  });

  it('should have exactly one MDC label element when opened through simple snack bar', () => {
    let config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
    snackBar.open(simpleMessage, simpleActionLabel, config);
    viewContainerFixture.detectChanges();

    expect(overlayContainerElement.querySelectorAll('.mdc-snackbar__label').length).toBe(1);
  });

  it('should open and close a snackbar without a ViewContainerRef', fakeAsync(() => {
    let snackBarRef = snackBar.open('Snack time!', 'Chew');
    viewContainerFixture.detectChanges();

    let messageElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
    expect(messageElement.textContent)
      .withContext('Expected snack bar to show a message without a ViewContainerRef')
      .toContain('Snack time!');

    snackBarRef.dismiss();
    viewContainerFixture.detectChanges();
    flush();

    expect(overlayContainerElement.childNodes.length)
      .withContext('Expected snack bar to be dismissed without a ViewContainerRef')
      .toBe(0);
  }));

  it('should open a simple message with a button', () => {
    let config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
    let snackBarRef = snackBar.open(simpleMessage, simpleActionLabel, config);

    viewContainerFixture.detectChanges();

    expect(snackBarRef.instance instanceof SimpleSnackBar)
      .withContext('Expected the snack bar content component to be SimpleSnackBar')
      .toBe(true);
    expect(snackBarRef.instance.snackBarRef)
      .withContext('Expected the snack bar reference to be placed in the component instance')
      .toBe(snackBarRef);

    let messageElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
    expect(messageElement.textContent)
      .withContext(`Expected the snack bar message to be '${simpleMessage}'`)
      .toContain(simpleMessage);

    let buttonElement = overlayContainerElement.querySelector('button.mat-mdc-button')!;
    expect(buttonElement.tagName)
      .withContext('Expected snack bar action label to be a <button>')
      .toBe('BUTTON');
    expect((buttonElement.textContent || '').trim())
      .withContext(`Expected the snack bar action label to be '${simpleActionLabel}'`)
      .toBe(simpleActionLabel);
  });

  it('should open a simple message with no button', () => {
    let config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
    let snackBarRef = snackBar.open(simpleMessage, undefined, config);

    viewContainerFixture.detectChanges();

    expect(snackBarRef.instance instanceof SimpleSnackBar)
      .withContext('Expected the snack bar content component to be SimpleSnackBar')
      .toBe(true);
    expect(snackBarRef.instance.snackBarRef)
      .withContext('Expected the snack bar reference to be placed in the component instance')
      .toBe(snackBarRef);

    let messageElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;
    expect(messageElement.textContent)
      .withContext(`Expected the snack bar message to be '${simpleMessage}'`)
      .toContain(simpleMessage);
    expect(overlayContainerElement.querySelector('button.mat-mdc-button'))
      .withContext('Expected the query selection for action label to be null')
      .toBeNull();
  });

  it('should dismiss the snack bar and remove itself from the view', fakeAsync(() => {
    let config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
    let dismissCompleteSpy = jasmine.createSpy('dismiss complete spy');

    let snackBarRef = snackBar.open(simpleMessage, undefined, config);
    viewContainerFixture.detectChanges();
    flush();
    expect(overlayContainerElement.childElementCount)
      .withContext('Expected overlay container element to have at least one child')
      .toBeGreaterThan(0);

    snackBarRef.afterDismissed().subscribe({complete: dismissCompleteSpy});
    const messageElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;

    snackBarRef.dismiss();
    viewContainerFixture.detectChanges();
    expect(messageElement.hasAttribute('mat-exit'))
      .withContext('Expected the snackbar container to have the "exit" attribute upon dismiss')
      .toBe(true);

    flush();

    expect(dismissCompleteSpy).toHaveBeenCalled();
    expect(overlayContainerElement.childElementCount)
      .withContext('Expected the overlay container element to have no child elements')
      .toBe(0);
  }));

  it('should clear the announcement message if it is the same as main message', fakeAsync(() => {
    spyOn(liveAnnouncer, 'announce');

    snackBar.open(simpleMessage, undefined, {announcementMessage: simpleMessage});
    viewContainerFixture.detectChanges();
    flush();

    expect(overlayContainerElement.childElementCount)
      .withContext('Expected the overlay with the default announcement message to be added')
      .toBe(1);

    expect(liveAnnouncer.announce).not.toHaveBeenCalled();
  }));

  it('should be able to specify a custom announcement message', fakeAsync(() => {
    spyOn(liveAnnouncer, 'announce');

    snackBar.open(simpleMessage, '', {
      announcementMessage: 'Custom announcement',
      politeness: 'assertive',
    });
    viewContainerFixture.detectChanges();
    flush();

    expect(overlayContainerElement.childElementCount)
      .withContext('Expected the overlay with a custom `announcementMessage` to be added')
      .toBe(1);

    expect(liveAnnouncer.announce).toHaveBeenCalledWith('Custom announcement', 'assertive');
  }));

  it('should be able to get dismissed through the service', fakeAsync(() => {
    snackBar.open(simpleMessage);
    viewContainerFixture.detectChanges();
    expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);

    snackBar.dismiss();
    viewContainerFixture.detectChanges();
    flush();

    expect(overlayContainerElement.childElementCount).toBe(0);
  }));

  it('should clean itself up when the view container gets destroyed', fakeAsync(() => {
    snackBar.open(simpleMessage, undefined, {viewContainerRef: testViewContainerRef});
    viewContainerFixture.detectChanges();
    expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);

    viewContainerFixture.componentInstance.childComponentExists.set(false);
    viewContainerFixture.detectChanges();
    flush();

    expect(overlayContainerElement.childElementCount)
      .withContext('Expected snack bar to be removed after the view container was destroyed')
      .toBe(0);
  }));

  it('should set the animation state to visible on entry', () => {
    const config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
    const snackBarRef = snackBar.open(simpleMessage, undefined, config);

    viewContainerFixture.detectChanges();
    const container = snackBarRef.containerInstance as MatSnackBarContainer;
    expect(container._animationState)
      .withContext(`Expected the animation state would be 'visible'.`)
      .toBe('visible');
    snackBarRef.dismiss();

    viewContainerFixture.detectChanges();
    expect(container._animationState)
      .withContext(`Expected the animation state would be 'hidden'.`)
      .toBe('hidden');
  });

  it('should set the animation state to complete on exit', () => {
    const config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
    const snackBarRef = snackBar.open(simpleMessage, undefined, config);
    snackBarRef.dismiss();

    viewContainerFixture.detectChanges();
    const container = snackBarRef.containerInstance as MatSnackBarContainer;
    expect(container._animationState)
      .withContext(`Expected the animation state would be 'hidden'.`)
      .toBe('hidden');
  });

  it(`should set the old snack bar animation state to complete and the new snack bar animation
      state to visible on entry of new snack bar`, fakeAsync(() => {
    const config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
    const snackBarRef = snackBar.open(simpleMessage, undefined, config);
    const dismissCompleteSpy = jasmine.createSpy('dismiss complete spy');

    viewContainerFixture.detectChanges();

    const containerElement = document.querySelector('mat-snack-bar-container')!;
    expect(containerElement.classList).toContain('ng-animating');
    const container1 = snackBarRef.containerInstance as MatSnackBarContainer;
    expect(container1._animationState)
      .withContext(`Expected the animation state would be 'visible'.`)
      .toBe('visible');

    const config2 = {viewContainerRef: testViewContainerRef};
    const snackBarRef2 = snackBar.open(simpleMessage, undefined, config2);

    viewContainerFixture.detectChanges();
    snackBarRef.afterDismissed().subscribe({complete: dismissCompleteSpy});
    flush();

    expect(dismissCompleteSpy).toHaveBeenCalled();
    const container2 = snackBarRef2.containerInstance as MatSnackBarContainer;
    expect(container1._animationState)
      .withContext(`Expected the animation state would be 'hidden'.`)
      .toBe('hidden');
    expect(container2._animationState)
      .withContext(`Expected the animation state would be 'visible'.`)
      .toBe('visible');
  }));

  it('should open a new snackbar after dismissing a previous snackbar', fakeAsync(() => {
    let config: MatSnackBarConfig = {viewContainerRef: testViewContainerRef};
    let snackBarRef = snackBar.open(simpleMessage, 'Dismiss', config);

    viewContainerFixture.detectChanges();

    snackBarRef.dismiss();
    viewContainerFixture.detectChanges();

    // Wait for the snackbar dismiss animation to finish.
    flush();
    snackBar.open('Second snackbar');
    viewContainerFixture.detectChanges();

    // Wait for the snackbar open animation to finish.
    flush();
    expect(overlayContainerElement.textContent!.trim()).toBe('Second snackbar');
  }));

  it('should remove past snackbars when opening new snackbars', fakeAsync(() => {
    snackBar.open('First snackbar');
    viewContainerFixture.detectChanges();

    snackBar.open('Second snackbar');
    viewContainerFixture.detectChanges();
    flush();

    snackBar.open('Third snackbar');
    viewContainerFixture.detectChanges();
    flush();

    expect(overlayContainerElement.textContent!.trim()).toBe('Third snackbar');
  }));

  it('should remove snackbar if another is shown while its still animating open', fakeAsync(() => {
    snackBar.open('First snackbar');
    viewContainerFixture.detectChanges();

    snackBar.open('Second snackbar');
    viewContainerFixture.detectChanges();

    flush();
    expect(overlayContainerElement.textContent!.trim()).toBe('Second snackbar');
  }));

  it('should dismiss the snackbar when the action is called, notifying of both action and dismiss', fakeAsync(() => {
    const dismissNextSpy = jasmine.createSpy('dismiss next spy');
    const dismissCompleteSpy = jasmine.createSpy('dismiss complete spy');
    const actionNextSpy = jasmine.createSpy('action next spy');
    const actionCompleteSpy = jasmine.createSpy('action complete spy');
    const snackBarRef = snackBar.open('Some content', 'Dismiss');
    viewContainerFixture.detectChanges();

    snackBarRef.afterDismissed().subscribe({next: dismissNextSpy, complete: dismissCompleteSpy});
    snackBarRef.onAction().subscribe({next: actionNextSpy, complete: actionCompleteSpy});

    const actionButton = overlayContainerElement.querySelector(
      'button.mat-mdc-button',
    ) as HTMLButtonElement;
    actionButton.click();
    viewContainerFixture.detectChanges();
    tick();

    expect(dismissNextSpy).toHaveBeenCalled();
    expect(dismissCompleteSpy).toHaveBeenCalled();
    expect(actionNextSpy).toHaveBeenCalled();
    expect(actionCompleteSpy).toHaveBeenCalled();

    tick(500);
  }));

  it('should allow manually dismissing with an action', fakeAsync(() => {
    const dismissCompleteSpy = jasmine.createSpy('dismiss complete spy');
    const actionCompleteSpy = jasmine.createSpy('action complete spy');
    const snackBarRef = snackBar.open('Some content');
    viewContainerFixture.detectChanges();

    snackBarRef.afterDismissed().subscribe({complete: dismissCompleteSpy});
    snackBarRef.onAction().subscribe({complete: actionCompleteSpy});

    snackBarRef.dismissWithAction();
    viewContainerFixture.detectChanges();
    flush();

    expect(dismissCompleteSpy).toHaveBeenCalled();
    expect(actionCompleteSpy).toHaveBeenCalled();
  }));

  it('should indicate in `afterClosed` whether it was dismissed by an action', fakeAsync(() => {
    const dismissSpy = jasmine.createSpy('dismiss spy');
    const snackBarRef = snackBar.open('Some content');
    viewContainerFixture.detectChanges();

    snackBarRef.afterDismissed().subscribe(dismissSpy);
    snackBarRef.dismissWithAction();
    viewContainerFixture.detectChanges();
    flush();

    expect(dismissSpy).toHaveBeenCalledWith(jasmine.objectContaining({dismissedByAction: true}));
  }));

  it('should complete the onAction stream when not closing via an action', fakeAsync(() => {
    const actionCompleteSpy = jasmine.createSpy('action complete spy');
    const snackBarRef = snackBar.open('Some content');
    viewContainerFixture.detectChanges();

    snackBarRef.onAction().subscribe({complete: actionCompleteSpy});
    snackBarRef.dismiss();
    viewContainerFixture.detectChanges();
    flush();

    expect(actionCompleteSpy).toHaveBeenCalled();
  }));

  it('should dismiss automatically after a specified timeout', fakeAsync(() => {
    const config = new MatSnackBarConfig();
    config.duration = 250;
    const snackBarRef = snackBar.open('content', 'test', config);
    const afterDismissSpy = jasmine.createSpy('after dismiss spy');
    snackBarRef.afterDismissed().subscribe(afterDismissSpy);

    viewContainerFixture.detectChanges();
    tick();
    expect(afterDismissSpy).not.toHaveBeenCalled();

    tick(1000);
    viewContainerFixture.detectChanges();
    tick();
    expect(afterDismissSpy).toHaveBeenCalled();
  }));

  it('should add extra classes to the container', () => {
    snackBar.open(simpleMessage, simpleActionLabel, {panelClass: ['one', 'two']});
    viewContainerFixture.detectChanges();

    let containerClasses =
      overlayContainerElement.querySelector('mat-snack-bar-container')!.classList;

    expect(containerClasses).toContain('one');
    expect(containerClasses).toContain('two');
  });

  it('should set the layout direction', () => {
    snackBar.open(simpleMessage, simpleActionLabel, {direction: 'rtl'});
    viewContainerFixture.detectChanges();

    let pane = overlayContainerElement.querySelector('.cdk-global-overlay-wrapper')!;

    expect(pane.getAttribute('dir'))
      .withContext('Expected the pane to be in RTL mode.')
      .toBe('rtl');
  });

  it('should be able to override the default config', fakeAsync(() => {
    viewContainerFixture.destroy();

    TestBed.resetTestingModule()
      .overrideProvider(MAT_SNACK_BAR_DEFAULT_OPTIONS, {
        deps: [],
        useFactory: () => ({panelClass: 'custom-class'}),
      })
      .configureTestingModule({imports: [MatSnackBarModule, NoopAnimationsModule]});

    snackBar = TestBed.inject(MatSnackBar);
    overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();

    snackBar.open(simpleMessage);
    flush();

    expect(overlayContainerElement.querySelector('mat-snack-bar-container')!.classList)
      .withContext('Expected class applied through the defaults to be applied.')
      .toContain('custom-class');
  }));

  it('should dismiss the open snack bar on destroy', fakeAsync(() => {
    snackBar.open(simpleMessage);
    viewContainerFixture.detectChanges();
    expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);

    snackBar.ngOnDestroy();
    viewContainerFixture.detectChanges();
    flush();

    expect(overlayContainerElement.childElementCount).toBe(0);
  }));

  it('should cap the timeout to the maximum accepted delay in setTimeout', fakeAsync(() => {
    const config = new MatSnackBarConfig();
    config.duration = Infinity;
    snackBar.open('content', 'test', config);
    viewContainerFixture.detectChanges();
    spyOn(window, 'setTimeout').and.callThrough();
    tick(100);

    expect(window.setTimeout).toHaveBeenCalledWith(jasmine.any(Function), Math.pow(2, 31) - 1);

    flush();
  }));

  it('should only keep one snack bar in the DOM if multiple are opened at the same time', fakeAsync(() => {
    for (let i = 0; i < 10; i++) {
      snackBar.open('Snack time!', 'Chew');
      viewContainerFixture.detectChanges();
    }

    flush();
    expect(overlayContainerElement.querySelectorAll('mat-snack-bar-container').length).toBe(1);
  }));

  describe('with custom component', () => {
    it('should open a custom component', () => {
      const snackBarRef = snackBar.openFromComponent(BurritosNotification);

      expect(snackBarRef.instance instanceof BurritosNotification)
        .withContext('Expected the snack bar content component to be BurritosNotification')
        .toBe(true);
      expect(overlayContainerElement.textContent!.trim())
        .withContext('Expected component to have the proper text.')
        .toBe('Burritos are on the way.');
    });

    it('should inject the snack bar reference into the component', () => {
      const snackBarRef = snackBar.openFromComponent(BurritosNotification);

      expect(snackBarRef.instance.snackBarRef)
        .withContext('Expected component to have an injected snack bar reference.')
        .toBe(snackBarRef);
    });

    it('should have exactly one MDC label element', () => {
      snackBar.openFromComponent(BurritosNotification);
      viewContainerFixture.detectChanges();
      expect(overlayContainerElement.querySelectorAll('.mdc-snackbar__label').length).toBe(1);
    });

    it('should be able to inject arbitrary user data', () => {
      const snackBarRef = snackBar.openFromComponent(BurritosNotification, {
        data: {
          burritoType: 'Chimichanga',
        },
      });

      expect(snackBarRef.instance.data)
        .withContext('Expected component to have a data object.')
        .toBeTruthy();
      expect(snackBarRef.instance.data.burritoType)
        .withContext('Expected the injected data object to be the one the user provided.')
        .toBe('Chimichanga');
    });

    it('should allow manually dismissing with an action', fakeAsync(() => {
      const dismissCompleteSpy = jasmine.createSpy('dismiss complete spy');
      const actionCompleteSpy = jasmine.createSpy('action complete spy');
      const snackBarRef = snackBar.openFromComponent(BurritosNotification);
      viewContainerFixture.detectChanges();

      snackBarRef.afterDismissed().subscribe({complete: dismissCompleteSpy});
      snackBarRef.onAction().subscribe({complete: actionCompleteSpy});

      snackBarRef.dismissWithAction();
      viewContainerFixture.detectChanges();
      flush();

      expect(dismissCompleteSpy).toHaveBeenCalled();
      expect(actionCompleteSpy).toHaveBeenCalled();
    }));
  });

  describe('with TemplateRef', () => {
    let templateFixture: ComponentFixture<ComponentWithTemplateRef>;

    beforeEach(() => {
      templateFixture = TestBed.createComponent(ComponentWithTemplateRef);
      templateFixture.detectChanges();
    });

    it('should be able to open a snack bar using a TemplateRef', () => {
      templateFixture.componentInstance.localValue = 'Pizza';
      templateFixture.changeDetectorRef.markForCheck();
      snackBar.openFromTemplate(templateFixture.componentInstance.templateRef);
      templateFixture.detectChanges();

      const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;

      expect(containerElement.textContent).toContain('Fries');
      expect(containerElement.textContent).toContain('Pizza');

      templateFixture.componentInstance.localValue = 'Pasta';
      templateFixture.changeDetectorRef.markForCheck();
      templateFixture.detectChanges();

      expect(containerElement.textContent).toContain('Pasta');
    });

    it('should be able to pass in contextual data when opening with a TemplateRef', () => {
      snackBar.openFromTemplate(templateFixture.componentInstance.templateRef, {
        data: {value: 'Oranges'},
      });
      templateFixture.detectChanges();

      const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;

      expect(containerElement.textContent).toContain('Oranges');
    });
  });
});

describe('MatSnackBar with parent MatSnackBar', () => {
  let parentSnackBar: MatSnackBar;
  let childSnackBar: MatSnackBar;
  let overlayContainerElement: HTMLElement;
  let fixture: ComponentFixture<ComponentThatProvidesMatSnackBar>;

  beforeEach(fakeAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        MatSnackBarModule,
        NoopAnimationsModule,
        ComponentThatProvidesMatSnackBar,
        DirectiveWithViewContainer,
      ],
    });

    parentSnackBar = TestBed.inject(MatSnackBar);
    overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();
    fixture = TestBed.createComponent(ComponentThatProvidesMatSnackBar);
    childSnackBar = fixture.componentInstance.snackBar;
    fixture.detectChanges();
  }));

  it('should close snackBars opened by parent when opening from child', fakeAsync(() => {
    parentSnackBar.open('Pizza');
    fixture.detectChanges();
    tick(1000);

    expect(overlayContainerElement.textContent)
      .withContext('Expected a snackBar to be opened')
      .toContain('Pizza');

    childSnackBar.open('Taco');
    fixture.detectChanges();
    tick(1000);

    expect(overlayContainerElement.textContent)
      .withContext('Expected parent snackbar msg to be dismissed by opening from child')
      .toContain('Taco');
  }));

  it('should close snackBars opened by child when opening from parent', fakeAsync(() => {
    childSnackBar.open('Pizza');
    fixture.detectChanges();
    tick(1000);

    expect(overlayContainerElement.textContent)
      .withContext('Expected a snackBar to be opened')
      .toContain('Pizza');

    parentSnackBar.open('Taco');
    fixture.detectChanges();
    tick(1000);

    expect(overlayContainerElement.textContent)
      .withContext('Expected child snackbar msg to be dismissed by opening from parent')
      .toContain('Taco');
  }));

  it('should not dismiss parent snack bar if child is destroyed', fakeAsync(() => {
    parentSnackBar.open('Pizza');
    fixture.detectChanges();
    expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);

    childSnackBar.ngOnDestroy();
    fixture.detectChanges();
    flush();

    expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);
  }));
});

describe('MatSnackBar Positioning', () => {
  let snackBar: MatSnackBar;
  let overlayContainerEl: HTMLElement;

  let viewContainerFixture: ComponentFixture<ComponentWithChildViewContainer>;

  let simpleMessage = 'Burritos are here!';
  let simpleActionLabel = 'pickup';

  beforeEach(fakeAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        MatSnackBarModule,
        NoopAnimationsModule,
        ComponentWithChildViewContainer,
        DirectiveWithViewContainer,
      ],
    });

    snackBar = TestBed.inject(MatSnackBar);
    overlayContainerEl = TestBed.inject(OverlayContainer).getContainerElement();
    viewContainerFixture = TestBed.createComponent(ComponentWithChildViewContainer);
    viewContainerFixture.detectChanges();
  }));

  it('should default to bottom center', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel);

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginTop).withContext('Expected margin-top to be ""').toBe('');
    expect(overlayPaneEl.style.marginRight).withContext('Expected margin-right to be ""').toBe('');
    expect(overlayPaneEl.style.marginLeft).withContext('Expected margin-left  to be ""').toBe('');
  }));

  it('should be in the bottom left corner', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'bottom',
      horizontalPosition: 'left',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginTop).withContext('Expected margin-top to be ""').toBe('');
    expect(overlayPaneEl.style.marginRight).withContext('Expected margin-right to be ""').toBe('');
    expect(overlayPaneEl.style.marginLeft)
      .withContext('Expected margin-left  to be "0px"')
      .toBe('0px');
  }));

  it('should be in the bottom right corner', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'bottom',
      horizontalPosition: 'right',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginTop).withContext('Expected margin-top to be ""').toBe('');
    expect(overlayPaneEl.style.marginRight)
      .withContext('Expected margin-right to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginLeft).withContext('Expected margin-left  to be ""').toBe('');
  }));

  it('should be in the bottom center', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'bottom',
      horizontalPosition: 'center',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginTop).withContext('Expected margin-top to be ""').toBe('');
    expect(overlayPaneEl.style.marginRight).withContext('Expected margin-right to be ""').toBe('');
    expect(overlayPaneEl.style.marginLeft).withContext('Expected margin-left  to be ""').toBe('');
  }));

  it('should be in the top left corner', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'top',
      horizontalPosition: 'left',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be ""')
      .toBe('');
    expect(overlayPaneEl.style.marginTop)
      .withContext('Expected margin-top to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginRight).withContext('Expected margin-right to be ""').toBe('');
    expect(overlayPaneEl.style.marginLeft)
      .withContext('Expected margin-left  to be "0px"')
      .toBe('0px');
  }));

  it('should be in the top right corner', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'top',
      horizontalPosition: 'right',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be ""')
      .toBe('');
    expect(overlayPaneEl.style.marginTop)
      .withContext('Expected margin-top to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginRight)
      .withContext('Expected margin-right to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginLeft).withContext('Expected margin-left  to be ""').toBe('');
  }));

  it('should be in the top center', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'top',
      horizontalPosition: 'center',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be ""')
      .toBe('');
    expect(overlayPaneEl.style.marginTop)
      .withContext('Expected margin-top to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginRight).withContext('Expected margin-right to be ""').toBe('');
    expect(overlayPaneEl.style.marginLeft).withContext('Expected margin-left  to be ""').toBe('');
  }));

  it('should handle start based on direction (rtl)', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'top',
      horizontalPosition: 'start',
      direction: 'rtl',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be ""')
      .toBe('');
    expect(overlayPaneEl.style.marginTop)
      .withContext('Expected margin-top to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginRight)
      .withContext('Expected margin-right to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginLeft).withContext('Expected margin-left  to be ""').toBe('');
  }));

  it('should handle start based on direction (ltr)', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'top',
      horizontalPosition: 'start',
      direction: 'ltr',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be ""')
      .toBe('');
    expect(overlayPaneEl.style.marginTop)
      .withContext('Expected margin-top to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginRight).withContext('Expected margin-right to be ""').toBe('');
    expect(overlayPaneEl.style.marginLeft)
      .withContext('Expected margin-left  to be "0px"')
      .toBe('0px');
  }));

  it('should handle end based on direction (rtl)', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'top',
      horizontalPosition: 'end',
      direction: 'rtl',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be ""')
      .toBe('');
    expect(overlayPaneEl.style.marginTop)
      .withContext('Expected margin-top to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginRight).withContext('Expected margin-right to be ""').toBe('');
    expect(overlayPaneEl.style.marginLeft)
      .withContext('Expected margin-left  to be "0px"')
      .toBe('0px');
  }));

  it('should handle end based on direction (ltr)', fakeAsync(() => {
    snackBar.open(simpleMessage, simpleActionLabel, {
      verticalPosition: 'top',
      horizontalPosition: 'end',
      direction: 'ltr',
    });

    viewContainerFixture.detectChanges();
    flush();

    const overlayPaneEl = overlayContainerEl.querySelector('.cdk-overlay-pane') as HTMLElement;
    expect(overlayPaneEl.style.marginBottom)
      .withContext('Expected margin-bottom to be ""')
      .toBe('');
    expect(overlayPaneEl.style.marginTop)
      .withContext('Expected margin-top to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginRight)
      .withContext('Expected margin-right to be "0px"')
      .toBe('0px');
    expect(overlayPaneEl.style.marginLeft).withContext('Expected margin-left  to be ""').toBe('');
  }));
});

@Directive({
  selector: 'dir-with-view-container',
  standalone: true,
})
class DirectiveWithViewContainer {
  viewContainerRef = inject(ViewContainerRef);
}

@Component({
  selector: 'arbitrary-component',
  template: `@if (childComponentExists()) {<dir-with-view-container></dir-with-view-container>}`,
  standalone: true,
  imports: [DirectiveWithViewContainer],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ComponentWithChildViewContainer {
  @ViewChild(DirectiveWithViewContainer) childWithViewContainer: DirectiveWithViewContainer;

  childComponentExists = signal(true);

  get childViewContainer() {
    return this.childWithViewContainer.viewContainerRef;
  }
}

@Component({
  selector: 'arbitrary-component-with-template-ref',
  template: `
    <ng-template let-data>
      Fries {{localValue}} {{data?.value}}
    </ng-template>
  `,
  standalone: true,
})
class ComponentWithTemplateRef {
  @ViewChild(TemplateRef) templateRef: TemplateRef<any>;
  localValue: string;
}

/** Simple component for testing ComponentPortal. */
@Component({
  template: '<p>Burritos are on the way.</p>',
  standalone: true,
})
class BurritosNotification {
  snackBarRef = inject<MatSnackBarRef<BurritosNotification>>(MatSnackBarRef);
  data = inject(MAT_SNACK_BAR_DATA);
}

@Component({
  template: '',
  providers: [MatSnackBar],
  standalone: true,
})
class ComponentThatProvidesMatSnackBar {
  snackBar = inject(MatSnackBar);
}
