import {dispatchFakeEvent} from '@angular/cdk/testing/private';
import {
  ApplicationRef,
  Component,
  DebugElement,
  EnvironmentProviders,
  Provider,
  Type,
} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {MAT_PROGRESS_BAR_DEFAULT_OPTIONS, MatProgressBarModule} from './index';
import {MatProgressBar} from './progress-bar';

describe('MatProgressBar', () => {
  function createComponent<T>(
    componentType: Type<T>,
    providers: (Provider | EnvironmentProviders)[] = [],
  ): ComponentFixture<T> {
    TestBed.configureTestingModule({
      imports: [MatProgressBarModule, componentType],
      providers,
    });

    return TestBed.createComponent<T>(componentType);
  }

  // All children need to be hidden for screen readers in order to support ChromeVox.
  // More context in the issue: https://github.com/angular/components/issues/22165.
  it('should have elements wrapped in aria-hidden div', () => {
    const fixture = createComponent(BasicProgressBar);
    const host = fixture.nativeElement as Element;
    const element = host.children[0];
    const children = element.children;
    expect(children.length).toBe(3);

    const ariaHidden = Array.from(children).map(child => child.getAttribute('aria-hidden'));
    expect(ariaHidden).toEqual(['true', 'true', 'true']);
  });

  describe('with animation', () => {
    describe('basic progress-bar', () => {
      it('should apply a mode of "determinate" if no mode is provided.', () => {
        const fixture = createComponent(BasicProgressBar);
        fixture.detectChanges();
        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        expect(progressElement.componentInstance.mode).toBe('determinate');
      });

      it('should define default values for value and bufferValue attributes', () => {
        const fixture = createComponent(BasicProgressBar);
        fixture.detectChanges();
        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        expect(progressElement.componentInstance.value).toBe(0);
        expect(progressElement.componentInstance.bufferValue).toBe(0);
      });

      it('should clamp value and bufferValue between 0 and 100', () => {
        const fixture = createComponent(BasicProgressBar);
        fixture.detectChanges();

        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        const progressComponent = progressElement.componentInstance;

        progressComponent.value = 50;
        expect(progressComponent.value).toBe(50);

        progressComponent.value = 999;
        expect(progressComponent.value).toBe(100);

        progressComponent.value = -10;
        expect(progressComponent.value).toBe(0);

        progressComponent.bufferValue = -29;
        expect(progressComponent.bufferValue).toBe(0);

        progressComponent.bufferValue = 9;
        expect(progressComponent.bufferValue).toBe(9);

        progressComponent.bufferValue = 1320;
        expect(progressComponent.bufferValue).toBe(100);
      });

      it('should set the proper transform based on the current value', () => {
        const fixture = createComponent(BasicProgressBar);
        fixture.detectChanges();

        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        const progressComponent = progressElement.componentInstance;
        const primaryStyles = progressElement.nativeElement.querySelector(
          '.mdc-linear-progress__primary-bar',
        ).style;
        const bufferStyles = progressElement.nativeElement.querySelector(
          '.mdc-linear-progress__buffer-bar',
        ).style;

        // Parse out and round the value since different
        // browsers return the value with a different precision.
        const getBufferValue = () => Math.floor(parseInt(bufferStyles.flexBasis));

        expect(primaryStyles.transform).toBe('scaleX(0)');
        expect(getBufferValue()).toBe(100);

        progressComponent.value = 40;
        fixture.detectChanges();
        expect(primaryStyles.transform).toBe('scaleX(0.4)');
        expect(getBufferValue()).toBe(100);

        progressComponent.value = 35;
        progressComponent.bufferValue = 55;
        fixture.detectChanges();
        expect(primaryStyles.transform).toBe('scaleX(0.35)');
        expect(getBufferValue()).toBe(100);

        progressComponent.mode = 'buffer';
        fixture.detectChanges();
        expect(primaryStyles.transform).toBe('scaleX(0.35)');
        expect(getBufferValue()).toEqual(55);

        progressComponent.value = 60;
        progressComponent.bufferValue = 60;
        fixture.detectChanges();
        expect(primaryStyles.transform).toBe('scaleX(0.6)');
        expect(getBufferValue()).toEqual(60);
      });

      it('should remove the `aria-valuenow` attribute in indeterminate mode', () => {
        const fixture = createComponent(BasicProgressBar);
        fixture.detectChanges();

        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        const progressComponent = progressElement.componentInstance;

        progressComponent.mode = 'determinate';
        progressComponent.value = 50;
        fixture.detectChanges();

        expect(progressElement.nativeElement.getAttribute('aria-valuenow'))
          .withContext('Expected aria-valuenow to be set in determinate mode.')
          .toBe('50');

        progressComponent.mode = 'indeterminate';
        fixture.detectChanges();

        expect(progressElement.nativeElement.hasAttribute('aria-valuenow'))
          .withContext('Expect aria-valuenow to be cleared in indeterminate mode.')
          .toBe(false);
      });

      it('should remove the `aria-valuenow` attribute in query mode', () => {
        const fixture = createComponent(BasicProgressBar);
        fixture.detectChanges();

        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        const progressComponent = progressElement.componentInstance;

        progressComponent.mode = 'determinate';
        progressComponent.value = 50;
        fixture.detectChanges();

        expect(progressElement.nativeElement.getAttribute('aria-valuenow'))
          .withContext('Expected aria-valuenow to be set in determinate mode.')
          .toBe('50');

        progressComponent.mode = 'query';
        fixture.detectChanges();

        expect(progressElement.nativeElement.hasAttribute('aria-valuenow'))
          .withContext('Expect aria-valuenow to be cleared in query mode.')
          .toBe(false);
      });

      it('should be able to configure the default progress bar options via DI', () => {
        const fixture = createComponent(BasicProgressBar, [
          {
            provide: MAT_PROGRESS_BAR_DEFAULT_OPTIONS,
            useValue: {
              mode: 'buffer',
              color: 'warn',
            },
          },
        ]);
        fixture.detectChanges();
        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        expect(progressElement.componentInstance.mode).toBe('buffer');
        expect(progressElement.componentInstance.color).toBe('warn');
      });

      it('should update the DOM transform when the value has changed', () => {
        const fixture = createComponent(BasicProgressBar);
        fixture.detectChanges();

        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        const progressComponent = progressElement.componentInstance;
        const primaryBar = progressElement.nativeElement.querySelector(
          '.mdc-linear-progress__primary-bar',
        );

        expect(primaryBar.style.transform).toBe('scaleX(0)');

        progressComponent.value = 40;
        fixture.detectChanges();

        expect(primaryBar.style.transform).toBe('scaleX(0.4)');
      });

      it('should update the DOM transform when the bufferValue has changed', () => {
        const fixture = createComponent(BasicProgressBar);
        fixture.detectChanges();

        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        const progressComponent = progressElement.componentInstance;
        const bufferBar = progressElement.nativeElement.querySelector(
          '.mdc-linear-progress__buffer-bar',
        );

        progressComponent.mode = 'buffer';
        fixture.detectChanges();

        expect(bufferBar.style.flexBasis).toBe('0%');

        progressComponent.bufferValue = 40;
        fixture.detectChanges();

        expect(bufferBar.style.flexBasis).toBe('40%');
      });
    });

    describe('animation trigger on determinate setting', () => {
      let fixture: ComponentFixture<BasicProgressBar>;
      let progressComponent: MatProgressBar;
      let primaryValueBar: DebugElement;

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

        const progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        progressComponent = progressElement.componentInstance;
        primaryValueBar = progressElement.query(By.css('.mdc-linear-progress__primary-bar'))!;
      });

      it('should trigger output event on primary value bar animation end', () => {
        fixture.detectChanges();

        const animationEndSpy = jasmine.createSpy();
        progressComponent.animationEnd.subscribe(animationEndSpy);

        progressComponent.value = 40;
        expect(animationEndSpy).not.toHaveBeenCalled();

        // On animation end, output should be emitted.
        dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);
        expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
      });
    });

    describe('animation trigger on buffer setting', () => {
      let fixture: ComponentFixture<BufferProgressBar>;
      let progressElement: DebugElement;
      let progressComponent: MatProgressBar;
      let primaryValueBar: DebugElement;

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

        progressElement = fixture.debugElement.query(By.css('mat-progress-bar'))!;
        progressComponent = progressElement.componentInstance;
        primaryValueBar = progressElement.query(By.css('.mdc-linear-progress__primary-bar'))!;
      });

      it('should bind on transitionend eventListener on primaryBarValue', () => {
        spyOn(progressElement.nativeElement, 'addEventListener');
        fixture.detectChanges();

        expect(progressElement.nativeElement.addEventListener).toHaveBeenCalled();
        expect(progressElement.nativeElement.addEventListener.calls.mostRecent().args[0]).toBe(
          'transitionend',
        );
      });

      it('should trigger output event on primary value bar animation end', () => {
        fixture.detectChanges();

        const animationEndSpy = jasmine.createSpy();
        progressComponent.animationEnd.subscribe(animationEndSpy);

        progressComponent.value = 40;
        expect(animationEndSpy).not.toHaveBeenCalled();

        // On animation end, output should be emitted.
        dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);
        expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
      });

      it('should trigger output event with value not bufferValue', () => {
        fixture.detectChanges();

        const animationEndSpy = jasmine.createSpy();
        progressComponent.animationEnd.subscribe(animationEndSpy);

        progressComponent.value = 40;
        progressComponent.bufferValue = 70;
        expect(animationEndSpy).not.toHaveBeenCalled();

        // On animation end, output should be emitted.
        dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);
        expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
      });

      it('should not run change detection if there are no `animationEnd` observers', () => {
        fixture.detectChanges();

        const animationEndSpy = jasmine.createSpy();
        const appRef = TestBed.inject(ApplicationRef);
        spyOn(appRef, 'tick');

        progressComponent.value = 30;
        progressComponent.bufferValue = 60;
        // On animation end, output should be emitted.
        dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);

        expect(appRef.tick).not.toHaveBeenCalled();

        progressComponent.animationEnd.subscribe(animationEndSpy);

        progressComponent.value = 40;
        progressComponent.bufferValue = 70;
        // On animation end, output should be emitted.
        dispatchFakeEvent(primaryValueBar.nativeElement, 'transitionend', true);

        expect(animationEndSpy).toHaveBeenCalledWith({value: 40});
      });
    });
  });
});

@Component({
  template: '<mat-progress-bar></mat-progress-bar>',
  standalone: true,
  imports: [MatProgressBar],
})
class BasicProgressBar {}

@Component({
  template: '<mat-progress-bar mode="buffer"></mat-progress-bar>',
  standalone: true,
  imports: [MatProgressBar],
})
class BufferProgressBar {}
